From a6c1dab3d7ef4f5d42df9db9322f240a03f6c654 Mon Sep 17 00:00:00 2001 From: Santinova Deployment Date: Sat, 24 Oct 2026 19:38:44 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20rafra=C3=AEchir=20les=20manifests=20du?= =?UTF-8?q?=20dashboard=20apr=C3=A8s=20d=C3=A9ploiement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/src/app-registry.js | 23 ++++++++++++++++++++- src/backend/src/routes.js | 12 +++++++++++ src/backend/test/app-registry.test.js | 29 ++++++++++++++++++++++++++- 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/backend/src/app-registry.js b/src/backend/src/app-registry.js index 1c732e3..10b67f6 100644 --- a/src/backend/src/app-registry.js +++ b/src/backend/src/app-registry.js @@ -13,6 +13,7 @@ const CATEGORIES = new Set(['ITINOVA', 'SANTINOVA', 'INFRA']); const ENVIRONMENTS = new Set(['recette', 'prod']); const AI_BILLING = new Set(['free', 'paid']); const AI_SOURCES = new Set(['application', 'referential']); +const DEFAULT_MANIFEST_REFRESH_INTERVAL_MS = 15_000; function isHttpsUrl(value) { if (typeof value !== 'string' || value.length === 0) return false; @@ -171,13 +172,14 @@ function discoverApps({ appsBasePath, environment }) { * Toute valeur obsolète est remplacée : seules les applications réellement * déployées et correctement déclarées restent disponibles. */ -function refreshApps(config, logger = console) { +function refreshApps(config, logger = console, now = Date.now()) { const result = discoverApps({ appsBasePath: config.appsBasePath, environment: config.environment, }); config.apps.splice(0, config.apps.length, ...result.apps); + config.appsRefreshedAt = now; for (const issue of result.errors) { logger.warn(`[app-registry] Manifeste ignoré (${issue.directory}) : ${issue.error}`); } @@ -186,6 +188,24 @@ function refreshApps(config, logger = console) { return result; } +/** + * Rafraîchit le registre après expiration, sans parcourir les applications à + * chaque requête. Les déploiements deviennent ainsi visibles sans redémarrage. + */ +function refreshAppsIfStale(config, { + logger = console, + now = Date.now(), + maxAgeMs = config.appManifestRefreshIntervalMs || DEFAULT_MANIFEST_REFRESH_INTERVAL_MS, +} = {}) { + const hasFreshCache = Array.isArray(config.apps) && config.apps.length > 0 && + Number.isFinite(config.appsRefreshedAt) && + now - config.appsRefreshedAt < maxAgeMs; + if (hasFreshCache) { + return { apps: config.apps, errors: [], refreshed: false }; + } + return { ...refreshApps(config, logger, now), refreshed: true }; +} + /** * Écrit un manifeste de manière atomique puis le valide avant publication. */ @@ -216,6 +236,7 @@ function findAppByRepo(config, repositoryName) { module.exports = { discoverApps, refreshApps, + refreshAppsIfStale, writeAppManifest, findAppDirectoryByRepo, findAppByRepo, diff --git a/src/backend/src/routes.js b/src/backend/src/routes.js index a593b64..f63796b 100644 --- a/src/backend/src/routes.js +++ b/src/backend/src/routes.js @@ -10,12 +10,24 @@ const { checkAllApps, addDeploymentLog, updateDeploymentLog, getDeploymentLogs, const { createApplication, getAvailableStacks, initDynamicApps } = require('./app-creator'); const { readCiAlerts } = require('./ci-alerts'); const config = require('./config'); +const { refreshAppsIfStale } = require('./app-registry'); // Nettoyer les déploiements orphelins au démarrage (entrées "running" restantes d'un crash précédent) cleanupOrphanedDeployments(); const router = express.Router(); +// Les manifests sont la source de vérité ; leur cache est rafraîchi après un +// court délai pour refléter les déploiements sans alourdir chaque requête API. +router.use((req, res, next) => { + try { + refreshAppsIfStale(config); + next(); + } catch (error) { + next(error); + } +}); + // Configuration multer pour l'upload de fichiers ZIP const uploadDir = '/tmp/dashboard-uploads'; if (!fs.existsSync(uploadDir)) { diff --git a/src/backend/test/app-registry.test.js b/src/backend/test/app-registry.test.js index 40cd027..ca3c183 100644 --- a/src/backend/test/app-registry.test.js +++ b/src/backend/test/app-registry.test.js @@ -7,7 +7,7 @@ const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); -const { discoverApps, refreshApps } = require('../src/app-registry'); +const { discoverApps, refreshApps, refreshAppsIfStale } = require('../src/app-registry'); function writeManifest(basePath, directory, manifest) { const appPath = path.join(basePath, directory); @@ -113,6 +113,33 @@ try { 'app.json doit prévaloir sur toute définition statique' ); + // Un déploiement met à jour app.json : le dashboard doit le détecter sans + // redémarrage, mais sans relire les manifests à chaque requête. + refreshApps(config, console, 1_000); + writeManifest(sandbox, 'contacts', { + id: 'itinova-contacts', + name: 'Itinova Contacts mis à jour', + category: 'ITINOVA', + urls: { + recette: 'https://contacts-itinova.recette.santinova-soft.org', + prod: 'https://contacts-itinova.santinova-soft.org', + }, + }); + const cachedRefresh = refreshAppsIfStale(config, { now: 1_999, maxAgeMs: 1_000 }); + assert.equal(cachedRefresh.refreshed, false, 'le cache reste utilisable avant son expiration'); + assert.equal( + config.apps.find((candidate) => candidate.id === 'itinova-contacts').name, + 'Itinova Contacts', + 'le cache ne doit pas relire les manifests avant son expiration' + ); + const expiredRefresh = refreshAppsIfStale(config, { now: 2_000, maxAgeMs: 1_000 }); + assert.equal(expiredRefresh.refreshed, true, 'le manifeste est relu lorsque le cache expire'); + assert.equal( + config.apps.find((candidate) => candidate.id === 'itinova-contacts').name, + 'Itinova Contacts mis à jour', + 'la modification de manifeste doit apparaître sans redémarrage du dashboard' + ); + console.log('OK app-registry'); } finally { fs.rmSync(sandbox, { recursive: true, force: true });