diff --git a/app.json b/app.json new file mode 100644 index 0000000..5036e1c --- /dev/null +++ b/app.json @@ -0,0 +1,10 @@ +{ + "id": "manus-dashboard", + "name": "Dashboard Production", + "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/docker-compose.yml b/docker-compose.yml index 70a8294..800ed01 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,7 +29,7 @@ services: - web labels: - "traefik.enable=true" - - "traefik.http.routers.manus-dashboard.rule=Host(`dashboard.recette.santinova-soft.org`)" + - "traefik.http.routers.manus-dashboard.rule=Host(`dashboard.recette.santinova-soft.org`) || 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" diff --git a/src/backend/src/app-creator.js b/src/backend/src/app-creator.js index 535aef0..0ec2901 100644 --- a/src/backend/src/app-creator.js +++ b/src/backend/src/app-creator.js @@ -434,6 +434,7 @@ function addDynamicApp(appDef) { } function initDynamicApps() { + // 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); @@ -441,7 +442,32 @@ function initDynamicApps() { config.apps.push(app); } } - console.log(`Apps dynamiques chargées: ${dynamicApps.length}`); + // 2. Scanner les dossiers app.json (découverte automatique) + try { + 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('Erreur scan app.json:', err.message); + } + 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 d4acb43..e6540a1 100644 --- a/src/backend/src/config.js +++ b/src/backend/src/config.js @@ -137,6 +137,7 @@ module.exports = { containerName: 'demat-facturation-app', healthCheckUrl: 'https://demat-facturation.recette.santinova-soft.org', port: 3000, + image: 'images/demat-facturation-dsi.jpg', category: 'SANTINOVA', status: 'production', }, @@ -188,7 +189,7 @@ module.exports = { containerName: 'portail-santinova', healthCheckUrl: 'https://portail.recette.santinova-soft.org', port: 3000, - category: 'SANTINOVA', + category: 'INFRA', status: 'production', }, { @@ -225,22 +226,5 @@ module.exports = { category: 'SANTINOVA', status: 'recette', }, - { - id: 'falc-generator', - name: 'FALC Generator', - description: 'Générateur de documents FALC (Facile À Lire et à Comprendre)', - directory: 'falc-generator', - giteaRepo: 'falc-generator', - giteaOwner: 'manus-admin', - urls: { - recette: 'https://falc.recette.santinova-soft.org', - prod: 'https://falc.santinova-soft.org', - }, - containerName: 'falc-generator', - healthCheckUrl: 'https://falc.santinova-soft.org', - port: 3000, - category: 'ITINOVA', - status: 'production', - }, ], }; diff --git a/src/backend/src/routes.js b/src/backend/src/routes.js index d7957b9..fbdd2e4 100644 --- a/src/backend/src/routes.js +++ b/src/backend/src/routes.js @@ -417,6 +417,36 @@ router.get('/public/status', async (req, res) => { } }); +// ============ PUBLIC APPS ROUTE (sans authentification) ============ +// Utilisé par le portail applicatif pour construire les tuiles dynamiquement +// Ne bloque jamais : retourne les apps avec status 'unknown' si les health checks ne sont pas encore disponibles +router.get('/public/apps', (req, res) => { + try { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + // getAllStatuses() retourne un tableau vide si aucun check n'a encore été effectué + // On ne déclenche PAS checkAllApps() ici pour éviter de bloquer la réponse + const statuses = getAllStatuses(); + const publicApps = config.apps + .filter((a) => a.category !== 'INFRA') // Exclure les apps infra (portail, dashboard) + .map((a) => { + const status = statuses.find((s) => s.id === a.id); + return { + id: a.id, + name: a.name, + category: a.category || 'SANTINOVA', + image: a.image || ('images/' + a.id + '.png'), + urls: a.urls || {}, + status: status ? status.status : 'unknown', + containerRunning: status && status.container ? status.container.running : false, + }; + }); + res.json(publicApps); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + // ============ DOCKER ROUTES ============ router.get('/docker/containers', authMiddleware, async (req, res) => { @@ -452,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 }); @@ -505,21 +535,14 @@ const GITEA_PASS_REC = process.env.GITEA_PASSWORD || 'Itinova69!'; const GITEA_PASS_PRD = process.env.GITEA_PASSWORD_PROD || 'ManusGitea2026!'; const GITEA_TOKEN_REC = process.env.GITEA_RECETTE_TOKEN || process.env.GITEA_TOKEN || null; -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: 'pilotage-masse-salariale', name: 'Pilotage Masse Salariale', repoName: 'pilotage-masse-salariale' }, - { id: 'itinova-budget-si', name: 'Gestion Budget Informatique', repoName: 'itinova-budget-si' }, - { id: 'falc-generator', name: 'FALC Generator', repoName: 'falc-generator' }, - { id: 'portail-santinova', name: 'Portail Applicatif', repoName: 'portail-santinova' }, - { id: 'manus-dashboard', name: 'Dashboard Manus', repoName: 'manus-dashboard' }, -]; +// INVENTORY_APPS est généré dynamiquement depuis config.apps (lui-même alimenté par la découverte des app.json) +function getInventoryApps() { + return config.apps.map((a) => ({ + id: a.id, + name: a.name, + repoName: a.giteaRepo || a.id, + })); +} function curlGitea(baseUrl, owner, repo, pass, publicBaseUrl, token) { return new Promise((resolve) => { @@ -559,6 +582,7 @@ function curlGitea(baseUrl, owner, repo, pass, publicBaseUrl, token) { router.get('/inventory', authMiddleware, async (req, res) => { try { const owner = GITEA_USER_INV; + const INVENTORY_APPS = getInventoryApps(); const results = await Promise.all( INVENTORY_APPS.map(async (app) => { const [repoRecette, repoProd] = await Promise.all([ diff --git a/src/frontend/index.html b/src/frontend/index.html index 4b7f321..2cbebf5 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -4,7 +4,7 @@ -
Recette
+{window.location.hostname.includes('recette') ? 'Recette' : 'Production'}
Gestion des applications