2 Commits

Author SHA1 Message Date
gitea-actions
7e342c9aeb chore: bump version to v0.1.20
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build & Push Docker Image / build (push) Successful in 3m31s
2026-04-07 08:31:10 +00:00
419d3b24cb fix : ajout d'un préfix pour les path des app et correction de l'affichage
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-04-07 10:30:58 +02:00
14 changed files with 174 additions and 47 deletions

4
.env
View File

@@ -44,6 +44,10 @@ 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

View File

@@ -1,2 +1,2 @@
parameters:
app.version: '0.1.19'
app.version: '0.1.20'

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-end gap-3">
<div class="flex justify-center gap-3">
<slot name="footer">
<MalioButton
:label="cancelLabel"

View File

@@ -106,6 +106,7 @@
"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"

View File

@@ -7,7 +7,7 @@
"name": "nuxt-app",
"hasInstallScript": true,
"dependencies": {
"@malio/layer-ui": "^1.2.0",
"@malio/layer-ui": "^1.2.2",
"@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.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==",
"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==",
"dependencies": {
"@nuxt/icon": "^2.2.1",
"@nuxtjs/tailwindcss": "^6.14.0",

View File

@@ -11,7 +11,7 @@
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
},
"dependencies": {
"@malio/layer-ui": "^1.2.0",
"@malio/layer-ui": "^1.2.2",
"@nuxt/icon": "^2.2.1",
"@nuxtjs/i18n": "^10.2.3",
"@nuxtjs/tailwindcss": "^6.14.0",

View File

@@ -256,20 +256,23 @@ onMounted(loadApplication)
<p v-if="application.description" class="text-neutral-500 mt-2">{{ application.description }}</p>
</div>
<div class="flex gap-2">
<MalioButton
:label="t('applications.detail.editButton')"
variant="secondary"
icon-name="mdi:pencil"
icon-position="left"
@click="openEditAppModal"
/>
<MalioButton
:label="t('applications.detail.deleteButton')"
variant="danger"
icon-name="mdi:trash-can-outline"
icon-position="left"
@click="handleDeleteApp"
/>
<div class="flex items-center">
<MalioButtonIcon
:aria-label="t('applications.detail.editButton')"
variant="filled"
icon="mdi:pencil"
@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"
@click="handleDeleteApp"
/>
</div>
</div>
</div>
@@ -277,11 +280,11 @@ onMounted(loadApplication)
<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="text-neutral-400">{{ t('applications.detail.registryImage') }} :</span>
<span class="font-bold">{{ 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="text-neutral-400">{{ t('applications.detail.giteaUrl') }} :</span>
<span class="font-bold">{{ t('applications.detail.giteaUrl') }} :</span>
<a :href="application.giteaUrl" target="_blank" class="text-primary-500 hover:underline ml-1">
{{ application.giteaUrl }}
</a>
@@ -354,7 +357,7 @@ onMounted(loadApplication)
<!-- Log files -->
<div v-if="env.logFiles.length" class="mt-4 border-t border-neutral-200 pt-3">
<p class="text-xs font-semibold uppercase tracking-wider text-neutral-400 mb-2">{{ t('environments.logFiles.title') }}</p>
<p class="text-sm font-bold uppercase tracking-wider 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>
@@ -362,9 +365,9 @@ onMounted(loadApplication)
</div>
<!-- Health metrics -->
<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 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>
<p class="text-xs text-neutral-400">{{ t('environments.health.status') }}</p>
<span
@@ -386,7 +389,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 class="col-span-2">
<div>
<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 }}
@@ -440,11 +443,13 @@ 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>
@@ -473,48 +478,59 @@ 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-2">
<p class="text-sm font-medium text-neutral-700">{{ t('environments.logFiles.title') }}</p>
<div class="flex items-center justify-between mb-4">
<p class="text-md font-bold">{{ 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">
<MalioInputText v-model="lf.label" :label="t('environments.logFiles.label')" groupClass="mt-0" inputClass="flex-1" required />
<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')"
variant="ghost"
icon-size="18"
button-class="text-red-500 hover:bg-red-50 hover:text-red-700 my-1"
@click="removeLogFile(index)"
/>
<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">
<MalioButtonIcon
icon="mdi:delete-outline"
:aria-label="t('environments.logFiles.remove')"
variant="ghost"
icon-size="18"
button-class="text-red-500 hover:bg-red-50 hover:text-red-700 my-1"
@click="removeLogFile(index)"
/>
</div>
</div>
</div>
</form>

View File

@@ -130,11 +130,13 @@ 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>

View File

@@ -12,6 +12,7 @@ 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;
@@ -24,11 +25,13 @@ 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')",

View File

@@ -196,6 +196,13 @@ class Environment
public function getMaintenance(): bool
{
return file_exists((string) $this->maintenanceFilePath);
return $this->maintenance ?? false;
}
public function setMaintenance(bool $maintenance): static
{
$this->maintenance = $maintenance;
return $this;
}
}

View File

@@ -0,0 +1,20 @@
<?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, '/');
}
}

View File

@@ -7,19 +7,33 @@ namespace App\Service;
use App\Entity\Environment;
use Symfony\Component\Process\Process;
final class DeployService
final readonly class DeployService
{
public function __construct(
private AppPathResolver $pathResolver,
) {}
/**
* @return array{success: bool, output: string, exitCode: int}
*/
public function deploy(Environment $environment, string $tag): array
{
$scriptPath = $environment->getDeployScriptPath();
$relativePath = $environment->getDeployScriptPath();
if (null === $scriptPath || !file_exists($scriptPath)) {
if (null === $relativePath) {
return [
'success' => false,
'output' => sprintf('Deploy script not found: %s', $scriptPath ?? 'null'),
'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),
'exitCode' => 1,
];
}

View File

@@ -0,0 +1,54 @@
<?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)));
}
}
}
}

View File

@@ -7,21 +7,27 @@ 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
{
$maintenancePath = $data->getMaintenanceFilePath();
$relativePath = $data->getMaintenanceFilePath();
if (null === $maintenancePath) {
if (null === $relativePath) {
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;