Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18589823f3 | ||
|
|
ab2b3fd9ef | ||
|
|
123d9b306f | ||
|
|
ca3445103d | ||
| 18f3de1ba9 | |||
| 52571c651f | |||
|
|
b9712643de | ||
| e954402959 | |||
|
|
98d9032068 | ||
| 5f6277d412 | |||
|
|
8fb71e6370 | ||
| e128b45caa | |||
|
|
d331ef4577 | ||
| b769abdbe1 | |||
|
|
7e342c9aeb | ||
| 419d3b24cb | |||
|
|
777224709d | ||
| 0a4b0cdc14 | |||
| 3fd745196f | |||
| 8481fe8fef | |||
| 061ab13d2b | |||
|
|
656aaf816f | ||
| 36777e50a8 | |||
| fdd1182caf | |||
| 98237daa56 | |||
|
|
67b722612c | ||
| 4e32fb8222 | |||
| f9555c515c | |||
|
|
6458a1b35c | ||
| de81b55867 | |||
| a3cd1f6b74 | |||
|
|
5eb4921c60 | ||
| c7e32c74b6 | |||
| e9850fdb2e | |||
|
|
18ff7b4dbb | ||
| e99c50d243 | |||
|
|
d13e789a63 | ||
| d03fc7d33c |
4
.env
4
.env
@@ -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
|
||||
|
||||
@@ -31,6 +31,10 @@ frontend/services/dto/ # Types TypeScript
|
||||
frontend/i18n/locales/ # Fichiers de traduction
|
||||
```
|
||||
|
||||
## Composants UI
|
||||
|
||||
La librairie `@malio/layer-ui` fournit les composants de formulaire et d'action. La documentation complète des props, events et exemples d'utilisation se trouve dans `frontend/node_modules/@malio/layer-ui/COMPONENTS.md`. Toujours s'y référer avant d'utiliser un composant Malio.
|
||||
|
||||
## Commandes
|
||||
|
||||
```bash
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"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.*",
|
||||
|
||||
132
composer.lock
generated
132
composer.lock
generated
@@ -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": "bfd26e903d79f710cfe95452c05f2a25",
|
||||
"content-hash": "51813b5c3b6dacd3cc99cfe121ab918b",
|
||||
"packages": [
|
||||
{
|
||||
"name": "api-platform/doctrine-common",
|
||||
@@ -6234,6 +6234,71 @@
|
||||
],
|
||||
"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",
|
||||
@@ -11018,71 +11083,6 @@
|
||||
],
|
||||
"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,2 +1,2 @@
|
||||
parameters:
|
||||
app.version: '0.1.12'
|
||||
app.version: '0.1.26'
|
||||
|
||||
@@ -26,12 +26,13 @@ services:
|
||||
- ./LOG:/var/www/html/LOG
|
||||
- uploads_data:/var/www/html/var/uploads
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${HOST_APPS_PATH}:/mnt/apps
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "3003:3003"
|
||||
- "3005:3003"
|
||||
restart: unless-stopped
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
@@ -46,7 +47,7 @@ services:
|
||||
restart: unless-stopped
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
command: -p ${POSTGRES_PORT:-5436}
|
||||
command: -p ${POSTGRES_PORT:-5437}
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
@@ -54,7 +55,7 @@ services:
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "${POSTGRES_PORT:-5436}:${POSTGRES_PORT:-5436}"
|
||||
- "${POSTGRES_PORT:-5437}:${POSTGRES_PORT:-5437}"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
pg_data:
|
||||
|
||||
@@ -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"
|
||||
|
||||
113
frontend/components/ui/LogModal.vue
Normal file
113
frontend/components/ui/LogModal.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: boolean
|
||||
title: string
|
||||
content: string
|
||||
loading?: boolean
|
||||
showLevelFilter?: boolean
|
||||
}>(), {
|
||||
loading: false,
|
||||
showLevelFilter: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'refresh', lines: number, level: string): void
|
||||
}>()
|
||||
|
||||
const selectedLines = ref(100)
|
||||
const selectedLevel = ref('')
|
||||
|
||||
const lineOptions = [50, 100, 500, 1000]
|
||||
|
||||
const copied = ref(false)
|
||||
|
||||
function refresh() {
|
||||
emit('refresh', selectedLines.value, selectedLevel.value)
|
||||
}
|
||||
|
||||
async function copyLogs() {
|
||||
if (!props.content) return
|
||||
await navigator.clipboard.writeText(props.content)
|
||||
copied.value = true
|
||||
setTimeout(() => { copied.value = false }, 2000)
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (open) => {
|
||||
if (open) {
|
||||
copied.value = false
|
||||
refresh()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppModal
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
max-width="2xl"
|
||||
>
|
||||
<template #title>{{ title }}</template>
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-neutral-400">{{ t('logs.lines') }}</label>
|
||||
<select
|
||||
v-model="selectedLines"
|
||||
class="rounded-md border border-neutral-300 px-2 py-1 text-sm"
|
||||
@change="refresh"
|
||||
>
|
||||
<option v-for="n in lineOptions" :key="n" :value="n">{{ n }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="showLevelFilter" class="flex items-center gap-2">
|
||||
<label class="text-xs text-neutral-400">{{ t('logs.level') }}</label>
|
||||
<select
|
||||
v-model="selectedLevel"
|
||||
class="rounded-md border border-neutral-300 px-2 py-1 text-sm"
|
||||
@change="refresh"
|
||||
>
|
||||
<option value="">{{ t('logs.levelAll') }}</option>
|
||||
<option value="ERROR">ERROR</option>
|
||||
<option value="WARNING">WARNING</option>
|
||||
<option value="INFO">INFO</option>
|
||||
<option value="DEBUG">DEBUG</option>
|
||||
</select>
|
||||
</div>
|
||||
<MalioButtonIcon
|
||||
icon="mdi:refresh"
|
||||
:aria-label="t('logs.refresh')"
|
||||
icon-size="18"
|
||||
@click="refresh"
|
||||
/>
|
||||
</div>
|
||||
<MalioButtonIcon
|
||||
:icon="copied ? 'mdi:check' : 'mdi:content-copy'"
|
||||
:aria-label="t('logs.copy')"
|
||||
icon-size="18"
|
||||
:button-class="copied ? 'text-green-500' : ''"
|
||||
@click="copyLogs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<pre
|
||||
v-if="content"
|
||||
class="max-h-96 overflow-auto rounded-lg bg-neutral-900 p-4 text-xs text-green-400 font-mono whitespace-pre-wrap"
|
||||
>{{ content }}</pre>
|
||||
|
||||
<p v-else-if="!loading" class="text-center text-neutral-400 py-8">
|
||||
{{ t('logs.noContent') }}
|
||||
</p>
|
||||
|
||||
<template #footer>
|
||||
<MalioButton
|
||||
:label="t('applications.form.cancel')"
|
||||
variant="tertiary"
|
||||
@click="emit('update:modelValue', false)"
|
||||
/>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
@@ -2,9 +2,7 @@
|
||||
<NuxtLink
|
||||
:to="to"
|
||||
class="group/link relative flex items-center transition-colors hover:text-primary-500"
|
||||
:class="linkClasses"
|
||||
:active-class="exact ? '' : activeClass"
|
||||
:exact-active-class="exact ? activeClass : ''"
|
||||
:class="[linkClasses, isActive ? activeClass : '']"
|
||||
>
|
||||
<Icon :name="icon" :size="sub ? '20' : '24'" class="flex-shrink-0" />
|
||||
<span
|
||||
@@ -33,6 +31,15 @@ const props = defineProps<{
|
||||
exact?: boolean
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const isActive = computed(() => {
|
||||
if (props.exact) {
|
||||
return route.path === props.to
|
||||
}
|
||||
return route.path === props.to || route.path.startsWith(props.to + '/')
|
||||
})
|
||||
|
||||
const activeClass = computed(() => {
|
||||
if (props.collapsed) {
|
||||
return '!text-primary-500 bg-primary-500/10'
|
||||
|
||||
@@ -48,6 +48,17 @@
|
||||
"logout": "Déconnexion réussie."
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
"docker": "Logs Docker",
|
||||
"symfony": "Logs Symfony",
|
||||
"lines": "Lignes",
|
||||
"level": "Niveau",
|
||||
"levelAll": "Tous",
|
||||
"refresh": "Actualiser",
|
||||
"noContent": "Aucun log disponible",
|
||||
"copy": "Copier les logs",
|
||||
"title": "Logs"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"description": "Vue d'ensemble du SI",
|
||||
@@ -56,7 +67,8 @@
|
||||
"running": "En ligne",
|
||||
"exited": "Arrete",
|
||||
"restarting": "Redemarrage",
|
||||
"not_found": "Introuvable"
|
||||
"not_found": "Introuvable",
|
||||
"unavailable": "Docker indisponible"
|
||||
}
|
||||
},
|
||||
"applications": {
|
||||
@@ -106,6 +118,8 @@
|
||||
"containerName": "Nom du container",
|
||||
"deployScriptPath": "Chemin du script de deploiement",
|
||||
"maintenanceFilePath": "Chemin du fichier de maintenance",
|
||||
"pathHint": "Prefixe automatique : /mnt/apps",
|
||||
"pathHintLog": "Chemin dans le container, ex : var/log/prod.log",
|
||||
"appUrl": "URL de l'application",
|
||||
"save": "Enregistrer",
|
||||
"cancel": "Annuler"
|
||||
@@ -124,6 +138,7 @@
|
||||
"uptime": "Uptime",
|
||||
"cpu": "CPU",
|
||||
"memory": "Memoire",
|
||||
"ports": "Ports",
|
||||
"noData": "Aucune donnee disponible"
|
||||
},
|
||||
"deploy": {
|
||||
|
||||
122
frontend/package-lock.json
generated
122
frontend/package-lock.json
generated
@@ -7,7 +7,7 @@
|
||||
"name": "nuxt-app",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@malio/layer-ui": "^1.2.0",
|
||||
"@malio/layer-ui": "^1.2.3",
|
||||
"@nuxt/icon": "^2.2.1",
|
||||
"@nuxtjs/i18n": "^10.2.3",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
@@ -72,7 +72,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -570,12 +569,36 @@
|
||||
"integrity": "sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
@@ -1019,6 +1042,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
||||
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
||||
}
|
||||
@@ -1028,6 +1052,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz",
|
||||
"integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^3.0.3",
|
||||
"debug": "^4.3.1",
|
||||
@@ -1042,6 +1067,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz",
|
||||
"integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint/core": "^1.1.1"
|
||||
},
|
||||
@@ -1054,6 +1080,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz",
|
||||
"integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
@@ -1066,6 +1093,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz",
|
||||
"integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
@@ -1075,6 +1103,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz",
|
||||
"integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint/core": "^1.1.1",
|
||||
"levn": "^0.4.1"
|
||||
@@ -1088,6 +1117,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
@@ -1097,6 +1127,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
|
||||
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@humanfs/core": "^0.19.1",
|
||||
"@humanwhocodes/retry": "^0.4.0"
|
||||
@@ -1110,6 +1141,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.22"
|
||||
},
|
||||
@@ -1123,6 +1155,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
|
||||
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
@@ -1635,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.3",
|
||||
"resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.2.3/layer-ui-1.2.3.tgz",
|
||||
"integrity": "sha512-5nRnBzRkXfs3PfKwKl6sH2ikrmSK7lTifcd0TX1QZP3rFRVRTgcT6mrsrpsbR9PwI27OeCNm0X6d0Ii92Rq7Yg==",
|
||||
"dependencies": {
|
||||
"@nuxt/icon": "^2.2.1",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
@@ -1929,7 +1962,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.4.2.tgz",
|
||||
"integrity": "sha512-5+IPRNX2CjkBhuWUwz0hBuLqiaJPRoKzQ+SvcdrQDbAyE+VDeFt74VpSFr5/R0ujrK4b+XnSHUJWdS72w6hsog==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"c12": "^3.3.3",
|
||||
"consola": "^3.4.2",
|
||||
@@ -2032,7 +2064,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-4.4.2.tgz",
|
||||
"integrity": "sha512-/q6C7Qhiricgi+PKR7ovBnJlKTL0memCbA1CzRT+itCW/oeYzUfeMdQ35mGntlBoyRPNrMXbzuSUhfDbSCU57w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/shared": "^3.5.30",
|
||||
"defu": "^6.1.4",
|
||||
@@ -4258,7 +4289,8 @@
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||
"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
@@ -4270,7 +4302,8 @@
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
@@ -4584,7 +4617,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz",
|
||||
"integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.2",
|
||||
"@vue/compiler-core": "3.5.32",
|
||||
@@ -4817,7 +4849,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -4857,6 +4888,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -5185,7 +5217,6 @@
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
@@ -5383,7 +5414,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
@@ -5485,7 +5515,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -5638,8 +5667,7 @@
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz",
|
||||
"integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "9.0.1",
|
||||
@@ -6128,7 +6156,8 @@
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
@@ -6532,6 +6561,7 @@
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -6621,6 +6651,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
|
||||
"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/esrecurse": "^4.3.1",
|
||||
"@types/estree": "^1.0.8",
|
||||
@@ -6651,6 +6682,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
@@ -6663,6 +6695,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
@@ -6672,6 +6705,7 @@
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
|
||||
"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.16.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
@@ -6689,6 +6723,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
@@ -6714,6 +6749,7 @@
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"estraverse": "^5.1.0"
|
||||
},
|
||||
@@ -6726,6 +6762,7 @@
|
||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
||||
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"estraverse": "^5.2.0"
|
||||
},
|
||||
@@ -6826,7 +6863,8 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
@@ -6866,13 +6904,15 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/fast-npm-meta": {
|
||||
"version": "1.4.2",
|
||||
@@ -6941,6 +6981,7 @@
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"flat-cache": "^4.0.0"
|
||||
},
|
||||
@@ -6971,6 +7012,7 @@
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"locate-path": "^6.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
@@ -6987,6 +7029,7 @@
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
||||
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"flatted": "^3.2.9",
|
||||
"keyv": "^4.5.4"
|
||||
@@ -6999,7 +7042,8 @@
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
@@ -7541,6 +7585,7 @@
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
@@ -7911,19 +7956,22 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
@@ -8001,6 +8049,7 @@
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
@@ -8261,6 +8310,7 @@
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prelude-ls": "^1.2.1",
|
||||
"type-check": "~0.4.0"
|
||||
@@ -8345,6 +8395,7 @@
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"p-locate": "^5.0.0"
|
||||
},
|
||||
@@ -8721,7 +8772,8 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
@@ -9453,7 +9505,6 @@
|
||||
"resolved": "https://registry.npmjs.org/nuxt/-/nuxt-4.4.2.tgz",
|
||||
"integrity": "sha512-iWVFpr/YEqVU/CenqIHMnIkvb2HE/9f+q8oxZ+pj2et+60NljGRClCgnmbvGPdmNFE0F1bEhoBCYfqbDOCim3Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dxup/nuxt": "^0.4.0",
|
||||
"@nuxt/cli": "^3.34.0",
|
||||
@@ -10505,6 +10556,7 @@
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"deep-is": "^0.1.3",
|
||||
"fast-levenshtein": "^2.0.6",
|
||||
@@ -10556,7 +10608,6 @@
|
||||
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.112.0.tgz",
|
||||
"integrity": "sha512-7rQ3QdJwobMQLMZwQaPuPYMEF2fDRZwf51lZ//V+bA37nejjKW5ifMHbbCwvA889Y4RLhT+/wLJpPRhAoBaZYw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "^0.112.0"
|
||||
},
|
||||
@@ -10640,6 +10691,7 @@
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"yocto-queue": "^0.1.0"
|
||||
},
|
||||
@@ -10655,6 +10707,7 @@
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"p-limit": "^3.0.2"
|
||||
},
|
||||
@@ -10691,6 +10744,7 @@
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -10794,7 +10848,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^7.7.7"
|
||||
},
|
||||
@@ -10863,7 +10916,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -11407,7 +11459,6 @@
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
|
||||
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -11470,6 +11521,7 @@
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
@@ -11506,6 +11558,7 @@
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -11957,7 +12010,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
||||
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -12764,7 +12816,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -13102,6 +13153,7 @@
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prelude-ls": "^1.2.1"
|
||||
},
|
||||
@@ -13586,6 +13638,7 @@
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
@@ -13610,7 +13663,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -14432,7 +14484,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
|
||||
"integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.32",
|
||||
"@vue/compiler-sfc": "3.5.32",
|
||||
@@ -14469,7 +14520,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz",
|
||||
"integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.3.0",
|
||||
"@intlify/devtools-types": "11.3.0",
|
||||
@@ -14555,6 +14605,7 @@
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -14776,6 +14827,7 @@
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
||||
@@ -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.3",
|
||||
"@nuxt/icon": "^2.2.1",
|
||||
"@nuxtjs/i18n": "^10.2.3",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { DeployResult } from '~/services/dto/deploy'
|
||||
import { getAvailableTags, deploy } from '~/services/deploy'
|
||||
import type { EnvironmentHealth } from '~/services/dto/dashboard'
|
||||
import { getEnvironmentHealth } from '~/services/dashboard'
|
||||
import type { LogOutput } from '~/services/dto/logs'
|
||||
import { getDockerLogs, getSymfonyLog } from '~/services/logs'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
@@ -29,6 +31,15 @@ const deployResult = ref<DeployResult | null>(null)
|
||||
const healthByEnvId = ref<Record<number, EnvironmentHealth>>({})
|
||||
const loadingHealth = ref(false)
|
||||
|
||||
// Log modals
|
||||
const showLogModal = ref(false)
|
||||
const logContent = ref('')
|
||||
const logLoading = ref(false)
|
||||
const logTitle = ref('')
|
||||
const logEnvId = ref<number | null>(null)
|
||||
const logFileId = ref<number | null>(null)
|
||||
const logIsSymfony = ref(false)
|
||||
|
||||
// App edit modal
|
||||
const showAppModal = ref(false)
|
||||
const editForm = ref<ApplicationWrite>({ name: '', slug: '', registryImage: '', description: '', giteaUrl: '' })
|
||||
@@ -226,6 +237,41 @@ function statusClass(status: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Log functions
|
||||
async function openDockerLogs(env: Environment) {
|
||||
logEnvId.value = env.id!
|
||||
logFileId.value = null
|
||||
logIsSymfony.value = false
|
||||
logTitle.value = `${t('logs.docker')} — ${env.name}`
|
||||
logContent.value = ''
|
||||
showLogModal.value = true
|
||||
}
|
||||
|
||||
async function openSymfonyLog(env: Environment, lf: { id?: number, label: string }) {
|
||||
logEnvId.value = env.id!
|
||||
logFileId.value = lf.id!
|
||||
logIsSymfony.value = true
|
||||
logTitle.value = `${t('logs.symfony')} — ${lf.label}`
|
||||
logContent.value = ''
|
||||
showLogModal.value = true
|
||||
}
|
||||
|
||||
async function refreshLogs(lines: number, level: string) {
|
||||
if (!logEnvId.value) return
|
||||
logLoading.value = true
|
||||
try {
|
||||
let result: LogOutput
|
||||
if (logIsSymfony.value && logFileId.value) {
|
||||
result = await getSymfonyLog(logEnvId.value, logFileId.value, lines, level || undefined)
|
||||
} else {
|
||||
result = await getDockerLogs(logEnvId.value, lines)
|
||||
}
|
||||
logContent.value = result.content
|
||||
} finally {
|
||||
logLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const envModalTitle = computed(() =>
|
||||
editingEnvId.value ? t('environments.editButton') : t('environments.addButton')
|
||||
)
|
||||
@@ -256,20 +302,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 +326,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>
|
||||
@@ -331,6 +380,13 @@ onMounted(loadApplication)
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<MalioButton
|
||||
:label="t('logs.docker')"
|
||||
variant="secondary"
|
||||
icon-name="mdi:text-box-outline"
|
||||
icon-position="left"
|
||||
@click="openDockerLogs(env)"
|
||||
/>
|
||||
<MalioButton
|
||||
:label="t('environments.deploy.button')"
|
||||
icon-name="mdi:rocket-launch-outline"
|
||||
@@ -354,17 +410,24 @@ 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>
|
||||
<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>
|
||||
<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 items-center">
|
||||
<span class="font-medium">{{ lf.label }}</span>
|
||||
<span class="font-mono text-neutral-400">{{ lf.path }}</span>
|
||||
<MalioButtonIcon
|
||||
icon="mdi:console"
|
||||
:aria-label="lf.label"
|
||||
variant="ghost"
|
||||
icon-size="16"
|
||||
@click="openSymfonyLog(env, lf)"
|
||||
/>
|
||||
</div>
|
||||
</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-6 gap-3">
|
||||
<div>
|
||||
<p class="text-xs text-neutral-400">{{ t('environments.health.status') }}</p>
|
||||
<span
|
||||
@@ -386,13 +449,26 @@ 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 }}
|
||||
<span class="text-neutral-400">({{ healthByEnvId[env.id!].memoryPercent }}%)</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-neutral-400">{{ t('environments.health.ports') }}</p>
|
||||
<div v-if="healthByEnvId[env.id!].ports?.length" class="mt-1 flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="(p, i) in healthByEnvId[env.id!].ports"
|
||||
:key="i"
|
||||
class="inline-block rounded bg-neutral-100 px-2 py-0.5 text-xs font-mono text-neutral-700"
|
||||
>
|
||||
{{ p.hostPort }}:{{ p.containerPort }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-else class="text-sm text-neutral-400 mt-1">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center gap-4 mt-4">
|
||||
@@ -440,11 +516,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 +551,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')}/${application?.slug ?? ''}`" 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>
|
||||
@@ -597,5 +686,15 @@ onMounted(loadApplication)
|
||||
/>
|
||||
</template>
|
||||
</AppModal>
|
||||
|
||||
<!-- Log modal -->
|
||||
<LogModal
|
||||
v-model="showLogModal"
|
||||
:title="logTitle"
|
||||
:content="logContent"
|
||||
:loading="logLoading"
|
||||
:show-level-filter="logIsSymfony"
|
||||
@refresh="refreshLogs"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -20,18 +20,12 @@
|
||||
v-model="username"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-neutral-700" for="password">
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
||||
/>
|
||||
</div>
|
||||
<MalioInputPassword
|
||||
v-model="password"
|
||||
label="Mot de passe"
|
||||
autocomplete="current-password"
|
||||
inputClass="w-full"
|
||||
/>
|
||||
|
||||
<MalioButton
|
||||
label="Se connecter"
|
||||
|
||||
@@ -16,6 +16,12 @@ type DashboardResponse = {
|
||||
applications: DashboardApplication[]
|
||||
}
|
||||
|
||||
type PortMapping = {
|
||||
hostPort: string
|
||||
containerPort: string
|
||||
protocol: string
|
||||
}
|
||||
|
||||
type EnvironmentHealth = {
|
||||
status: string
|
||||
version: string
|
||||
@@ -24,4 +30,5 @@ type EnvironmentHealth = {
|
||||
memoryUsage: string
|
||||
memoryLimit: string
|
||||
memoryPercent: number
|
||||
ports: PortMapping[]
|
||||
}
|
||||
|
||||
5
frontend/services/dto/logs.ts
Normal file
5
frontend/services/dto/logs.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
type LogOutput = {
|
||||
content: string
|
||||
lines: number
|
||||
source: string
|
||||
}
|
||||
15
frontend/services/logs.ts
Normal file
15
frontend/services/logs.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { LogOutput } from './dto/logs'
|
||||
|
||||
export function getDockerLogs(envId: number, lines: number = 100): Promise<LogOutput> {
|
||||
return useApi().get<LogOutput>(`/environments/${envId}/logs/docker`, { lines }, {
|
||||
toast: false,
|
||||
})
|
||||
}
|
||||
|
||||
export function getSymfonyLog(envId: number, logFileId: number, lines: number = 100, level?: string): Promise<LogOutput> {
|
||||
const query: Record<string, any> = { lines }
|
||||
if (level) query.level = level
|
||||
return useApi().get<LogOutput>(`/environments/${envId}/logs/symfony/${logFileId}`, query, {
|
||||
toast: false,
|
||||
})
|
||||
}
|
||||
@@ -5,5 +5,6 @@ APP_USER=www-data
|
||||
POSTGRES_DB=central
|
||||
POSTGRES_USER=root
|
||||
POSTGRES_PASSWORD=root
|
||||
POSTGRES_PORT=5436
|
||||
POSTGRES_PORT=5437
|
||||
XDEBUG_CLIENT_HOST=host.docker.internal
|
||||
HOST_APPS_PATH=/home/user/workspace
|
||||
|
||||
@@ -81,7 +81,7 @@ RUN mkdir -p /var/www/.composer/cache/vcs \
|
||||
ENV COMPOSER_HOME=/var/www/.composer
|
||||
|
||||
# Création de la structure du projet
|
||||
RUN mkdir /var/www/html/LOG
|
||||
RUN mkdir -p /var/www/html/LOG /var/www/html/var/cache /var/www/html/var/log
|
||||
|
||||
###> User ###
|
||||
ARG CURRENT_UID
|
||||
|
||||
@@ -40,10 +40,16 @@ FROM php:8.4-fpm AS production
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libicu-dev libpq-dev libpng-dev libzip-dev libxml2-dev \
|
||||
nginx supervisor docker.io \
|
||||
nginx supervisor docker.io curl \
|
||||
&& docker-php-ext-install -j$(nproc) intl pdo_pgsql zip gd opcache \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Docker Compose plugin
|
||||
RUN DOCKER_COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep '"tag_name"' | sed 's/.*"v\(.*\)".*/\1/') \
|
||||
&& mkdir -p /usr/local/lib/docker/cli-plugins \
|
||||
&& curl -SL "https://github.com/docker/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64" -o /usr/local/lib/docker/cli-plugins/docker-compose \
|
||||
&& chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
|
||||
|
||||
# PHP production config
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
@@ -71,10 +77,12 @@ 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
|
||||
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
|
||||
# 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
|
||||
|
||||
WORKDIR /var/www/html
|
||||
EXPOSE 80
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
services:
|
||||
app:
|
||||
image: gitea.malio.fr/malio-dev/central:${CENTRAL_IMAGE_TAG:-latest}
|
||||
container_name: central-app
|
||||
env_file: .env
|
||||
ports:
|
||||
- "8084:80"
|
||||
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/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
|
||||
app:
|
||||
image: gitea.malio.fr/malio-dev/central:${CENTRAL_IMAGE_TAG:-latest}
|
||||
container_name: central-app
|
||||
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
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
restart: unless-stopped
|
||||
|
||||
6
makefile
6
makefile
@@ -43,7 +43,11 @@ install: composer-install cache-clear node-use build-nuxtJS migration-migrate
|
||||
# Supprime tout est réinstalle tout (Attention ça supprime la bdd aussi)
|
||||
reset: delete_built_dir remove_orphans build-without-cache start wait install
|
||||
|
||||
composer-install:
|
||||
fix-permissions:
|
||||
$(EXEC_PHP_ROOT) mkdir -p var/cache var/log
|
||||
$(EXEC_PHP_ROOT) chown -R $(APP_USER):$(APP_USER) var/
|
||||
|
||||
composer-install: fix-permissions
|
||||
$(EXEC_PHP) composer install
|
||||
$(SYMFONY_CONSOLE) lexik:jwt:generate-keypair --skip-if-exists
|
||||
|
||||
|
||||
@@ -26,4 +26,6 @@ final class EnvironmentHealth
|
||||
public string $memoryUsage = '';
|
||||
public string $memoryLimit = '';
|
||||
public float $memoryPercent = 0.0;
|
||||
/** @var list<array{hostPort: string, containerPort: string, protocol: string}> */
|
||||
public array $ports = [];
|
||||
}
|
||||
|
||||
31
src/ApiResource/LogOutput.php
Normal file
31
src/ApiResource/LogOutput.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\ApiResource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use App\State\DockerLogProvider;
|
||||
use App\State\SymfonyLogProvider;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/environments/{id}/logs/docker',
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
provider: DockerLogProvider::class,
|
||||
),
|
||||
new Get(
|
||||
uriTemplate: '/environments/{id}/logs/symfony/{logFileId}',
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
provider: SymfonyLogProvider::class,
|
||||
),
|
||||
],
|
||||
)]
|
||||
final class LogOutput
|
||||
{
|
||||
public string $content = '';
|
||||
public int $lines = 0;
|
||||
public string $source = '';
|
||||
}
|
||||
@@ -57,21 +57,21 @@ class AppFixtures extends Fixture
|
||||
$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->setName('Production');
|
||||
$sirhProd->setContainerName('php-sirh-fpm');
|
||||
$sirhProd->setDeployScriptPath('/SIRH/deploy/docker/deploy.sh');
|
||||
$sirhProd->setMaintenanceFilePath('/SIRH/deploy/docker/maintenance.on');
|
||||
$sirhProd->setAppUrl('http://sirh.malio-dev.fr');
|
||||
$sirh->addEnvironment($sirhProd);
|
||||
|
||||
$sirhProdLog = new LogFile();
|
||||
$sirhProdLog->setLabel('prod');
|
||||
$sirhProdLog->setPath('/home/m-tristan/workspace/SIRH/var/log/prod.log');
|
||||
$sirhProdLog->setLabel('dev');
|
||||
$sirhProdLog->setPath('/SIRH/var/log/dev.log');
|
||||
$sirhProd->addLogFile($sirhProdLog);
|
||||
|
||||
$sirhCronLog = new LogFile();
|
||||
$sirhCronLog->setLabel('cron');
|
||||
$sirhCronLog->setPath('/home/m-tristan/workspace/SIRH/var/log/cron.log');
|
||||
$sirhCronLog->setPath('/SIRH/var/log/cron.log');
|
||||
$sirhProd->addLogFile($sirhCronLog);
|
||||
|
||||
$manager->persist($sirh);
|
||||
@@ -86,8 +86,8 @@ class AppFixtures extends Fixture
|
||||
$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->setDeployScriptPath('/lesstime/deploy/docker/deploy.sh');
|
||||
$lesstimeProd->setMaintenanceFilePath('/lesstime/deploy/docker/maintenance.on');
|
||||
$lesstimeProd->setAppUrl('http://lesstime.malio-dev.fr');
|
||||
$lesstime->addEnvironment($lesstimeProd);
|
||||
|
||||
@@ -103,16 +103,16 @@ class AppFixtures extends Fixture
|
||||
$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->setDeployScriptPath('/inventory/deploy/docker/deploy.sh');
|
||||
$inventoryProd->setMaintenanceFilePath('/inventory/deploy/docker/maintenance.on');
|
||||
$inventoryProd->setAppUrl('http://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->setDeployScriptPath('/inventory/deploy/docker/deploy-test.sh');
|
||||
$inventoryRecette->setMaintenanceFilePath('/inventory/deploy/docker/maintenance-test.on');
|
||||
$inventoryRecette->setAppUrl('http://inventory-test.malio-dev.fr');
|
||||
$inventory->addEnvironment($inventoryRecette);
|
||||
|
||||
|
||||
@@ -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')",
|
||||
|
||||
@@ -10,6 +10,7 @@ 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;
|
||||
@@ -21,9 +22,11 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
new Post(
|
||||
uriTemplate: '/applications/{slug}/environments',
|
||||
uriVariables: [
|
||||
'slug' => new Link(fromClass: Application::class, fromProperty: 'environments'),
|
||||
'slug' => new Link(toProperty: 'application', fromClass: Application::class, identifiers: ['slug']),
|
||||
],
|
||||
read: false,
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
processor: EnvironmentCreateProcessor::class,
|
||||
),
|
||||
new Patch(
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
@@ -193,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;
|
||||
}
|
||||
}
|
||||
|
||||
20
src/Service/AppPathResolver.php
Normal file
20
src/Service/AppPathResolver.php
Normal 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, '/');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -8,14 +8,38 @@ use Symfony\Component\Process\Process;
|
||||
|
||||
final class DockerService
|
||||
{
|
||||
private ?bool $dockerAvailable = null;
|
||||
|
||||
private function isDockerAvailable(): bool
|
||||
{
|
||||
if (null === $this->dockerAvailable) {
|
||||
$process = new Process(['which', 'docker']);
|
||||
$process->setTimeout(5);
|
||||
$process->run();
|
||||
$this->dockerAvailable = $process->isSuccessful();
|
||||
}
|
||||
|
||||
return $this->dockerAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{status: string, image: string, version: string, startedAt: string}
|
||||
* @return array{status: string, image: string, version: string, startedAt: string, ports: list<array{hostPort: string, containerPort: string, protocol: string}>}
|
||||
*/
|
||||
public function getContainerStatus(string $containerName): array
|
||||
{
|
||||
if (!$this->isDockerAvailable()) {
|
||||
return [
|
||||
'status' => 'unavailable',
|
||||
'image' => '',
|
||||
'version' => '',
|
||||
'startedAt' => '',
|
||||
'ports' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$process = new Process([
|
||||
'docker', 'inspect',
|
||||
'--format', '{{.State.Status}}||{{.Config.Image}}||{{.State.StartedAt}}',
|
||||
'--format', '{{.State.Status}}||{{.Config.Image}}||{{.State.StartedAt}}||{{json .NetworkSettings.Ports}}',
|
||||
$containerName,
|
||||
]);
|
||||
$process->setTimeout(10);
|
||||
@@ -27,10 +51,11 @@ final class DockerService
|
||||
'image' => '',
|
||||
'version' => '',
|
||||
'startedAt' => '',
|
||||
'ports' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$parts = explode('||', trim($process->getOutput()));
|
||||
$parts = explode('||', trim($process->getOutput()), 4);
|
||||
|
||||
if (\count($parts) < 3) {
|
||||
return [
|
||||
@@ -38,6 +63,7 @@ final class DockerService
|
||||
'image' => '',
|
||||
'version' => '',
|
||||
'startedAt' => '',
|
||||
'ports' => [],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -47,11 +73,32 @@ final class DockerService
|
||||
$version = substr($image, strrpos($image, ':') + 1);
|
||||
}
|
||||
|
||||
$ports = [];
|
||||
if (isset($parts[3])) {
|
||||
$portsJson = json_decode($parts[3], true);
|
||||
if (\is_array($portsJson)) {
|
||||
foreach ($portsJson as $containerPort => $bindings) {
|
||||
if (!\is_array($bindings)) {
|
||||
continue;
|
||||
}
|
||||
[$port, $protocol] = explode('/', $containerPort) + [1 => 'tcp'];
|
||||
foreach ($bindings as $binding) {
|
||||
$ports[] = [
|
||||
'hostPort' => $binding['HostPort'] ?? '',
|
||||
'containerPort' => $port,
|
||||
'protocol' => $protocol,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => $parts[0],
|
||||
'image' => $image,
|
||||
'version' => $version,
|
||||
'startedAt' => $parts[2],
|
||||
'ports' => $ports,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -60,6 +107,15 @@ final class DockerService
|
||||
*/
|
||||
public function getContainerStats(string $containerName): array
|
||||
{
|
||||
if (!$this->isDockerAvailable()) {
|
||||
return [
|
||||
'cpuPercent' => 0.0,
|
||||
'memoryUsage' => '',
|
||||
'memoryLimit' => '',
|
||||
'memoryPercent' => 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
$process = new Process([
|
||||
'docker', 'stats', '--no-stream',
|
||||
'--format', '{{.CPUPerc}}||{{.MemUsage}}||{{.MemPerc}}',
|
||||
|
||||
179
src/Service/LogService.php
Normal file
179
src/Service/LogService.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
final readonly class LogService
|
||||
{
|
||||
public function __construct(
|
||||
private AppPathResolver $pathResolver,
|
||||
) {}
|
||||
|
||||
public function getDockerLogs(string $containerName, int $lines = 100, ?string $since = null): string
|
||||
{
|
||||
$check = new Process(['which', 'docker']);
|
||||
$check->setTimeout(5);
|
||||
$check->run();
|
||||
|
||||
if (!$check->isSuccessful()) {
|
||||
return 'Docker CLI is not available in this environment.';
|
||||
}
|
||||
|
||||
$args = ['docker', 'logs', '--tail', (string) $lines];
|
||||
|
||||
if (null !== $since) {
|
||||
$args[] = '--since';
|
||||
$args[] = $since;
|
||||
}
|
||||
|
||||
$args[] = $containerName;
|
||||
|
||||
$process = new Process($args);
|
||||
$process->setTimeout(10);
|
||||
$process->run();
|
||||
|
||||
$output = $process->getOutput() . $process->getErrorOutput();
|
||||
|
||||
return $this->formatDockerOutput($output);
|
||||
}
|
||||
|
||||
private function formatDockerOutput(string $output): string
|
||||
{
|
||||
$rawLines = explode("\n", trim($output));
|
||||
$formatted = [];
|
||||
|
||||
foreach ($rawLines as $line) {
|
||||
if ('' === $line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try parsing as Symfony monolog JSON format
|
||||
$parsed = $this->parseSymfonyLogLine($line);
|
||||
|
||||
if (null !== $parsed) {
|
||||
if ('doctrine' === $parsed['channel']) {
|
||||
continue;
|
||||
}
|
||||
$formatted[] = sprintf('[%s] %s.%s: %s', $parsed['date'], $parsed['channel'], $parsed['level'], $parsed['message']);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try parsing as JSON log ({"message":"...","level":...})
|
||||
$json = json_decode($line, true);
|
||||
if (\is_array($json) && isset($json['message'])) {
|
||||
$date = isset($json['datetime']) ? substr($json['datetime'], 0, 19) : '';
|
||||
$date = str_replace('T', ' ', $date);
|
||||
$channel = $json['channel'] ?? 'app';
|
||||
$level = $json['level_name'] ?? 'INFO';
|
||||
|
||||
if ('doctrine' === $channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$formatted[] = sprintf('[%s] %s.%s: %s', $date, $channel, $level, $json['message']);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep raw lines that don't match any format (nginx access logs, etc.)
|
||||
$formatted[] = $line;
|
||||
}
|
||||
|
||||
return implode("\n", $formatted);
|
||||
}
|
||||
|
||||
public function getSymfonyLog(string $containerName, string $logPath, int $lines = 100, ?string $level = null): string
|
||||
{
|
||||
$check = new Process(['which', 'docker']);
|
||||
$check->setTimeout(5);
|
||||
$check->run();
|
||||
|
||||
if (!$check->isSuccessful()) {
|
||||
// Fallback: try reading from filesystem (dev mode)
|
||||
$localPath = $this->pathResolver->resolve($logPath);
|
||||
if (!file_exists($localPath)) {
|
||||
return sprintf('Log file not found: %s (Docker CLI unavailable, local path: %s)', $logPath, $localPath);
|
||||
}
|
||||
$readLines = (null !== $level && '' !== $level) ? $lines * 5 : $lines;
|
||||
$process = new Process(['tail', '-n', (string) $readLines, $localPath]);
|
||||
$process->setTimeout(10);
|
||||
$process->run();
|
||||
return $this->formatSymfonyOutput($process->getOutput(), $lines, $level);
|
||||
}
|
||||
|
||||
// Read more lines than requested to compensate for filtering
|
||||
$readLines = (null !== $level && '' !== $level) ? $lines * 5 : $lines;
|
||||
|
||||
$process = new Process(['docker', 'exec', $containerName, 'tail', '-n', (string) $readLines, $logPath]);
|
||||
$process->setTimeout(10);
|
||||
$process->run();
|
||||
|
||||
if (!$process->isSuccessful()) {
|
||||
return sprintf('Error reading log: %s', trim($process->getErrorOutput()));
|
||||
}
|
||||
|
||||
return $this->formatSymfonyOutput($process->getOutput(), $lines, $level);
|
||||
}
|
||||
|
||||
private function formatSymfonyOutput(string $output, int $lines, ?string $level): string
|
||||
{
|
||||
$rawLines = explode("\n", trim($output));
|
||||
$formatted = [];
|
||||
|
||||
foreach ($rawLines as $line) {
|
||||
if ('' === $line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parsed = $this->parseSymfonyLogLine($line);
|
||||
|
||||
if (null === $parsed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip noisy channels
|
||||
if ('doctrine' === $parsed['channel']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null !== $level && '' !== $level && !str_contains(strtoupper($parsed['level']), strtoupper($level))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$formatted[] = sprintf('[%s] %s.%s: %s', $parsed['date'], $parsed['channel'], $parsed['level'], $parsed['message']);
|
||||
}
|
||||
|
||||
// Keep only the last N lines after filtering
|
||||
$formatted = \array_slice($formatted, -$lines);
|
||||
|
||||
return implode("\n", $formatted);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{date: string, level: string, channel: string, message: string}|null
|
||||
*/
|
||||
private function parseSymfonyLogLine(string $line): ?array
|
||||
{
|
||||
// Standard Symfony monolog format: [2026-04-03T15:33:19.304937+02:00] channel.LEVEL: message {context} []
|
||||
if (preg_match('/^\[([^\]]+)\]\s+(\w+)\.(\w+):\s+(.+)$/', $line, $matches)) {
|
||||
$date = substr($matches[1], 0, 19); // Trim to YYYY-MM-DDTHH:MM:SS
|
||||
$message = $matches[4];
|
||||
|
||||
// Remove JSON context at the end: {"key":"value"} []
|
||||
$message = preg_replace('/\s*\{.*\}\s*\[\]\s*$/', '', $message) ?? $message;
|
||||
// Remove trailing []
|
||||
$message = preg_replace('/\s*\[\]\s*$/', '', $message) ?? $message;
|
||||
|
||||
return [
|
||||
'date' => str_replace('T', ' ', $date),
|
||||
'level' => $matches[3],
|
||||
'channel' => $matches[2],
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
54
src/State/ApplicationProvider.php
Normal file
54
src/State/ApplicationProvider.php
Normal 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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/State/DockerLogProvider.php
Normal file
49
src/State/DockerLogProvider.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\ApiResource\LogOutput;
|
||||
use App\Repository\EnvironmentRepository;
|
||||
use App\Service\LogService;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final readonly class DockerLogProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EnvironmentRepository $environmentRepository,
|
||||
private LogService $logService,
|
||||
private RequestStack $requestStack,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): LogOutput
|
||||
{
|
||||
$id = $uriVariables['id'] ?? null;
|
||||
$environment = $id ? $this->environmentRepository->find($id) : null;
|
||||
|
||||
if (null === $environment) {
|
||||
throw new NotFoundHttpException(sprintf('Environment "%s" not found.', $id));
|
||||
}
|
||||
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
$lines = (int) ($request?->query->get('lines', '100') ?? 100);
|
||||
$since = $request?->query->get('since');
|
||||
|
||||
$content = $this->logService->getDockerLogs(
|
||||
$environment->getContainerName(),
|
||||
$lines,
|
||||
$since,
|
||||
);
|
||||
|
||||
$dto = new LogOutput();
|
||||
$dto->content = $content;
|
||||
$dto->lines = $lines;
|
||||
$dto->source = sprintf('docker:%s', $environment->getContainerName());
|
||||
|
||||
return $dto;
|
||||
}
|
||||
}
|
||||
44
src/State/EnvironmentCreateProcessor.php
Normal file
44
src/State/EnvironmentCreateProcessor.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ final readonly class EnvironmentHealthProvider implements ProviderInterface
|
||||
$dto->memoryUsage = $stats['memoryUsage'];
|
||||
$dto->memoryLimit = $stats['memoryLimit'];
|
||||
$dto->memoryPercent = $stats['memoryPercent'];
|
||||
$dto->ports = $status['ports'];
|
||||
|
||||
return $dto;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
58
src/State/SymfonyLogProvider.php
Normal file
58
src/State/SymfonyLogProvider.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\ApiResource\LogOutput;
|
||||
use App\Repository\EnvironmentRepository;
|
||||
use App\Repository\LogFileRepository;
|
||||
use App\Service\LogService;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final readonly class SymfonyLogProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EnvironmentRepository $environmentRepository,
|
||||
private LogFileRepository $logFileRepository,
|
||||
private LogService $logService,
|
||||
private RequestStack $requestStack,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): LogOutput
|
||||
{
|
||||
$envId = $uriVariables['id'] ?? null;
|
||||
$logFileId = $uriVariables['logFileId'] ?? null;
|
||||
|
||||
$environment = $envId ? $this->environmentRepository->find($envId) : null;
|
||||
if (null === $environment) {
|
||||
throw new NotFoundHttpException(sprintf('Environment "%s" not found.', $envId));
|
||||
}
|
||||
|
||||
$logFile = $logFileId ? $this->logFileRepository->find($logFileId) : null;
|
||||
if (null === $logFile || $logFile->getEnvironment()?->getId() !== $environment->getId()) {
|
||||
throw new NotFoundHttpException(sprintf('Log file "%s" not found.', $logFileId));
|
||||
}
|
||||
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
$lines = (int) ($request?->query->get('lines', '100') ?? 100);
|
||||
$level = $request?->query->get('level');
|
||||
|
||||
$content = $this->logService->getSymfonyLog(
|
||||
$environment->getContainerName(),
|
||||
$logFile->getPath(),
|
||||
$lines,
|
||||
$level,
|
||||
);
|
||||
|
||||
$dto = new LogOutput();
|
||||
$dto->content = $content;
|
||||
$dto->lines = $lines;
|
||||
$dto->source = sprintf('symfony:%s', $logFile->getLabel());
|
||||
|
||||
return $dto;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user