From 8f585b4be83fe261d14a0007eec65630d1398e75 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 6 Apr 2026 14:23:20 +0000 Subject: [PATCH] feat/ajout-de-fonctionnalites (#1) Reviewed-on: https://gitea.malio.fr/MALIO-DEV/Central/pulls/1 Co-authored-by: tristan Co-committed-by: tristan --- .env | 9 +- config/applications.yaml | 5 - config/services.yaml | 1 - docker-compose.yml | 1 + ...-04-06-phase1-applications-environments.md | 1959 +++++++++++++++++ .../2026-04-06-phase2a-deploy-versions.md | 800 +++++++ .../2026-04-06-phase2b-dashboard-sante.md | 837 +++++++ ...phase1-applications-environments-design.md | 172 ++ ...26-04-06-phase2a-deploy-versions-design.md | 155 ++ ...26-04-06-phase2b-dashboard-sante-design.md | 147 ++ frontend/components/ui/AppModal.vue | 126 ++ frontend/i18n/locales/fr.json | 130 +- frontend/layouts/default.vue | 13 +- frontend/middleware/auth.global.ts | 4 +- frontend/package-lock.json | 43 +- frontend/pages/applications/[slug].vue | 601 +++++ frontend/pages/applications/index.vue | 151 ++ frontend/pages/dashboard.vue | 96 + frontend/pages/index.vue | 176 -- frontend/services/applications.ts | 41 + frontend/services/dashboard.ts | 13 + frontend/services/deploy.ts | 13 + frontend/services/dto/application.ts | 46 + frontend/services/dto/dashboard.ts | 27 + frontend/services/dto/deploy.ts | 9 + frontend/services/dto/managed-application.ts | 10 - frontend/services/environments.ts | 36 + frontend/services/managed-applications.ts | 33 - infra/prod/Dockerfile | 2 +- infra/prod/docker-compose.yml | 7 +- migrations/Version20260406113025.php | 42 + src/ApiResource/Dashboard.php | 24 + src/ApiResource/DeployResult.php | 25 + src/ApiResource/EnvironmentHealth.php | 29 + src/ApiResource/ManagedApplication.php | 51 - src/ApiResource/TagList.php | 24 + src/DataFixtures/AppFixtures.php | 83 +- src/Entity/Application.php | 189 ++ src/Entity/Environment.php | 198 ++ src/Entity/LogFile.php | 72 + src/Repository/ApplicationRepository.php | 20 + src/Repository/EnvironmentRepository.php | 20 + src/Repository/LogFileRepository.php | 20 + src/Service/DeployService.php | 41 + src/Service/DockerService.php | 100 + src/Service/GiteaRegistryService.php | 93 + src/State/DashboardProvider.php | 50 + src/State/DeployProcessor.php | 47 + src/State/EnvironmentHealthProvider.php | 45 + src/State/MaintenanceToggleProcessor.php | 43 +- src/State/ManagedApplicationProvider.php | 56 - src/State/TagListProvider.php | 35 + 52 files changed, 6536 insertions(+), 434 deletions(-) delete mode 100644 config/applications.yaml create mode 100644 docs/superpowers/plans/2026-04-06-phase1-applications-environments.md create mode 100644 docs/superpowers/plans/2026-04-06-phase2a-deploy-versions.md create mode 100644 docs/superpowers/plans/2026-04-06-phase2b-dashboard-sante.md create mode 100644 docs/superpowers/specs/2026-04-06-phase1-applications-environments-design.md create mode 100644 docs/superpowers/specs/2026-04-06-phase2a-deploy-versions-design.md create mode 100644 docs/superpowers/specs/2026-04-06-phase2b-dashboard-sante-design.md create mode 100644 frontend/components/ui/AppModal.vue create mode 100644 frontend/pages/applications/[slug].vue create mode 100644 frontend/pages/applications/index.vue create mode 100644 frontend/pages/dashboard.vue delete mode 100644 frontend/pages/index.vue create mode 100644 frontend/services/applications.ts create mode 100644 frontend/services/dashboard.ts create mode 100644 frontend/services/deploy.ts create mode 100644 frontend/services/dto/application.ts create mode 100644 frontend/services/dto/dashboard.ts create mode 100644 frontend/services/dto/deploy.ts delete mode 100644 frontend/services/dto/managed-application.ts create mode 100644 frontend/services/environments.ts delete mode 100644 frontend/services/managed-applications.ts create mode 100644 migrations/Version20260406113025.php create mode 100644 src/ApiResource/Dashboard.php create mode 100644 src/ApiResource/DeployResult.php create mode 100644 src/ApiResource/EnvironmentHealth.php delete mode 100644 src/ApiResource/ManagedApplication.php create mode 100644 src/ApiResource/TagList.php create mode 100644 src/Entity/Application.php create mode 100644 src/Entity/Environment.php create mode 100644 src/Entity/LogFile.php create mode 100644 src/Repository/ApplicationRepository.php create mode 100644 src/Repository/EnvironmentRepository.php create mode 100644 src/Repository/LogFileRepository.php create mode 100644 src/Service/DeployService.php create mode 100644 src/Service/DockerService.php create mode 100644 src/Service/GiteaRegistryService.php create mode 100644 src/State/DashboardProvider.php create mode 100644 src/State/DeployProcessor.php create mode 100644 src/State/EnvironmentHealthProvider.php delete mode 100644 src/State/ManagedApplicationProvider.php create mode 100644 src/State/TagListProvider.php diff --git a/.env b/.env index 6f533b6..0c3cfae 100644 --- a/.env +++ b/.env @@ -44,8 +44,7 @@ DEFAULT_URI=http://localhost DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" ###< doctrine/doctrine-bundle ### -###> malio/maintenance ### -SIRH_MAINTENANCE_PATH=/var/www/maintenance/sirh/maintenance.on -LESSTIME_MAINTENANCE_PATH=/var/www/maintenance/lesstime/maintenance.on -INVENTORY_MAINTENANCE_PATH=/var/www/maintenance/inventory/maintenance.on -###< malio/maintenance ### +###> gitea ### +GITEA_API_URL=https://gitea.malio.fr +GITEA_API_TOKEN=change_me_in_env_local +###< gitea ### diff --git a/config/applications.yaml b/config/applications.yaml deleted file mode 100644 index 97abed8..0000000 --- a/config/applications.yaml +++ /dev/null @@ -1,5 +0,0 @@ -parameters: - app.managed_applications: - - { name: 'SIRH', slug: 'sirh', maintenance_path: '%env(SIRH_MAINTENANCE_PATH)%' } - - { name: 'Lesstime', slug: 'lesstime', maintenance_path: '%env(LESSTIME_MAINTENANCE_PATH)%' } - - { name: 'Inventory', slug: 'inventory', maintenance_path: '%env(INVENTORY_MAINTENANCE_PATH)%' } diff --git a/config/services.yaml b/config/services.yaml index 17985ab..fbcf601 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -4,7 +4,6 @@ parameters: imports: - { resource: version.yaml } - - { resource: applications.yaml } services: # default configuration for services in *this* file diff --git a/docker-compose.yml b/docker-compose.yml index 13929fa..2a0c757 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,7 @@ services: - ./infra/dev/xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini - ./LOG:/var/www/html/LOG - uploads_data:/var/www/html/var/uploads + - /var/run/docker.sock:/var/run/docker.sock extra_hosts: - "host.docker.internal:host-gateway" depends_on: diff --git a/docs/superpowers/plans/2026-04-06-phase1-applications-environments.md b/docs/superpowers/plans/2026-04-06-phase1-applications-environments.md new file mode 100644 index 0000000..3dad70f --- /dev/null +++ b/docs/superpowers/plans/2026-04-06-phase1-applications-environments.md @@ -0,0 +1,1959 @@ +# Phase 1 — Applications & Environments Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace static YAML-based application management with dynamic database-backed CRUD for applications, environments, and log files, with a full UI. + +**Architecture:** Three new Doctrine entities (Application, Environment, LogFile) exposed via API Platform with standard Doctrine providers. Frontend gets two new pages (list + detail) replacing the current dashboard. Old config-based system is removed entirely. + +**Tech Stack:** PHP 8.4, Symfony 8, API Platform 4, Doctrine ORM, PostgreSQL 16, Nuxt 4, Vue 3, Pinia, Tailwind CSS, @malio/layer-ui + +--- + +## File Structure + +### Backend — Create + +| File | Responsibility | +|------|---------------| +| `src/Entity/Application.php` | Doctrine entity with API Platform resource config | +| `src/Entity/Environment.php` | Doctrine entity with API Platform resource config | +| `src/Entity/LogFile.php` | Doctrine entity, embedded in Environment | +| `src/Repository/ApplicationRepository.php` | Doctrine repository for Application | +| `src/Repository/EnvironmentRepository.php` | Doctrine repository for Environment | +| `src/Repository/LogFileRepository.php` | Doctrine repository for LogFile | +| `src/State/MaintenanceToggleProcessor.php` | Rewritten processor using new entities | +| `migrations/VersionXXX.php` | Auto-generated migration for 3 new tables | + +### Backend — Modify + +| File | Change | +|------|--------| +| `src/DataFixtures/AppFixtures.php` | Add Application/Environment/LogFile seed data | +| `config/services.yaml` | Remove `applications.yaml` import | + +### Backend — Delete + +| File | Reason | +|------|--------| +| `config/applications.yaml` | Replaced by database | +| `src/ApiResource/ManagedApplication.php` | Replaced by Application entity | +| `src/State/ManagedApplicationProvider.php` | Replaced by Doctrine default provider | + +### Frontend — Create + +| File | Responsibility | +|------|---------------| +| `frontend/services/dto/application.ts` | TypeScript types for Application, Environment, LogFile | +| `frontend/services/applications.ts` | CRUD service for applications | +| `frontend/services/environments.ts` | CRUD service for environments + maintenance toggle | +| `frontend/pages/applications/index.vue` | Applications list page | +| `frontend/pages/applications/[slug].vue` | Application detail page | + +### Frontend — Modify + +| File | Change | +|------|--------| +| `frontend/layouts/default.vue` | Update sidebar link to /applications | +| `frontend/i18n/locales/fr.json` | New translation keys | + +### Frontend — Delete + +| File | Reason | +|------|--------| +| `frontend/pages/index.vue` | Replaced by /applications | +| `frontend/services/managed-applications.ts` | Replaced by new services | +| `frontend/services/dto/managed-application.ts` | Replaced by new DTOs | + +--- + +## Task 1: Application Entity + +**Files:** +- Create: `src/Entity/Application.php` +- Create: `src/Repository/ApplicationRepository.php` + +- [ ] **Step 1: Create ApplicationRepository** + +```php + + */ +class ApplicationRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Application::class); + } +} +``` + +- [ ] **Step 2: Create Application entity** + +```php + ['app:read']], + security: "is_granted('ROLE_ADMIN')", + ), + new Get( + uriVariables: ['slug'], + normalizationContext: ['groups' => ['app:read', 'app:detail']], + security: "is_granted('ROLE_ADMIN')", + ), + new Post( + security: "is_granted('ROLE_ADMIN')", + ), + new Patch( + uriVariables: ['slug'], + security: "is_granted('ROLE_ADMIN')", + ), + new Delete( + uriVariables: ['slug'], + security: "is_granted('ROLE_ADMIN')", + ), + ], + normalizationContext: ['groups' => ['app:read']], + denormalizationContext: ['groups' => ['app:write']], +)] +#[ORM\Entity(repositoryClass: ApplicationRepository::class)] +class Application +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['app:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['app:read', 'app:write'])] + private ?string $name = null; + + #[ORM\Column(length: 255, unique: true)] + #[ApiProperty(identifier: true)] + #[Groups(['app:read', 'app:write'])] + private ?string $slug = null; + + #[ORM\Column(length: 255)] + #[Groups(['app:read', 'app:write'])] + private ?string $registryImage = null; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + #[Groups(['app:read', 'app:write'])] + private ?string $description = null; + + #[ORM\Column(length: 255, nullable: true)] + #[Groups(['app:read', 'app:write'])] + private ?string $giteaUrl = null; + + #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] + #[Groups(['app:read'])] + private ?DateTimeImmutable $createdAt = null; + + /** @var Collection */ + #[ORM\OneToMany(targetEntity: Environment::class, mappedBy: 'application', cascade: ['persist', 'remove'], orphanRemoval: true)] + #[Groups(['app:detail'])] + private Collection $environments; + + public function __construct() + { + $this->createdAt = new DateTimeImmutable(); + $this->environments = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getSlug(): ?string + { + return $this->slug; + } + + public function setSlug(string $slug): static + { + $this->slug = $slug; + + return $this; + } + + public function getRegistryImage(): ?string + { + return $this->registryImage; + } + + public function setRegistryImage(string $registryImage): static + { + $this->registryImage = $registryImage; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + + return $this; + } + + public function getGiteaUrl(): ?string + { + return $this->giteaUrl; + } + + public function setGiteaUrl(?string $giteaUrl): static + { + $this->giteaUrl = $giteaUrl; + + return $this; + } + + public function getCreatedAt(): ?DateTimeImmutable + { + return $this->createdAt; + } + + /** @return Collection */ + public function getEnvironments(): Collection + { + return $this->environments; + } + + public function addEnvironment(Environment $environment): static + { + if (!$this->environments->contains($environment)) { + $this->environments->add($environment); + $environment->setApplication($this); + } + + return $this; + } + + public function removeEnvironment(Environment $environment): static + { + if ($this->environments->removeElement($environment)) { + if ($environment->getApplication() === $this) { + $environment->setApplication(null); + } + } + + return $this; + } +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/Entity/Application.php src/Repository/ApplicationRepository.php +git commit -m "feat : add Application entity with API Platform resource" +``` + +--- + +## Task 2: Environment Entity + +**Files:** +- Create: `src/Entity/Environment.php` +- Create: `src/Repository/EnvironmentRepository.php` + +- [ ] **Step 1: Create EnvironmentRepository** + +```php + + */ +class EnvironmentRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Environment::class); + } +} +``` + +- [ ] **Step 2: Create Environment entity** + +```php + new Link(fromClass: Application::class, fromProperty: 'environments'), + ], + security: "is_granted('ROLE_ADMIN')", + ), + new Patch( + security: "is_granted('ROLE_ADMIN')", + ), + new Delete( + security: "is_granted('ROLE_ADMIN')", + ), + new Post( + uriTemplate: '/environments/{id}/maintenance', + security: "is_granted('ROLE_ADMIN')", + denormalizationContext: ['groups' => ['maintenance:write']], + processor: MaintenanceToggleProcessor::class, + ), + ], + normalizationContext: ['groups' => ['env:read']], + denormalizationContext: ['groups' => ['env:write']], +)] +#[ORM\Entity(repositoryClass: EnvironmentRepository::class)] +class Environment +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['env:read', 'app:detail'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['env:read', 'env:write', 'app:detail'])] + private ?string $name = null; + + #[ORM\Column(length: 255)] + #[Groups(['env:read', 'env:write', 'app:detail'])] + private ?string $containerName = null; + + #[ORM\Column(length: 255)] + #[Groups(['env:read', 'env:write', 'app:detail'])] + private ?string $deployScriptPath = null; + + #[ORM\Column(length: 255)] + #[Groups(['env:read', 'env:write', 'app:detail'])] + private ?string $maintenanceFilePath = null; + + #[ORM\Column(length: 255, nullable: true)] + #[Groups(['env:read', 'env:write', 'app:detail'])] + private ?string $appUrl = null; + + #[ORM\ManyToOne(targetEntity: Application::class, inversedBy: 'environments')] + #[ORM\JoinColumn(nullable: false)] + private ?Application $application = null; + + /** @var Collection */ + #[ORM\OneToMany(targetEntity: LogFile::class, mappedBy: 'environment', cascade: ['persist', 'remove'], orphanRemoval: true)] + #[Groups(['env:read', 'env:write', 'app:detail'])] + private Collection $logFiles; + + #[Groups(['env:read', 'app:detail'])] + private ?bool $maintenance = null; + + public function __construct() + { + $this->logFiles = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getContainerName(): ?string + { + return $this->containerName; + } + + public function setContainerName(string $containerName): static + { + $this->containerName = $containerName; + + return $this; + } + + public function getDeployScriptPath(): ?string + { + return $this->deployScriptPath; + } + + public function setDeployScriptPath(string $deployScriptPath): static + { + $this->deployScriptPath = $deployScriptPath; + + return $this; + } + + public function getMaintenanceFilePath(): ?string + { + return $this->maintenanceFilePath; + } + + public function setMaintenanceFilePath(string $maintenanceFilePath): static + { + $this->maintenanceFilePath = $maintenanceFilePath; + + return $this; + } + + public function getAppUrl(): ?string + { + return $this->appUrl; + } + + public function setAppUrl(?string $appUrl): static + { + $this->appUrl = $appUrl; + + return $this; + } + + public function getApplication(): ?Application + { + return $this->application; + } + + public function setApplication(?Application $application): static + { + $this->application = $application; + + return $this; + } + + /** @return Collection */ + public function getLogFiles(): Collection + { + return $this->logFiles; + } + + public function addLogFile(LogFile $logFile): static + { + if (!$this->logFiles->contains($logFile)) { + $this->logFiles->add($logFile); + $logFile->setEnvironment($this); + } + + return $this; + } + + public function removeLogFile(LogFile $logFile): static + { + if ($this->logFiles->removeElement($logFile)) { + if ($logFile->getEnvironment() === $this) { + $logFile->setEnvironment(null); + } + } + + return $this; + } + + public function getMaintenance(): bool + { + return file_exists((string) $this->maintenanceFilePath); + } +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/Entity/Environment.php src/Repository/EnvironmentRepository.php +git commit -m "feat : add Environment entity with API Platform resource" +``` + +--- + +## Task 3: LogFile Entity + +**Files:** +- Create: `src/Entity/LogFile.php` +- Create: `src/Repository/LogFileRepository.php` + +- [ ] **Step 1: Create LogFileRepository** + +```php + + */ +class LogFileRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, LogFile::class); + } +} +``` + +- [ ] **Step 2: Create LogFile entity** + +```php +id; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(string $label): static + { + $this->label = $label; + + return $this; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function setPath(string $path): static + { + $this->path = $path; + + return $this; + } + + public function getEnvironment(): ?Environment + { + return $this->environment; + } + + public function setEnvironment(?Environment $environment): static + { + $this->environment = $environment; + + return $this; + } +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/Entity/LogFile.php src/Repository/LogFileRepository.php +git commit -m "feat : add LogFile entity" +``` + +--- + +## Task 4: Generate and Run Migration + +**Files:** +- Create: `migrations/VersionXXX.php` (auto-generated) + +- [ ] **Step 1: Generate migration** + +Run inside PHP container: + +```bash +make shell +php bin/console doctrine:migrations:diff +exit +``` + +Expected: a new migration file is created in `migrations/` with CREATE TABLE statements for `application`, `environment`, and `log_file`. + +- [ ] **Step 2: Review the generated migration** + +Open the generated file and verify it contains: +- `CREATE TABLE application` with columns: id, name, slug (unique), registry_image, description, gitea_url, created_at +- `CREATE TABLE environment` with columns: id, name, container_name, deploy_script_path, maintenance_file_path, app_url, application_id (FK) +- `CREATE TABLE log_file` with columns: id, label, path, environment_id (FK) +- Unique index on `application.slug` +- Foreign keys with ON DELETE CASCADE + +- [ ] **Step 3: Run migration** + +```bash +make migration-migrate +``` + +Expected: migration executes successfully. + +- [ ] **Step 4: Commit** + +```bash +git add migrations/ +git commit -m "feat : add migration for application, environment, log_file tables" +``` + +--- + +## Task 5: Rewrite MaintenanceToggleProcessor + +**Files:** +- Modify: `src/State/MaintenanceToggleProcessor.php` + +- [ ] **Step 1: Rewrite the processor to use the Environment entity** + +Replace the entire file content: + +```php +getMaintenanceFilePath(); + + if (null === $maintenancePath) { + throw new BadRequestHttpException('Maintenance file path is not configured for this environment.'); + } + + $requestData = $context['request']?->toArray() ?? []; + $enableMaintenance = $requestData['maintenance'] ?? false; + + if ($enableMaintenance) { + $directory = dirname($maintenancePath); + + if (!is_dir($directory) && !mkdir($directory, 0755, true)) { + throw new \RuntimeException(sprintf('Cannot create directory "%s".', $directory)); + } + + if (!touch($maintenancePath)) { + throw new \RuntimeException(sprintf('Cannot create maintenance file at "%s".', $maintenancePath)); + } + } elseif (file_exists($maintenancePath)) { + if (!unlink($maintenancePath)) { + throw new \RuntimeException(sprintf('Cannot remove maintenance file at "%s".', $maintenancePath)); + } + } + + return $data; + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/State/MaintenanceToggleProcessor.php +git commit -m "refactor : rewrite MaintenanceToggleProcessor for Environment entity" +``` + +--- + +## Task 6: Remove Old Application Config System + +**Files:** +- Delete: `config/applications.yaml` +- Delete: `src/ApiResource/ManagedApplication.php` +- Delete: `src/State/ManagedApplicationProvider.php` +- Modify: `config/services.yaml` + +- [ ] **Step 1: Remove the import from services.yaml** + +In `config/services.yaml`, remove the line: + +```yaml + - { resource: applications.yaml } +``` + +So the imports section becomes: + +```yaml +imports: + - { resource: version.yaml } +``` + +- [ ] **Step 2: Delete old files** + +```bash +rm config/applications.yaml +rm src/ApiResource/ManagedApplication.php +rm src/State/ManagedApplicationProvider.php +``` + +- [ ] **Step 3: Remove env variables from .env files** + +Remove `SIRH_MAINTENANCE_PATH`, `LESSTIME_MAINTENANCE_PATH`, `INVENTORY_MAINTENANCE_PATH` from `.env` and `.env.local` if they exist. + +- [ ] **Step 4: Verify the app boots** + +```bash +make cache-clear +``` + +Expected: no errors. + +- [ ] **Step 5: Commit** + +```bash +git add -A +git commit -m "refactor : remove old YAML-based application config system" +``` + +--- + +## Task 7: Update Fixtures + +**Files:** +- Modify: `src/DataFixtures/AppFixtures.php` + +- [ ] **Step 1: Add Application, Environment, and LogFile fixtures** + +Replace the entire file: + +```php +loadUsers($manager); + $this->loadApplications($manager); + + $manager->flush(); + } + + private function loadUsers(ObjectManager $manager): void + { + $admin = new User(); + $admin->setUsername('admin'); + $admin->setRoles(['ROLE_ADMIN']); + $admin->setPassword($this->passwordHasher->hashPassword($admin, 'admin')); + $manager->persist($admin); + + $userAlice = new User(); + $userAlice->setUsername('alice'); + $userAlice->setRoles(['ROLE_USER']); + $userAlice->setPassword($this->passwordHasher->hashPassword($userAlice, 'alice')); + $manager->persist($userAlice); + + $userBob = new User(); + $userBob->setUsername('bob'); + $userBob->setRoles(['ROLE_USER']); + $userBob->setPassword($this->passwordHasher->hashPassword($userBob, 'bob')); + $manager->persist($userBob); + } + + private function loadApplications(ObjectManager $manager): void + { + $sirh = new Application(); + $sirh->setName('SIRH'); + $sirh->setSlug('sirh'); + $sirh->setRegistryImage('gitea.malio.fr/malio-dev/sirh'); + $sirh->setDescription('Application de gestion des absences'); + $sirh->setGiteaUrl('https://gitea.malio.fr/malio-dev/sirh'); + + $sirhProd = new Environment(); + $sirhProd->setName('production'); + $sirhProd->setContainerName('sirh-app'); + $sirhProd->setDeployScriptPath('/home/m-tristan/workspace/SIRH/deploy/docker/deploy.sh'); + $sirhProd->setMaintenanceFilePath('/home/m-tristan/workspace/SIRH/deploy/docker/maintenance.on'); + $sirhProd->setAppUrl('https://sirh.malio-dev.fr'); + $sirh->addEnvironment($sirhProd); + + $sirhProdLog = new LogFile(); + $sirhProdLog->setLabel('prod'); + $sirhProdLog->setPath('/home/m-tristan/workspace/SIRH/var/log/prod.log'); + $sirhProd->addLogFile($sirhProdLog); + + $sirhCronLog = new LogFile(); + $sirhCronLog->setLabel('cron'); + $sirhCronLog->setPath('/home/m-tristan/workspace/SIRH/var/log/cron.log'); + $sirhProd->addLogFile($sirhCronLog); + + $manager->persist($sirh); + + $lesstime = new Application(); + $lesstime->setName('Lesstime'); + $lesstime->setSlug('lesstime'); + $lesstime->setRegistryImage('gitea.malio.fr/malio-dev/lesstime'); + $lesstime->setDescription('Application de gestion du temps'); + $lesstime->setGiteaUrl('https://gitea.malio.fr/malio-dev/lesstime'); + + $lesstimeProd = new Environment(); + $lesstimeProd->setName('production'); + $lesstimeProd->setContainerName('lesstime-app'); + $lesstimeProd->setDeployScriptPath('/home/m-tristan/workspace/lesstime/deploy/docker/deploy.sh'); + $lesstimeProd->setMaintenanceFilePath('/home/m-tristan/workspace/lesstime/deploy/docker/maintenance.on'); + $lesstimeProd->setAppUrl('https://lesstime.malio-dev.fr'); + $lesstime->addEnvironment($lesstimeProd); + + $manager->persist($lesstime); + + $inventory = new Application(); + $inventory->setName('Inventory'); + $inventory->setSlug('inventory'); + $inventory->setRegistryImage('gitea.malio.fr/malio-dev/inventory'); + $inventory->setDescription('Application de gestion des inventaires'); + $inventory->setGiteaUrl('https://gitea.malio.fr/malio-dev/inventory'); + + $inventoryProd = new Environment(); + $inventoryProd->setName('production'); + $inventoryProd->setContainerName('inventory-app'); + $inventoryProd->setDeployScriptPath('/home/m-tristan/workspace/inventory/deploy/docker/deploy.sh'); + $inventoryProd->setMaintenanceFilePath('/home/m-tristan/workspace/inventory/deploy/docker/maintenance.on'); + $inventoryProd->setAppUrl('https://inventory.malio-dev.fr'); + $inventory->addEnvironment($inventoryProd); + + $inventoryRecette = new Environment(); + $inventoryRecette->setName('recette'); + $inventoryRecette->setContainerName('inventory-test-app'); + $inventoryRecette->setDeployScriptPath('/home/m-tristan/workspace/inventory/deploy/docker/deploy-test.sh'); + $inventoryRecette->setMaintenanceFilePath('/home/m-tristan/workspace/inventory/deploy/docker/maintenance-test.on'); + $inventoryRecette->setAppUrl('https://inventory-test.malio-dev.fr'); + $inventory->addEnvironment($inventoryRecette); + + $manager->persist($inventory); + } +} +``` + +- [ ] **Step 2: Load fixtures to verify** + +```bash +make db-reset +``` + +Expected: database reset succeeds, fixtures load without errors. + +- [ ] **Step 3: Verify via API** + +```bash +# Login first +docker exec -t php-central-fpm curl -s -c /tmp/cookies -X POST http://nginx/login_check \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin"}' + +# List applications +docker exec -t php-central-fpm curl -s -b /tmp/cookies http://nginx/api/applications | python3 -m json.tool +``` + +Expected: JSON response with 3 applications (SIRH, Lesstime, Inventory). + +```bash +# Get detail with environments +docker exec -t php-central-fpm curl -s -b /tmp/cookies http://nginx/api/applications/sirh | python3 -m json.tool +``` + +Expected: SIRH application with its environments and log files nested. + +- [ ] **Step 4: Commit** + +```bash +git add src/DataFixtures/AppFixtures.php +git commit -m "feat : add application, environment, logfile fixtures" +``` + +--- + +## Task 8: Frontend TypeScript Types + +**Files:** +- Create: `frontend/services/dto/application.ts` +- Delete: `frontend/services/dto/managed-application.ts` + +- [ ] **Step 1: Create the new DTO file** + +```typescript +type LogFile = { + id?: number + label: string + path: string +} + +type Environment = { + id?: number + '@id'?: string + name: string + containerName: string + deployScriptPath: string + maintenanceFilePath: string + appUrl?: string + logFiles: LogFile[] + maintenance: boolean +} + +type EnvironmentWrite = { + name: string + containerName: string + deployScriptPath: string + maintenanceFilePath: string + appUrl?: string + logFiles: LogFile[] +} + +type Application = { + id?: number + '@id'?: string + slug: string + name: string + registryImage: string + description?: string + giteaUrl?: string + createdAt?: string + environments?: Environment[] +} + +type ApplicationWrite = { + name: string + slug: string + registryImage: string + description?: string + giteaUrl?: string +} +``` + +- [ ] **Step 2: Delete old DTO** + +```bash +rm frontend/services/dto/managed-application.ts +``` + +- [ ] **Step 3: Commit** + +```bash +git add frontend/services/dto/application.ts +git rm frontend/services/dto/managed-application.ts +git commit -m "feat : add Application/Environment/LogFile TypeScript types" +``` + +--- + +## Task 9: Frontend Application Service + +**Files:** +- Create: `frontend/services/applications.ts` +- Delete: `frontend/services/managed-applications.ts` + +- [ ] **Step 1: Create the applications service** + +```typescript +export function getApplications(): Promise { + const api = useApi() + return api.get('/applications', undefined, { + toast: false, + }).then((response) => { + if (Array.isArray(response)) { + return response + } + return extractHydraMembers(response as HydraCollection) + }) +} + +export function getApplication(slug: string): Promise { + return useApi().get(`/applications/${slug}`, undefined, { + toast: false, + }) +} + +export function createApplication(data: ApplicationWrite): Promise { + return useApi().post('/applications', data, { + toastSuccessKey: 'success.applications.create', + toastErrorKey: 'errors.applications.create', + }) +} + +export function updateApplication(slug: string, data: Partial): Promise { + return useApi().patch(`/applications/${slug}`, data, { + toastSuccessKey: 'success.applications.update', + toastErrorKey: 'errors.applications.update', + }) +} + +export function deleteApplication(slug: string): Promise { + return useApi().delete(`/applications/${slug}`, undefined, { + toastSuccessKey: 'success.applications.delete', + toastErrorKey: 'errors.applications.delete', + }) +} +``` + +- [ ] **Step 2: Delete old service** + +```bash +rm frontend/services/managed-applications.ts +``` + +- [ ] **Step 3: Commit** + +```bash +git add frontend/services/applications.ts +git rm frontend/services/managed-applications.ts +git commit -m "feat : add applications CRUD service" +``` + +--- + +## Task 10: Frontend Environments Service + +**Files:** +- Create: `frontend/services/environments.ts` + +- [ ] **Step 1: Create the environments service** + +```typescript +export function createEnvironment(appSlug: string, data: EnvironmentWrite): Promise { + return useApi().post(`/applications/${appSlug}/environments`, data, { + toastSuccessKey: 'success.environments.create', + toastErrorKey: 'errors.environments.create', + }) +} + +export function updateEnvironment(id: number, data: Partial): Promise { + return useApi().patch(`/environments/${id}`, data, { + toastSuccessKey: 'success.environments.update', + toastErrorKey: 'errors.environments.update', + }) +} + +export function deleteEnvironment(id: number): Promise { + return useApi().delete(`/environments/${id}`, undefined, { + toastSuccessKey: 'success.environments.delete', + toastErrorKey: 'errors.environments.delete', + }) +} + +export function toggleMaintenance(id: number, maintenance: boolean): Promise { + const successKey = maintenance + ? 'success.environments.activateMaintenance' + : 'success.environments.deactivateMaintenance' + const errorKey = maintenance + ? 'errors.environments.activateMaintenance' + : 'errors.environments.deactivateMaintenance' + + return useApi().post(`/environments/${id}/maintenance`, { maintenance }, { + toastSuccessKey: successKey, + toastErrorKey: errorKey, + }) +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add frontend/services/environments.ts +git commit -m "feat : add environments service with maintenance toggle" +``` + +--- + +## Task 11: Frontend i18n + +**Files:** +- Modify: `frontend/i18n/locales/fr.json` + +- [ ] **Step 1: Update translations** + +Add/replace the following keys in `fr.json`. Keep existing keys for `errors.http.*`, `errors.auth.*`, `success.auth.*`, and `dashboard.*`. Replace the old `applications.*` and `success.applications.*` / `errors.applications.*` sections entirely: + +```json +{ + "errors": { + "http": { "...existing..." }, + "applications": { + "create": "Erreur lors de la creation de l'application", + "update": "Erreur lors de la modification de l'application", + "delete": "Erreur lors de la suppression de l'application", + "load": "Erreur lors du chargement des applications" + }, + "environments": { + "create": "Erreur lors de la creation de l'environnement", + "update": "Erreur lors de la modification de l'environnement", + "delete": "Erreur lors de la suppression de l'environnement", + "activateMaintenance": "Erreur lors de l'activation de la maintenance", + "deactivateMaintenance": "Erreur lors de la desactivation de la maintenance" + }, + "auth": { "...existing..." } + }, + "success": { + "applications": { + "create": "Application creee avec succes", + "update": "Application modifiee avec succes", + "delete": "Application supprimee avec succes" + }, + "environments": { + "create": "Environnement cree avec succes", + "update": "Environnement modifie avec succes", + "delete": "Environnement supprime avec succes", + "activateMaintenance": "Maintenance activee", + "deactivateMaintenance": "Maintenance desactivee" + }, + "auth": { "...existing..." } + }, + "applications": { + "title": "Applications", + "description": "Gerer les applications du SI", + "addButton": "Ajouter une application", + "emptyTitle": "Aucune application", + "emptyDescription": "Aucune application configuree pour le moment.", + "card": { + "environments": "environnement | environnements", + "noEnvironments": "Aucun environnement configure" + }, + "detail": { + "title": "Detail de l'application", + "registryImage": "Image registry", + "giteaUrl": "Depot Gitea", + "description": "Description", + "editButton": "Modifier", + "deleteButton": "Supprimer", + "deleteConfirm": "Etes-vous sur de vouloir supprimer cette application et tous ses environnements ?" + }, + "form": { + "name": "Nom", + "slug": "Slug", + "registryImage": "Image registry", + "description": "Description", + "giteaUrl": "URL Gitea", + "save": "Enregistrer", + "cancel": "Annuler" + } + }, + "environments": { + "title": "Environnements", + "addButton": "Ajouter un environnement", + "maintenance": { + "active": "Maintenance active", + "inactive": "En ligne", + "activate": "Activer la maintenance", + "deactivate": "Desactiver la maintenance", + "pending": "En cours..." + }, + "editButton": "Modifier", + "deleteButton": "Supprimer", + "deleteConfirm": "Etes-vous sur de vouloir supprimer cet environnement ?", + "form": { + "name": "Nom", + "containerName": "Nom du container", + "deployScriptPath": "Chemin du script de deploiement", + "maintenanceFilePath": "Chemin du fichier de maintenance", + "appUrl": "URL de l'application", + "save": "Enregistrer", + "cancel": "Annuler" + }, + "logFiles": { + "title": "Fichiers de log", + "addButton": "Ajouter un fichier de log", + "label": "Label", + "path": "Chemin", + "remove": "Retirer" + } + } +} +``` + +Note: merge these keys into the existing `fr.json`, preserving `errors.http.*`, `errors.auth.*`, `success.auth.*`, and `dashboard.*` as-is. + +- [ ] **Step 2: Commit** + +```bash +git add frontend/i18n/locales/fr.json +git commit -m "feat : add i18n translations for applications and environments" +``` + +--- + +## Task 12: Applications List Page + +**Files:** +- Create: `frontend/pages/applications/index.vue` +- Delete: `frontend/pages/index.vue` +- Modify: `frontend/layouts/default.vue` + +- [ ] **Step 1: Create the applications list page** + +```vue + + +