# 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