diff --git a/app.json b/app.json new file mode 100644 index 0000000..310710f --- /dev/null +++ b/app.json @@ -0,0 +1,13 @@ +{ + "id": "manus-dashboard", + "name": "Dashboard Recette", + "category": "INFRA", + "urls": { + "recette": "https://dashboard.recette.santinova-soft.org", + "prod": "https://dashboard.santinova-soft.org" + }, + "containerName": "manus-dashboard", + "image": "images/dashboard.png", + "giteaRepo": "manus-dashboard", + "giteaOwner": "manus-admin" +} diff --git a/src/backend/src/app-creator.js b/src/backend/src/app-creator.js index da870bd..0ec2901 100644 --- a/src/backend/src/app-creator.js +++ b/src/backend/src/app-creator.js @@ -434,7 +434,7 @@ function addDynamicApp(appDef) { } function initDynamicApps() { - // 1. Charger les apps depuis .dashboard-apps.json (créées via l'interface) + // 1. Charger les apps depuis .dashboard-apps.json (legacy) const dynamicApps = loadDynamicApps(); for (const app of dynamicApps) { const existingInConfig = config.apps.findIndex((a) => a.id === app.id); @@ -442,39 +442,32 @@ function initDynamicApps() { config.apps.push(app); } } - - // 2. Découverte automatique : scanner les dossiers apps/*.json - let discovered = 0; + // 2. Scanner les dossiers app.json (découverte automatique) try { - const appsBasePath = config.appsBasePath; - if (fs.existsSync(appsBasePath)) { - const entries = fs.readdirSync(appsBasePath, { withFileTypes: true }); - for (const entry of entries) { - if (!entry.isDirectory()) continue; - const appJsonPath = path.join(appsBasePath, entry.name, 'app.json'); - if (!fs.existsSync(appJsonPath)) continue; - try { - const appDef = JSON.parse(fs.readFileSync(appJsonPath, 'utf-8')); - if (!appDef.id) continue; - // Ne pas écraser une app déjà en config (statique ou dynamique) - const existingIdx = config.apps.findIndex((a) => a.id === appDef.id); - if (existingIdx >= 0) { - // Mettre à jour avec les données du app.json (source de vérité) - config.apps[existingIdx] = { ...config.apps[existingIdx], ...appDef }; - } else { - config.apps.push(appDef); - discovered++; - } - } catch (e) { - console.warn(`[discovery] Erreur lecture ${appJsonPath}:`, e.message); + const appsBasePath = config.appsBasePath || process.env.APPS_BASE_PATH || '/opt/manus-deploy/apps'; + const entries = fs.readdirSync(appsBasePath, { withFileTypes: true }); + let scanned = 0; + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const appJsonPath = path.join(appsBasePath, entry.name, 'app.json'); + if (!fs.existsSync(appJsonPath)) continue; + try { + const appDef = JSON.parse(fs.readFileSync(appJsonPath, 'utf-8')); + if (!appDef.id) continue; + const existingInConfig = config.apps.findIndex((a) => a.id === appDef.id); + if (existingInConfig < 0) { + config.apps.push(appDef); + scanned++; } + } catch (e) { + console.error(`Erreur lecture app.json dans ${entry.name}:`, e.message); } } + console.log(`Apps découvertes via app.json: ${scanned}`); } catch (err) { - console.error('[discovery] Erreur scan apps:', err.message); + console.error('Erreur scan app.json:', err.message); } - - console.log(`Apps chargées: ${dynamicApps.length} dynamiques + ${discovered} découvertes (total: ${config.apps.length})`); + console.log(`Total apps chargées: ${config.apps.length}`); } // ============ CREATION D'APPLICATION ============ diff --git a/src/backend/src/config.js b/src/backend/src/config.js index 4a5f493..a885ce3 100644 --- a/src/backend/src/config.js +++ b/src/backend/src/config.js @@ -121,6 +121,7 @@ module.exports = { healthCheckUrl: 'https://pilotage-ms.recette.santinova-soft.org', port: 3000, category: 'ITINOVA', + image: 'images/pilotage-ms.png', status: 'recette', }, { @@ -155,6 +156,7 @@ module.exports = { healthCheckUrl: 'https://demat-facturation.recette.santinova-soft.org', port: 3000, category: 'SANTINOVA', + image: 'images/demat-facturation-dsi.jpg', status: 'production', }, { @@ -205,7 +207,7 @@ module.exports = { containerName: 'portail-santinova', healthCheckUrl: 'https://portail.recette.santinova-soft.org', port: 3000, - category: 'SANTINOVA', + category: 'INFRA', status: 'production', }, { diff --git a/src/backend/src/docker.js b/src/backend/src/docker.js index 3245e27..7122a79 100644 --- a/src/backend/src/docker.js +++ b/src/backend/src/docker.js @@ -139,11 +139,38 @@ function gitPull(appConfig) { /** * Démarrer un conteneur Docker */ -async function startContainer(containerName) { +async function startContainer(containerName, appId) { try { + // Tenter d'abord un docker start (conteneur existant mais arrêté) const container = docker.getContainer(containerName); - await container.start(); - return { success: true, message: `Conteneur ${containerName} démarré` }; + const info = await container.inspect().catch(() => null); + if (info) { + // Le conteneur existe, on le démarre + if (!info.State.Running) { + await container.start(); + } + return { success: true, message: `Conteneur ${containerName} démarré` }; + } + // Le conteneur n'existe pas : docker compose up dans le répertoire de l'app + const id = appId || containerName; + const appsBasePath = config.appsBasePath || process.env.APPS_BASE_PATH || '/opt/manus-deploy/apps'; + const appDir = path.join(appsBasePath, id); + const fs = require('fs'); + const composeFile = fs.existsSync(path.join(appDir, 'docker-compose.yml')) + ? path.join(appDir, 'docker-compose.yml') + : fs.existsSync(path.join(appDir, 'docker-compose.prod.yml')) + ? path.join(appDir, 'docker-compose.prod.yml') + : null; + if (!composeFile) { + return { success: false, message: `Aucun docker-compose.yml trouvé dans ${appDir}` }; + } + await new Promise((resolve, reject) => { + exec(`docker compose -f ${composeFile} up -d`, { cwd: appDir }, (err, stdout, stderr) => { + if (err) reject(new Error(stderr || err.message)); + else resolve(stdout); + }); + }); + return { success: true, message: `Conteneur ${containerName} créé et démarré via docker compose` }; } catch (err) { return { success: false, message: err.message }; } diff --git a/src/backend/src/routes.js b/src/backend/src/routes.js index b4eb6d2..fbdd2e4 100644 --- a/src/backend/src/routes.js +++ b/src/backend/src/routes.js @@ -482,7 +482,7 @@ router.post('/apps/:id/start', authMiddleware, async (req, res) => { const app = config.apps.find((a) => a.id === req.params.id); if (!app) return res.status(404).json({ error: 'Application non trouvée' }); const containerName = app.containerName || app.id; - const result = await startContainer(containerName); + const result = await startContainer(containerName, app.id); res.json(result); } catch (err) { res.status(500).json({ error: err.message });