Compare commits

..

46 Commits

Author SHA1 Message Date
tristan 96c5f3bab0 fix : revert to node:lts-alpine and regenerate package-lock.json
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:28:08 +02:00
tristan 27b9c39f26 fix : refresh button style 2026-04-06 16:22:12 +02:00
tristan b297de862c feat : add health metrics block on environment detail
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:12:04 +02:00
tristan 75530ea46a feat : add Dashboard to sidebar and redirect / to /dashboard 2026-04-06 16:10:50 +02:00
tristan 14ee2f65db feat : add dashboard page with container status overview 2026-04-06 16:10:37 +02:00
tristan 114a29eb0f feat : add i18n translations for dashboard and health
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:09:38 +02:00
tristan ce9b9a598b feat : add frontend dashboard types and service
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:09:27 +02:00
tristan a2eab27da1 feat : add EnvironmentHealthProvider for detailed env metrics 2026-04-06 16:08:43 +02:00
tristan e2000352f5 feat : add DashboardProvider for container status overview 2026-04-06 16:08:41 +02:00
tristan 0acb8b28e5 feat : add Dashboard and EnvironmentHealth API Platform DTOs 2026-04-06 16:08:02 +02:00
tristan 15ad990756 feat : add DockerService for container status and stats 2026-04-06 16:07:52 +02:00
tristan ae8e033152 docs : plan d'implementation phase 2b dashboard sante
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:05:04 +02:00
tristan f487c929fd docs : spec phase 2b dashboard sante
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:02:57 +02:00
tristan 7127bdfdfc fix : use Docker Registry V2 Bearer token auth for Gitea
The Gitea container registry requires a two-step auth flow:
1. Get Bearer token from /v2/token with Basic auth
2. Use Bearer token for /v2/{owner}/{package}/tags/list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:53:15 +02:00
tristan 5d2aba634e feat : add deploy modal with tag selection and result display 2026-04-06 15:25:56 +02:00
tristan 046feab8ac feat : add i18n translations for deploy feature 2026-04-06 15:24:30 +02:00
tristan aa42a0ee35 feat : add frontend deploy types and service 2026-04-06 15:24:17 +02:00
tristan 73849b3ef8 feat : add DeployProcessor for triggering deployments
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 15:23:39 +02:00
tristan 2107e95188 feat : add TagListProvider for listing registry tags
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 15:23:30 +02:00
tristan d6948a8155 feat : add TagList and DeployResult API Platform DTOs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 15:23:23 +02:00
tristan d9aebe2be2 feat : add DeployService for executing deploy scripts 2026-04-06 15:22:29 +02:00
tristan 0dbde148b7 feat : add GiteaRegistryService for listing container tags 2026-04-06 15:22:22 +02:00
tristan c1014de637 feat : add Gitea env vars, mount Docker socket and deploy dirs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:21:38 +02:00
tristan 6d6b3e171c docs : plan d'implementation phase 2a deploy et versions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:19:12 +02:00
tristan db71cc92ee docs : spec phase 2a deploiement de version et versions disponibles
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:12:17 +02:00
tristan e6aec7d95a fix : use MalioButton/MalioButtonIcon everywhere, fix env count and fixture URLs
- Replace all native HTML buttons with MalioButton and MalioButtonIcon components
- Add app:read group on environments relation to fix 0 count in list
- Fix fixture URLs (http for apps, https for gitea)
- Maintenance icons: alert-outline (activate) / shield-check-outline (deactivate)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:38:34 +02:00
tristan 4a43770238 fix : add labels on log file fields and use MalioButtonIcon for delete
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:05:28 +02:00
tristan 157d7c96b9 style : use modal component for forms based on Lesstime pattern
- Create reusable AppModal component (Teleport, backdrop blur, transitions)
- Replace inline forms with modals on list and detail pages
- Consistent with Lesstime TaskModal design (header, body scroll, footer)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:02:51 +02:00
tristan 3a3a46992c style : align design with SIRH/Lesstime visual patterns
- Remove max-width constraint, use responsive padding (px-4 sm:px-8 lg:px-16)
- Replace heavy border cards with bg-tertiary-500 backgrounds
- Use primary-500 colored titles like SIRH
- Use neutral color palette instead of m-* custom colors
- Auto-fill grid for responsive card layout
- Consistent button styles with SIRH/Lesstime patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:51:23 +02:00
tristan 8366e00017 fix : add missing imports in services and pages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:46:47 +02:00
tristan df51feaba6 feat : redirect authenticated users to /applications
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:40:34 +02:00
tristan e1dd04488e feat : add application detail page with environment management
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:39:56 +02:00
tristan 0ec551e717 feat : add applications list page, replace old dashboard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:37:44 +02:00
tristan 71c0821248 feat : add i18n translations for applications and environments 2026-04-06 13:36:09 +02:00
tristan e3044f82b0 feat : add environments service with maintenance toggle 2026-04-06 13:35:26 +02:00
tristan 9673ea0125 feat : add applications CRUD service 2026-04-06 13:35:14 +02:00
tristan a2e849e168 feat : add Application/Environment/LogFile TypeScript types 2026-04-06 13:34:48 +02:00
tristan 3ffe7904b3 feat : add application, environment, logfile fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:33:36 +02:00
tristan 467c2fcef8 refactor : remove old YAML-based application config system
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:32:17 +02:00
tristan bf62ccf9e9 refactor : rewrite MaintenanceToggleProcessor for Environment entity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:31:33 +02:00
tristan 77dc804547 feat : add migration for application, environment, log_file tables
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:30:44 +02:00
tristan 25ca2fb6ca feat : add LogFile entity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:30:01 +02:00
tristan d292ddbbbe feat : add Environment entity with API Platform resource
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:29:41 +02:00
tristan 82169b254c feat : add Application entity with API Platform resource
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:29:01 +02:00
tristan 6f26202bcb docs : plan d'implementation phase 1 applications et environnements
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:25:06 +02:00
tristan 969ca37d9f docs : spec phase 1 gestion applications et environnements
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:17:47 +02:00
19 changed files with 134 additions and 311 deletions
-4
View File
@@ -44,10 +44,6 @@ DEFAULT_URI=http://localhost
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
###< doctrine/doctrine-bundle ###
###> apps ###
APPS_BASE_PATH=/mnt/apps
###< apps ###
###> gitea ###
GITEA_API_URL=https://gitea.malio.fr
GITEA_API_TOKEN=change_me_in_env_local
-1
View File
@@ -26,7 +26,6 @@
"symfony/http-client": "8.0.*",
"symfony/mime": "8.0.*",
"symfony/monolog-bundle": "^4.0",
"symfony/process": "8.0.*",
"symfony/property-access": "8.0.*",
"symfony/property-info": "8.0.*",
"symfony/rate-limiter": "8.0.*",
Generated
+66 -66
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "51813b5c3b6dacd3cc99cfe121ab918b",
"content-hash": "bfd26e903d79f710cfe95452c05f2a25",
"packages": [
{
"name": "api-platform/doctrine-common",
@@ -6234,71 +6234,6 @@
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/process",
"version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
"shasum": ""
},
"require": {
"php": ">=8.4"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Process\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v8.0.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/property-access",
"version": "v8.0.8",
@@ -11083,6 +11018,71 @@
],
"time": "2024-10-20T05:08:20+00:00"
},
{
"name": "symfony/process",
"version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
"shasum": ""
},
"require": {
"php": ">=8.4"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Process\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v8.0.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-30T15:14:47+00:00"
},
{
"name": "theseer/tokenizer",
"version": "2.0.1",
+1 -1
View File
@@ -1,2 +1,2 @@
parameters:
app.version: '0.1.20'
app.version: '0.1.11'
+1 -1
View File
@@ -37,7 +37,7 @@
<!-- Footer -->
<div class="border-t border-neutral-100 px-4 py-4 sm:px-8">
<div class="flex justify-center gap-3">
<div class="flex justify-end gap-3">
<slot name="footer">
<MalioButton
:label="cancelLabel"
-1
View File
@@ -106,7 +106,6 @@
"containerName": "Nom du container",
"deployScriptPath": "Chemin du script de deploiement",
"maintenanceFilePath": "Chemin du fichier de maintenance",
"pathHint": "Prefixe automatique : /mnt/apps",
"appUrl": "URL de l'application",
"save": "Enregistrer",
"cancel": "Annuler"
+4 -4
View File
@@ -7,7 +7,7 @@
"name": "nuxt-app",
"hasInstallScript": true,
"dependencies": {
"@malio/layer-ui": "^1.2.2",
"@malio/layer-ui": "^1.2.0",
"@nuxt/icon": "^2.2.1",
"@nuxtjs/i18n": "^10.2.3",
"@nuxtjs/tailwindcss": "^6.14.0",
@@ -1668,9 +1668,9 @@
"license": "MIT"
},
"node_modules/@malio/layer-ui": {
"version": "1.2.2",
"resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.2.2/layer-ui-1.2.2.tgz",
"integrity": "sha512-nV4FL19rYSiXqMDTUlAtp6AYdj7YiwpHbf7/usiOPj7llpjHIC3GmcOX0X7oQeOMTtSU1aKL8k8wn1bhptrHYg==",
"version": "1.2.1",
"resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.2.1/layer-ui-1.2.1.tgz",
"integrity": "sha512-kY6Jeg11wceSgeJ/OX0xsYMENfXogb+nGduP7yVmc6HHIwKDtpn7VLRcJPlhNBUsKAvcFNk6IU08o6izdTMEQg==",
"dependencies": {
"@nuxt/icon": "^2.2.1",
"@nuxtjs/tailwindcss": "^6.14.0",
+1 -1
View File
@@ -11,7 +11,7 @@
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
},
"dependencies": {
"@malio/layer-ui": "^1.2.2",
"@malio/layer-ui": "^1.2.0",
"@nuxt/icon": "^2.2.1",
"@nuxtjs/i18n": "^10.2.3",
"@nuxtjs/tailwindcss": "^6.14.0",
+20 -36
View File
@@ -256,35 +256,32 @@ onMounted(loadApplication)
<p v-if="application.description" class="text-neutral-500 mt-2">{{ application.description }}</p>
</div>
<div class="flex gap-2">
<div class="flex items-center">
<MalioButtonIcon
:aria-label="t('applications.detail.editButton')"
variant="filled"
icon="mdi:pencil"
<MalioButton
:label="t('applications.detail.editButton')"
variant="secondary"
icon-name="mdi:pencil"
icon-position="left"
@click="openEditAppModal"
/>
</div>
<div class="flex items-center">
<MalioButtonIcon
:aria-label="t('applications.detail.editButton')"
variant="filled"
icon="mdi:trash-can-outline"
button-class="bg-m-btn-danger hover:bg-m-btn-danger-hover active:bg-m-btn-danger-active text-white cursor-pointer"
<MalioButton
:label="t('applications.detail.deleteButton')"
variant="danger"
icon-name="mdi:trash-can-outline"
icon-position="left"
@click="handleDeleteApp"
/>
</div>
</div>
</div>
<!-- Application info -->
<div class="rounded-lg bg-tertiary-500 p-5 mb-8">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<span class="font-bold">{{ t('applications.detail.registryImage') }} :</span>
<span class="text-neutral-400">{{ t('applications.detail.registryImage') }} :</span>
<span class="text-neutral-800 ml-1 font-mono">{{ application.registryImage }}</span>
</div>
<div v-if="application.giteaUrl">
<span class="font-bold">{{ t('applications.detail.giteaUrl') }} :</span>
<span class="text-neutral-400">{{ t('applications.detail.giteaUrl') }} :</span>
<a :href="application.giteaUrl" target="_blank" class="text-primary-500 hover:underline ml-1">
{{ application.giteaUrl }}
</a>
@@ -357,7 +354,7 @@ onMounted(loadApplication)
<!-- Log files -->
<div v-if="env.logFiles.length" class="mt-4 border-t border-neutral-200 pt-3">
<p class="text-sm font-bold uppercase tracking-wider mb-2">{{ t('environments.logFiles.title') }}</p>
<p class="text-xs font-semibold uppercase tracking-wider text-neutral-400 mb-2">{{ t('environments.logFiles.title') }}</p>
<div v-for="lf in env.logFiles" :key="lf.id" class="text-sm text-neutral-700 flex gap-2">
<span class="font-medium">{{ lf.label }} :</span>
<span class="font-mono text-neutral-400">{{ lf.path }}</span>
@@ -365,9 +362,9 @@ onMounted(loadApplication)
</div>
<!-- Health metrics -->
<div v-if="healthByEnvId[env.id!]" class="mt-4 border-t border-neutral-200 py-3">
<p class="text-sm font-bold uppercase tracking-wider mb-2">{{ t('environments.health.title') }}</p>
<div class="grid grid-cols-2 sm:grid-cols-5 gap-3">
<div v-if="healthByEnvId[env.id!]" class="mt-4 border-t border-neutral-200 pt-3">
<p class="text-xs font-semibold uppercase tracking-wider text-neutral-400 mb-3">{{ t('environments.health.title') }}</p>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div>
<p class="text-xs text-neutral-400">{{ t('environments.health.status') }}</p>
<span
@@ -389,7 +386,7 @@ onMounted(loadApplication)
<p class="text-xs text-neutral-400">{{ t('environments.health.cpu') }}</p>
<p class="text-sm text-neutral-800 mt-1">{{ healthByEnvId[env.id!].cpuPercent }}%</p>
</div>
<div>
<div class="col-span-2">
<p class="text-xs text-neutral-400">{{ t('environments.health.memory') }}</p>
<p class="text-sm text-neutral-800 mt-1">
{{ healthByEnvId[env.id!].memoryUsage }} / {{ healthByEnvId[env.id!].memoryLimit }}
@@ -443,13 +440,11 @@ onMounted(loadApplication)
<MalioInputText
v-model="editForm.registryImage"
:label="t('applications.form.registryImage')"
hint="Ex : gitea.malio.fr/malio-dev/sirh"
required
/>
<MalioInputText
v-model="editForm.giteaUrl"
:label="t('applications.form.giteaUrl')"
hint="Ex : https://gitea.malio.fr/malio-dev/sirh"
/>
</div>
<div>
@@ -478,50 +473,40 @@ onMounted(loadApplication)
<MalioInputText
v-model="envForm.name"
:label="t('environments.form.name')"
groupClass="mt-0"
required
/>
<MalioInputText
v-model="envForm.containerName"
:label="t('environments.form.containerName')"
groupClass="mt-0"
required
/>
<MalioInputText
v-model="envForm.deployScriptPath"
:label="t('environments.form.deployScriptPath')"
:hint="t('environments.form.pathHint')"
required
/>
<MalioInputText
v-model="envForm.maintenanceFilePath"
:label="t('environments.form.maintenanceFilePath')"
:hint="t('environments.form.pathHint')"
required
/>
<MalioInputText
v-model="envForm.appUrl"
:label="t('environments.form.appUrl')"
groupClass="mt-0"
/>
</div>
<!-- Log files -->
<div>
<div class="flex items-center justify-between mb-4">
<p class="text-md font-bold">{{ t('environments.logFiles.title') }}</p>
<div class="flex items-center justify-between mb-2">
<p class="text-sm font-medium text-neutral-700">{{ t('environments.logFiles.title') }}</p>
<button type="button" @click="addLogFile" class="text-primary-500 hover:underline text-sm font-semibold">
+ {{ t('environments.logFiles.addButton') }}
</button>
</div>
<div v-for="(lf, index) in envForm.logFiles" :key="index" class="flex gap-2 mb-2">
<div class="w-1/3">
<MalioInputText v-model="lf.label" :label="t('environments.logFiles.label')" groupClass="mt-0" inputClass="flex-1" required />
</div>
<div class="w-2/3">
<MalioInputText v-model="lf.path" :label="t('environments.logFiles.path')" groupClass="mt-0" inputClass="flex-[2]" :hint="t('environments.form.pathHint')" required />
</div>
<div class="h-[46px] flex items-center">
<MalioInputText v-model="lf.path" :label="t('environments.logFiles.path')" groupClass="mt-0" inputClass="flex-[2]" required />
<MalioButtonIcon
icon="mdi:delete-outline"
:aria-label="t('environments.logFiles.remove')"
@@ -532,7 +517,6 @@ onMounted(loadApplication)
/>
</div>
</div>
</div>
</form>
</AppModal>
-2
View File
@@ -130,13 +130,11 @@ onMounted(loadApplications)
<MalioInputText
v-model="createForm.registryImage"
:label="t('applications.form.registryImage')"
hint="Ex : gitea.malio.fr/malio-dev/sirh"
required
/>
<MalioInputText
v-model="createForm.giteaUrl"
:label="t('applications.form.giteaUrl')"
hint="Ex : https://gitea.malio.fr/malio-dev/sirh"
/>
</div>
<div>
+4 -6
View File
@@ -71,12 +71,10 @@ COPY --from=frontend-build /app/frontend/.output/public /var/www/html/frontend/.
# Symfony needs a .env file to boot (variables are overridden by env_file in docker-compose)
RUN echo "APP_ENV=prod" > /var/www/html/.env
# Permissions + directories
RUN mkdir -p /var/www/html/var/log /var/www/html/var/uploads \
&& chown -R www-data:www-data /var/www/html/var
# Allow www-data to use Docker socket (GID 987 matches host's docker group)
RUN groupadd -g 987 dockerhost 2>/dev/null; usermod -aG dockerhost www-data
# Permissions
RUN mkdir -p /var/www/html/var /var/www/html/var/uploads \
/var/www/maintenance/sirh /var/www/maintenance/lesstime /var/www/maintenance/inventory \
&& chown -R www-data:www-data /var/www/html/var /var/www/maintenance
WORKDIR /var/www/html
EXPOSE 80
+3 -3
View File
@@ -5,13 +5,13 @@ services:
env_file: .env
ports:
- "8084:80"
group_add:
- "987"
volumes:
- ./config/jwt:/var/www/html/config/jwt:ro
- ./uploads:/var/www/html/var/uploads
- /var/run/docker.sock:/var/run/docker.sock
- /var/www:/mnt/apps
- /var/www/sirh:/var/www/sirh
- /var/www/lesstime:/var/www/lesstime
- /var/www/inventory:/var/www/inventory
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
-3
View File
@@ -12,7 +12,6 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\ApplicationRepository;
use App\State\ApplicationProvider;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
@@ -25,13 +24,11 @@ use Symfony\Component\Serializer\Attribute\Groups;
new GetCollection(
normalizationContext: ['groups' => ['app:read']],
security: "is_granted('ROLE_ADMIN')",
provider: ApplicationProvider::class,
),
new Get(
uriVariables: ['slug'],
normalizationContext: ['groups' => ['app:read', 'app:detail']],
security: "is_granted('ROLE_ADMIN')",
provider: ApplicationProvider::class,
),
new Post(
security: "is_granted('ROLE_ADMIN')",
+2 -12
View File
@@ -10,7 +10,6 @@ use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\EnvironmentRepository;
use App\State\EnvironmentCreateProcessor;
use App\State\MaintenanceToggleProcessor;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
@@ -22,11 +21,9 @@ use Symfony\Component\Serializer\Attribute\Groups;
new Post(
uriTemplate: '/applications/{slug}/environments',
uriVariables: [
'slug' => new Link(toProperty: 'application', fromClass: Application::class, identifiers: ['slug']),
'slug' => new Link(fromClass: Application::class, fromProperty: 'environments'),
],
read: false,
security: "is_granted('ROLE_ADMIN')",
processor: EnvironmentCreateProcessor::class,
),
new Patch(
security: "is_granted('ROLE_ADMIN')",
@@ -196,13 +193,6 @@ class Environment
public function getMaintenance(): bool
{
return $this->maintenance ?? false;
}
public function setMaintenance(bool $maintenance): static
{
$this->maintenance = $maintenance;
return $this;
return file_exists((string) $this->maintenanceFilePath);
}
}
-20
View File
@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final readonly class AppPathResolver
{
public function __construct(
#[Autowire('%env(APPS_BASE_PATH)%')]
private string $basePath,
) {}
public function resolve(string $relativePath): string
{
return rtrim($this->basePath, '/') . '/' . ltrim($relativePath, '/');
}
}
+4 -18
View File
@@ -7,33 +7,19 @@ namespace App\Service;
use App\Entity\Environment;
use Symfony\Component\Process\Process;
final readonly class DeployService
final class DeployService
{
public function __construct(
private AppPathResolver $pathResolver,
) {}
/**
* @return array{success: bool, output: string, exitCode: int}
*/
public function deploy(Environment $environment, string $tag): array
{
$relativePath = $environment->getDeployScriptPath();
$scriptPath = $environment->getDeployScriptPath();
if (null === $relativePath) {
if (null === $scriptPath || !file_exists($scriptPath)) {
return [
'success' => false,
'output' => 'Deploy script path is not configured.',
'exitCode' => 1,
];
}
$scriptPath = $this->pathResolver->resolve($relativePath);
if (!file_exists($scriptPath)) {
return [
'success' => false,
'output' => sprintf('Deploy script not found: %s', $scriptPath),
'output' => sprintf('Deploy script not found: %s', $scriptPath ?? 'null'),
'exitCode' => 1,
];
}
-54
View File
@@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\Application;
use App\Repository\ApplicationRepository;
use App\Service\AppPathResolver;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
final readonly class ApplicationProvider implements ProviderInterface
{
public function __construct(
private ApplicationRepository $applicationRepository,
private AppPathResolver $pathResolver,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Application|array
{
if ($operation instanceof GetCollection) {
$apps = $this->applicationRepository->findAll();
foreach ($apps as $app) {
$this->resolveMaintenanceStatus($app);
}
return $apps;
}
$slug = $uriVariables['slug'] ?? '';
$app = $this->applicationRepository->findOneBy(['slug' => $slug]);
if (null === $app) {
throw new NotFoundHttpException(sprintf('Application "%s" not found.', $slug));
}
$this->resolveMaintenanceStatus($app);
return $app;
}
private function resolveMaintenanceStatus(Application $app): void
{
foreach ($app->getEnvironments() as $env) {
$path = $env->getMaintenanceFilePath();
if (null !== $path) {
$env->setMaintenance(file_exists($this->pathResolver->resolve($path)));
}
}
}
}
-44
View File
@@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Environment;
use App\Repository\ApplicationRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
final readonly class EnvironmentCreateProcessor implements ProcessorInterface
{
public function __construct(
private ApplicationRepository $applicationRepository,
private EntityManagerInterface $entityManager,
) {}
/**
* @param Environment $data
*/
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Environment
{
$slug = $uriVariables['slug']
?? $context['request']?->attributes->get('slug')
?? $context['request']?->attributes->get('_route_params')['slug']
?? '';
$application = $this->applicationRepository->findOneBy(['slug' => $slug]);
if (null === $application) {
throw new NotFoundHttpException(sprintf('Application "%s" not found.', $slug));
}
$data->setApplication($application);
$this->entityManager->persist($data);
$this->entityManager->flush();
return $data;
}
}
+2 -8
View File
@@ -7,27 +7,21 @@ namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Environment;
use App\Service\AppPathResolver;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
final readonly class MaintenanceToggleProcessor implements ProcessorInterface
{
public function __construct(
private AppPathResolver $pathResolver,
) {}
/**
* @param Environment $data
*/
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Environment
{
$relativePath = $data->getMaintenanceFilePath();
$maintenancePath = $data->getMaintenanceFilePath();
if (null === $relativePath) {
if (null === $maintenancePath) {
throw new BadRequestHttpException('Maintenance file path is not configured for this environment.');
}
$maintenancePath = $this->pathResolver->resolve($relativePath);
$requestData = $context['request']?->toArray() ?? [];
$enableMaintenance = $requestData['maintenance'] ?? false;