diff --git a/docker-compose.yml b/docker-compose.yml index 8feb276..46c7229 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,38 +5,35 @@ services: dockerfile: Dockerfile container_name: manus-dashboard restart: unless-stopped - privileged: true - env_file: - - .env environment: - - NODE_ENV=production - - PORT=3001 - - JWT_SECRET=${DASHBOARD_JWT_SECRET} - - ADMIN_USERNAME=${DASHBOARD_ADMIN_USERNAME} - - ADMIN_PASSWORD=${DASHBOARD_ADMIN_PASSWORD} - - GITEA_URL=${GITEA_URL} - - GITEA_USERNAME=${GITEA_USERNAME} - - GITEA_PASSWORD=${GITEA_PASSWORD} - - WEBHOOK_SECRET=${WEBHOOK_SECRET} - - APPS_BASE_PATH=${APPS_BASE_PATH} - - INFRA_BASE_PATH=${INFRA_BASE_PATH} - - HEALTH_CHECK_INTERVAL=${HEALTH_CHECK_INTERVAL} + NODE_ENV: production + PORT: 3001 + JWT_SECRET: manus-dashboard-prod-secret-2026 + ADMIN_USERNAME: adminItinova + ADMIN_PASSWORD: Itinova69! + GITEA_URL: https://git.santinova-soft.org + GITEA_USERNAME: manus-admin + GITEA_PASSWORD: ManusGitea2026! + APPS_BASE_PATH: /opt/manus-deploy/apps + INFRA_BASE_PATH: /opt/manus-deploy/infrastructure + WEBHOOK_SECRET: webhook-prod-secret-2026 volumes: - - /opt/manus-deploy/webhook-patched.js:/app/backend/src/webhook.js:ro - /var/run/docker.sock:/var/run/docker.sock - - /opt/manus-deploy:/opt/manus-deploy + - /opt/manus-deploy/apps:/opt/manus-deploy/apps + - /opt/manus-deploy/infrastructure:/opt/manus-deploy/infrastructure + - /opt/manus-deploy/logs:/opt/manus-deploy/logs + # Montage /proc du VPS pour CPU (namespace partagé avec le VPS) + - /proc:/host/proc:ro + # Montage cgroup du VPS pour RAM réelle (memory.max, memory.current) + - /sys/fs/cgroup:/host/cgroup:ro networks: - web labels: - "traefik.enable=true" - - "traefik.http.routers.manus-dashboard.rule=Host(`dashboard.santinova-soft.org`)" - - "traefik.http.routers.manus-dashboard.entrypoints=websecure" - - "traefik.http.routers.manus-dashboard.tls=true" - - "traefik.http.routers.manus-dashboard.tls.certresolver=letsencrypt" - - "traefik.http.routers.manus-dashboard.service=manus-dashboard-svc" - - "traefik.http.routers.manus-dashboard.priority=100" - - "traefik.http.services.manus-dashboard-svc.loadbalancer.server.port=3001" - - "traefik.docker.network=web" + - "traefik.http.routers.dashboard.rule=Host(`dashboard.santinova-soft.org`)" + - "traefik.http.routers.dashboard.entrypoints=websecure" + - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt" + - "traefik.http.services.dashboard.loadbalancer.server.port=3001" networks: web: external: true diff --git a/src/backend/src/config.js b/src/backend/src/config.js index 3a60082..c359ff7 100644 --- a/src/backend/src/config.js +++ b/src/backend/src/config.js @@ -2,113 +2,105 @@ module.exports = { port: process.env.PORT || 3001, jwtSecret: process.env.JWT_SECRET || 'manus-dashboard-secret-2026', jwtExpiry: '24h', - // Authentification auth: { username: process.env.ADMIN_USERNAME || 'adminItinova', password: process.env.ADMIN_PASSWORD || 'Itinova69!', }, - // Gitea gitea: { url: process.env.GITEA_URL || 'https://git.santinova-soft.org', username: process.env.GITEA_USERNAME || 'manus-admin', password: process.env.GITEA_PASSWORD || 'ManusGitea2026!', }, - // Applications config appsBasePath: process.env.APPS_BASE_PATH || '/opt/manus-deploy/apps', infrastructurePath: process.env.INFRA_BASE_PATH || '/opt/manus-deploy/infrastructure', - // Health check interval (ms) healthCheckInterval: parseInt(process.env.HEALTH_CHECK_INTERVAL) || 30000, - // Apps configuration - can be extended apps: [ { id: 'itinova-contacts', name: 'Itinova Contacts', - description: 'Application de gestion des contacts', - directory: 'itinova-contacts', - giteaRepo: 'itinova-contacts', - giteaOwner: 'manus-admin', + description: 'Application de gestion des contacts Itinova', urls: { recette: 'https://contacts.recette.santinova-soft.org', - prod: null, + prod: 'https://contacts.santinova-soft.org', }, + giteaOwner: 'manus-admin', + giteaRepo: 'itinova-contacts', + composeFile: 'docker-compose.prod.yml', containerName: 'itinova-contacts', - healthCheckUrl: 'https://contacts.recette.santinova-soft.org', - port: 3000, + category: 'ITINOVA', + status: 'production', }, { - id: 'itinova-podcasts', - name: 'Itinova Podcasts', - description: 'Application de gestion de podcasts', - directory: 'itinova-podcasts', - giteaRepo: 'itinova-podcasts', - giteaOwner: 'manus-admin', + id: 'itinova-vehicle-exchange', + name: 'Gestion de Flotte', + description: 'Bourse de vehicules Itinova - Partage et echange de vehicules entre etablissements', urls: { - recette: 'https://podcasts.recette.santinova-soft.org', - prod: null, + recette: 'https://flotte.recette.santinova-soft.org', + prod: 'https://flotte.santinova-soft.org', }, - containerName: 'itinova-podcasts', - healthCheckUrl: 'https://podcasts.recette.santinova-soft.org', - port: 3000, + giteaOwner: 'manus-admin', + giteaRepo: 'itinova-vehicle-exchange', + composeFile: 'docker-compose.yml', + containerName: 'itinova-vehicle-exchange', + category: 'ITINOVA', + status: 'production', }, { id: 'veille-reglementaire', name: 'Veille Réglementaire', - description: 'Application de veille réglementaire et appels à projets', - directory: 'veille-reglementaire', - giteaRepo: 'veille-reglementaire', - giteaOwner: 'manus-admin', + description: 'Application de veille réglementaire — Direction des Opérations Itinova', urls: { recette: 'https://veille.recette.santinova-soft.org', - prod: null, + prod: 'https://veille.santinova-soft.org', }, containerName: 'veille-reglementaire', - healthCheckUrl: 'https://veille.recette.santinova-soft.org', - port: 3000, + category: 'ITINOVA', + status: 'production', }, { - id: 'itinova-vehicle-exchange', - name: 'Itinova Gestion de Flotte', - description: 'Application de gestion de flotte de véhicules', - directory: 'itinova-vehicle-exchange', - giteaRepo: 'itinova-vehicle-exchange', - giteaOwner: 'manus-admin', + id: 'itinova-podcasts', + name: 'Itinova Podcasts', + description: 'Gestionnaire de podcasts pour les établissements Itinova', urls: { - recette: 'https://flotte.recette.santinova-soft.org', - prod: null, + recette: 'https://podcasts.recette.santinova-soft.org', + prod: 'https://podcasts.santinova-soft.org', }, - containerName: 'itinova-vehicle-exchange', - healthCheckUrl: 'https://flotte.recette.santinova-soft.org', - port: 3000, + giteaOwner: 'manus-admin', + giteaRepo: 'itinova-podcasts', + composeFile: 'docker-compose.yml', + containerName: 'itinova-podcasts', + category: 'ITINOVA', + status: 'production', }, { id: 'sonum', - name: 'Sonum', - description: 'Application Sonum', - directory: 'sonum', - giteaRepo: 'sonum', - giteaOwner: 'manus-admin', + name: 'SONUM', + description: 'Cartographie des Solutions Numériques FEHAP', urls: { recette: 'https://sonum.recette.santinova-soft.org', - prod: null, + prod: 'https://sonum.santinova-soft.org', }, + giteaOwner: 'manus-admin', + giteaRepo: 'sonum', + composeFile: 'docker-compose.yml', containerName: 'sonum', - healthCheckUrl: 'https://sonum.recette.santinova-soft.org', - port: 3000, + category: 'SANTINOVA', + status: 'production', }, { - id: 'facturation-santinova', - name: 'Facturation Santinova', - description: 'Application de gestion de la facturation', - directory: 'facturation-santinova', - giteaRepo: 'facturation-santinova', - giteaOwner: 'manus-admin', + id: 'demat-facturation-dsi', + name: 'Démat Facturation DSI', + description: 'Dématérialisation de la facturation fournisseurs — Direction des Systèmes d\'Information', urls: { - recette: 'https://facturation.recette.santinova-soft.org', - prod: null, + recette: 'https://demat-facturation.recette.santinova-soft.org', + prod: 'https://demat-facturation.santinova-soft.org', }, - containerName: 'facturation-santinova-app', - healthCheckUrl: 'https://facturation.recette.santinova-soft.org', - port: 3001, + giteaOwner: 'manus-admin', + giteaRepo: 'demat-facturation-dsi', + composeFile: 'docker-compose.yml', + containerName: 'demat-facturation-app', + category: 'SANTINOVA', + status: 'production', }, ], -}; +}; \ No newline at end of file diff --git a/src/backend/src/docker.js b/src/backend/src/docker.js index 4b351a9..9cb71cc 100644 --- a/src/backend/src/docker.js +++ b/src/backend/src/docker.js @@ -1,10 +1,14 @@ const Docker = require('dockerode'); const { exec } = require('child_process'); +const fs = require('fs'); const path = require('path'); const config = require('./config'); const docker = new Docker({ socketPath: '/var/run/docker.sock' }); +// Chemin du fichier de métriques écrit par le script host +const HOST_METRICS_FILE = '/opt/manus-deploy/logs/host_metrics.json'; + /** * Récupérer les informations d'un conteneur Docker */ @@ -38,7 +42,6 @@ async function getContainerLogs(containerName, tail = 100) { tail: tail, timestamps: true, }); - // Parse buffer to string return logs.toString('utf8'); } catch (err) { return `Erreur lors de la récupération des logs: ${err.message}`; @@ -80,8 +83,8 @@ function redeployApp(appConfig) { logLines.push(`[${new Date().toISOString()}] Commande: ${cmd}`); const process = exec(cmd, { - maxBuffer: 1024 * 1024 * 10, // 10MB - timeout: 300000, // 5 minutes + maxBuffer: 1024 * 1024 * 10, + timeout: 300000, }); process.stdout.on('data', (data) => { @@ -134,8 +137,6 @@ function gitPull(appConfig) { }); } - - /** * Démarrer un conteneur Docker */ @@ -176,83 +177,134 @@ async function restartContainer(containerName) { } /** - * Récupérer les métriques système du serveur (CPU, RAM, disque, réseau) + * Récupérer les métriques système du serveur (CPU, RAM, disque, réseau, uptime) + * + * Architecture LXC + Docker : le conteneur est isolé dans ses namespaces réseau/PID, + * ce qui empêche la lecture directe de /proc pour certaines métriques. + * + * Solution : lire le fichier /opt/manus-deploy/logs/host_metrics.json + * écrit en continu par le script metrics_collector.sh tournant sur le VPS hôte. + * Ce fichier est accessible via le volume monté dans le conteneur. + * + * Fallback : calcul direct depuis /proc (moins précis pour RAM/uptime/réseau). */ function getServerMetrics() { return new Promise((resolve) => { - const { execSync } = require("child_process"); + // ── Tentative 1 : lire le fichier de métriques du host ───────────────── try { - // nsenter -t 1 -m lit les métriques réelles de l'hôte VPS (pas de l'hyperviseur) - const ns = "nsenter -t 1 -m --"; + if (fs.existsSync(HOST_METRICS_FILE)) { + const stat = fs.statSync(HOST_METRICS_FILE); + const ageMs = Date.now() - stat.mtimeMs; + // Fichier valide si moins de 60 secondes + if (ageMs < 60000) { + const raw = fs.readFileSync(HOST_METRICS_FILE, 'utf8'); + const metrics = JSON.parse(raw); + // Rafraîchir le timestamp + metrics.timestamp = new Date().toISOString(); + return resolve(metrics); + } + } + } catch (e) { + // Fichier absent ou corrompu → fallback + } - // CPU : delta /proc/stat sur 200ms + // ── Fallback : calcul depuis /proc (valeurs approchées) ──────────────── + const { execSync } = require('child_process'); + try { + // CPU via /host/proc/stat (namespace CPU partagé avec le VPS) let cpuUsage = 0; try { - const s1 = execSync(`${ns} cat /proc/stat | grep '^cpu '`).toString().trim().split(/\s+/); - execSync("sleep 0.2"); - const s2 = execSync(`${ns} cat /proc/stat | grep '^cpu '`).toString().trim().split(/\s+/); - const idle1 = parseInt(s1[4]), total1 = s1.slice(1).reduce((a,b)=>a+parseInt(b),0); - const idle2 = parseInt(s2[4]), total2 = s2.slice(1).reduce((a,b)=>a+parseInt(b),0); - const dIdle = idle2 - idle1, dTotal = total2 - total1; + const procStat = fs.existsSync('/host/proc/stat') ? '/host/proc/stat' : '/proc/stat'; + const s1 = execSync(`grep '^cpu ' ${procStat}`).toString().trim().split(/\s+/).slice(1).map(Number); + execSync('sleep 0.3'); + const s2 = execSync(`grep '^cpu ' ${procStat}`).toString().trim().split(/\s+/).slice(1).map(Number); + const total1 = s1.reduce((a, b) => a + b, 0); + const total2 = s2.reduce((a, b) => a + b, 0); + const dTotal = total2 - total1; + const dIdle = s2[3] - s1[3]; cpuUsage = dTotal > 0 ? Math.round((1 - dIdle / dTotal) * 100) : 0; - } catch(e) { + } catch (e) {} + + // RAM depuis cgroup v2 du VPS (accessible via /sys/fs/cgroup monté ou /host/cgroup) + let memTotal = 0, memUsed = 0, memFree = 0, memAvailable = 0; + try { + // Essayer /host/cgroup d'abord, puis /sys/fs/cgroup + const cgroupBase = fs.existsSync('/host/cgroup/memory.max') ? '/host/cgroup' : '/sys/fs/cgroup'; + const memMaxRaw = fs.readFileSync(`${cgroupBase}/memory.max`, 'utf8').trim(); + const memCurrRaw = fs.readFileSync(`${cgroupBase}/memory.current`, 'utf8').trim(); + + if (memMaxRaw !== 'max') { + memTotal = parseInt(memMaxRaw); + memUsed = parseInt(memCurrRaw); + // Tenter de lire la RAM inactive (cache) depuis memory.stat + try { + const memStat = fs.readFileSync(`${cgroupBase}/memory.stat`, 'utf8'); + const inactiveFile = parseInt((memStat.match(/^inactive_file\s+(\d+)/m) || [0, 0])[1]); + const activeFile = parseInt((memStat.match(/^active_file\s+(\d+)/m) || [0, 0])[1]); + const cache = inactiveFile + activeFile; + memAvailable = memTotal - memUsed + cache; + memFree = memAvailable; + } catch (e) { + memAvailable = memTotal - memUsed; + memFree = memAvailable; + } + } + } catch (e) { + // Fallback /proc/meminfo (valeurs du namespace du conteneur) try { - const cpuRaw = execSync(`${ns} top -bn1 | grep 'Cpu(s)' | awk '{print $2}'`).toString().trim(); - cpuUsage = parseFloat(cpuRaw) || 0; - } catch(e2) {} + const meminfo = fs.readFileSync('/proc/meminfo', 'utf8'); + const getVal = (key) => { + const m = meminfo.match(new RegExp(`^${key}:\\s+(\\d+)`, 'm')); + return m ? parseInt(m[1]) * 1024 : 0; + }; + memTotal = getVal('MemTotal'); + memFree = getVal('MemFree'); + memAvailable = getVal('MemAvailable') || memFree; + memUsed = memTotal - memAvailable; + } catch (e2) {} } - // RAM : cgroup de l'hôte (fiable même en environnement VPS virtualisé) - // La RAM totale est lue depuis /sys/fs/cgroup/memory.max de l'hôte - // La RAM utilisée est calculée via docker stats (valeurs réelles du VPS) - let memTotal = 8589934592; // 8 Go par défaut + // Disque : df sur / + let diskTotal = 0, diskUsed = 0, diskFree = 0; try { - const cgroupMax = execSync(`${ns} cat /sys/fs/cgroup/memory.max 2>/dev/null || cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null`).toString().trim(); - if (cgroupMax && cgroupMax !== 'max' && !isNaN(parseInt(cgroupMax))) { - memTotal = parseInt(cgroupMax); - } - } catch(e) {} + const diskRaw = execSync('df -B1 / | tail -1').toString().trim().split(/\s+/); + diskTotal = parseInt(diskRaw[1]); + diskUsed = parseInt(diskRaw[2]); + diskFree = parseInt(diskRaw[3]); + } catch (e) {} - // RAM utilisée : somme de tous les conteneurs via docker stats - let memUsed = 0; + // Uptime depuis /proc/uptime du VPS (namespace partagé avec le VPS) + let uptimeSeconds = 0; try { - const statsRaw = execSync("docker stats --no-stream --format '{{.MemUsage}}'").toString().trim(); - statsRaw.split('\n').forEach(line => { - const match = line.match(/([\d.]+)(\w+)\s*\//); - if (match) { - const val = parseFloat(match[1]); - const unit = match[2].toLowerCase(); - if (unit === 'gib') memUsed += val * 1024 * 1024 * 1024; - else if (unit === 'mib') memUsed += val * 1024 * 1024; - else if (unit === 'kib') memUsed += val * 1024; - else memUsed += val; - } - }); - } catch(e) {} - const memAvailable = memTotal - memUsed; - const memFree = memAvailable; + const uptimeFile = fs.existsSync('/host/proc/uptime') ? '/host/proc/uptime' : '/proc/uptime'; + uptimeSeconds = parseFloat(fs.readFileSync(uptimeFile, 'utf8').trim().split(' ')[0]) || 0; + } catch (e) {} - // Disque : df sur l'hôte - const diskRaw = execSync(`${ns} df -B1 / | tail -1`).toString().trim().split(/\s+/); - const diskTotal = parseInt(diskRaw[1]); - const diskUsed = parseInt(diskRaw[2]); - const diskFree = parseInt(diskRaw[3]); + // Load average + let load1 = 0, load5 = 0, load15 = 0; + try { + const loadFile = fs.existsSync('/host/proc/loadavg') ? '/host/proc/loadavg' : '/proc/loadavg'; + const loadRaw = fs.readFileSync(loadFile, 'utf8').trim().split(' '); + load1 = parseFloat(loadRaw[0]) || 0; + load5 = parseFloat(loadRaw[1]) || 0; + load15 = parseFloat(loadRaw[2]) || 0; + } catch (e) {} - // Uptime et load - const uptimeRaw = execSync(`${ns} cat /proc/uptime`).toString().trim().split(" "); - const uptimeSeconds = parseFloat(uptimeRaw[0]); - const loadRaw = execSync(`${ns} cat /proc/loadavg`).toString().trim().split(" "); - const load1 = parseFloat(loadRaw[0]); - const load5 = parseFloat(loadRaw[1]); - const load15 = parseFloat(loadRaw[2]); - - // Réseau + // Réseau : /proc/net/dev du VPS (namespace réseau partagé avec le VPS, pas le conteneur) let netRx = 0, netTx = 0; try { - const netRaw = execSync(`${ns} cat /proc/net/dev | grep -E 'eth0|ens|enp' | head -1`).toString().trim().split(/\s+/); - netRx = parseInt(netRaw[1]) || 0; - netTx = parseInt(netRaw[9]) || 0; - } catch(e) {} + const netFile = fs.existsSync('/host/proc/net/dev') ? '/host/proc/net/dev' : '/proc/net/dev'; + const netRaw = fs.readFileSync(netFile, 'utf8'); + const line = netRaw.split('\n').find((l) => /^\s*(eth0|venet0|ens\d+|enp\d+)/.test(l)); + if (line) { + const parts = line.trim().split(/\s+/); + netRx = parseInt(parts[1]) || 0; + netTx = parseInt(parts[9]) || 0; + } + } catch (e) {} + + const memPct = memTotal > 0 ? Math.round((memUsed / memTotal) * 100) : 0; + const diskPct = diskTotal > 0 ? Math.round((diskUsed / diskTotal) * 100) : 0; resolve({ cpu: { usage: cpuUsage }, @@ -261,24 +313,26 @@ function getServerMetrics() { used: memUsed, free: memFree, available: memAvailable, - usagePercent: Math.round((memUsed / memTotal) * 100) + usagePercent: memPct, }, disk: { total: diskTotal, used: diskUsed, free: diskFree, - usagePercent: Math.round((diskUsed / diskTotal) * 100) + usagePercent: diskPct, }, uptime: uptimeSeconds, load: { load1, load5, load15 }, network: { rx: netRx, tx: netTx }, - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), + source: 'fallback', }); } catch (err) { resolve({ error: err.message }); } }); } + module.exports = { docker, getContainerInfo, diff --git a/src/backend/src/routes.js b/src/backend/src/routes.js index e83a978..35ea215 100644 --- a/src/backend/src/routes.js +++ b/src/backend/src/routes.js @@ -475,3 +475,78 @@ router.get('/system/metrics', authMiddleware, async (req, res) => { }); module.exports = router; + +// ===== INVENTAIRE DES APPLICATIONS ===== +const { exec } = require('child_process'); +const GITEA_RECETTE_INTERNAL = process.env.GITEA_RECETTE_URL || 'https://git.recette.santinova-soft.org'; +const GITEA_PROD_EXTERNAL = process.env.GITEA_PROD_INTERNAL_URL || 'http://172.18.0.9:3000'; +const GITEA_PROD_PUBLIC = process.env.GITEA_PROD_PUBLIC_URL || 'https://git.santinova-soft.org'; +const GITEA_USER_INV = process.env.GITEA_USERNAME || 'manus-admin'; +const GITEA_PASS_REC = process.env.GITEA_PASSWORD || 'Itinova69!'; +const GITEA_PASS_PRD = process.env.GITEA_PASSWORD_PROD || 'ManusGitea2026!'; + +const INVENTORY_APPS = [ + { id: 'itinova-contacts', name: 'Itinova Contacts', repoName: 'itinova-contacts' }, + { id: 'itinova-podcasts', name: 'Itinova Podcasts', repoName: 'itinova-podcasts' }, + { id: 'veille-reglementaire', name: 'Veille Réglementaire', repoName: 'veille-reglementaire' }, + { id: 'itinova-vehicle-exchange', name: 'Itinova Gestion de Flotte', repoName: 'itinova-vehicle-exchange' }, + { id: 'sonum', name: 'SONUM', repoName: 'sonum' }, + { id: 'demat-facturation-dsi', name: 'Démat. Facturation DSI', repoName: 'demat-facturation-dsi' }, + { id: 'facturation-santinova', name: 'Facturation Santinova', repoName: 'facturation-santinova' }, + { id: 'formation-manager-itinova', name: 'Formation Manager', repoName: 'formation-manager-itinova' }, + { id: 'portail-santinova', name: 'Portail Applicatif', repoName: 'portail-santinova' }, + { id: 'manus-dashboard', name: 'Dashboard Manus', repoName: 'manus-dashboard' }, +]; + +function curlGitea(baseUrl, owner, repo, pass, publicBaseUrl) { + return new Promise((resolve) => { + const auth = `${GITEA_USER_INV}:${pass}`; + const cmd = `curl -sk --max-time 6 -u "${auth}" "${baseUrl}/api/v1/repos/${owner}/${repo}"`; + exec(cmd, { timeout: 7000 }, (err, stdout) => { + if (err || !stdout) return resolve({ present: false, url: null, version: null }); + try { + const data = JSON.parse(stdout); + if (!data.id) return resolve({ present: false, url: null, version: null }); + // Récupérer le dernier commit + const cmd2 = `curl -sk --max-time 6 -u "${auth}" "${baseUrl}/api/v1/repos/${owner}/${repo}/commits?limit=1"`; + exec(cmd2, { timeout: 7000 }, (err2, stdout2) => { + let version = null; + try { + const commits = JSON.parse(stdout2 || '[]'); + if (commits.length > 0) { + const c = commits[0]; + const sha = c.sha ? c.sha.substring(0, 7) : null; + const date = c.commit && c.commit.author && c.commit.author.date + ? new Date(c.commit.author.date).toLocaleDateString('fr-FR') : null; + version = sha && date ? `${sha} (${date})` : sha; + } + } catch(e) {} + const displayUrl = publicBaseUrl || baseUrl; + resolve({ present: true, url: `${displayUrl}/${owner}/${repo}`, version }); + }); + } catch(e) { + resolve({ present: false, url: null, version: null }); + } + }); + }); +} + +router.get('/inventory', authMiddleware, async (req, res) => { + try { + const owner = GITEA_USER_INV; + const results = await Promise.all( + INVENTORY_APPS.map(async (app) => { + const [repoRecette, repoProd] = await Promise.all([ + curlGitea(GITEA_RECETTE_INTERNAL, owner, app.repoName, GITEA_PASS_REC), + curlGitea(GITEA_PROD_EXTERNAL, owner, app.repoName, GITEA_PASS_PRD, GITEA_PROD_PUBLIC), + ]); + return { ...app, repoRecette, repoProd }; + }) + ); + res.json(results); + } catch (err) { + console.error('Erreur /inventory:', err.message); + res.status(500).json({ error: err.message }); + } +}); +; diff --git a/src/docker-compose.yml b/src/docker-compose.yml index 8134de9..f702596 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -5,6 +5,8 @@ services: dockerfile: Dockerfile container_name: manus-dashboard restart: unless-stopped + ports: + - "3001:3001" environment: - NODE_ENV=production - PORT=3001 @@ -13,7 +15,10 @@ services: - ADMIN_PASSWORD=Itinova69! - GITEA_URL=https://git.santinova-soft.org - GITEA_USERNAME=manus-admin - - GITEA_PASSWORD=ManusGitea2026! + - GITEA_PASSWORD=Itinova69! + - GITEA_PASSWORD_PROD=ManusGitea2026! + - GITEA_RECETTE_URL=https://git.recette.santinova-soft.org + - GITEA_PROD_INTERNAL_URL=http://172.18.0.9:3000 - APPS_BASE_PATH=/opt/manus-deploy/apps - INFRA_BASE_PATH=/opt/manus-deploy/infrastructure - HEALTH_CHECK_INTERVAL=30000 diff --git a/src/frontend/public/infra_schema_v2.png b/src/frontend/public/infra_schema_v2.png new file mode 100644 index 0000000..43fb240 Binary files /dev/null and b/src/frontend/public/infra_schema_v2.png differ diff --git a/src/frontend/src/App.jsx b/src/frontend/src/App.jsx index 9c415b8..6642b20 100644 --- a/src/frontend/src/App.jsx +++ b/src/frontend/src/App.jsx @@ -6,16 +6,16 @@ import MonitoringPage from './pages/MonitoringPage'; import DeploymentsPage from './pages/DeploymentsPage'; import GiteaPage from './pages/GiteaPage'; import DockerPage from './pages/DockerPage'; +import DocumentationPage from './pages/DocumentationPage'; +import InventairePage from './pages/InventairePage'; import Sidebar from './components/Sidebar'; import useWebSocket from './hooks/useWebSocket'; import { getMe, getApps, logout as apiLogout } from './utils/api'; - export default function App() { const [authenticated, setAuthenticated] = useState(false); const [loading, setLoading] = useState(true); const [currentPage, setCurrentPage] = useState('dashboard'); const [apps, setApps] = useState([]); - // Vérifier l'authentification au chargement useEffect(() => { const token = localStorage.getItem('dashboard_token'); @@ -34,7 +34,6 @@ export default function App() { setLoading(false); } }, []); - const fetchApps = async () => { try { const res = await getApps(); @@ -43,23 +42,19 @@ export default function App() { console.error('Erreur chargement apps:', err); } }; - // WebSocket handler const handleWsMessage = useCallback((data) => { if (data.type === 'health_update') { setApps(data.data); } }, []); - const { connected: wsConnected } = useWebSocket( authenticated ? handleWsMessage : null ); - const handleLogin = (data) => { setAuthenticated(true); fetchApps(); }; - const handleLogout = async () => { try { await apiLogout(); @@ -70,7 +65,6 @@ export default function App() { setAuthenticated(false); setApps([]); }; - if (loading) { return (