fix: rafraîchir les manifests du dashboard après déploiement
All checks were successful
Validation Dashboard / Tests backend et build frontend (push) Successful in 40s

This commit is contained in:
Santinova Deployment
2026-10-24 19:38:44 +00:00
parent 9fd5dbb0df
commit a6c1dab3d7
3 changed files with 62 additions and 2 deletions

View File

@@ -13,6 +13,7 @@ const CATEGORIES = new Set(['ITINOVA', 'SANTINOVA', 'INFRA']);
const ENVIRONMENTS = new Set(['recette', 'prod']); const ENVIRONMENTS = new Set(['recette', 'prod']);
const AI_BILLING = new Set(['free', 'paid']); const AI_BILLING = new Set(['free', 'paid']);
const AI_SOURCES = new Set(['application', 'referential']); const AI_SOURCES = new Set(['application', 'referential']);
const DEFAULT_MANIFEST_REFRESH_INTERVAL_MS = 15_000;
function isHttpsUrl(value) { function isHttpsUrl(value) {
if (typeof value !== 'string' || value.length === 0) return false; 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 * Toute valeur obsolète est remplacée : seules les applications réellement
* déployées et correctement déclarées restent disponibles. * 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({ const result = discoverApps({
appsBasePath: config.appsBasePath, appsBasePath: config.appsBasePath,
environment: config.environment, environment: config.environment,
}); });
config.apps.splice(0, config.apps.length, ...result.apps); config.apps.splice(0, config.apps.length, ...result.apps);
config.appsRefreshedAt = now;
for (const issue of result.errors) { for (const issue of result.errors) {
logger.warn(`[app-registry] Manifeste ignoré (${issue.directory}) : ${issue.error}`); logger.warn(`[app-registry] Manifeste ignoré (${issue.directory}) : ${issue.error}`);
} }
@@ -186,6 +188,24 @@ function refreshApps(config, logger = console) {
return result; 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. * Écrit un manifeste de manière atomique puis le valide avant publication.
*/ */
@@ -216,6 +236,7 @@ function findAppByRepo(config, repositoryName) {
module.exports = { module.exports = {
discoverApps, discoverApps,
refreshApps, refreshApps,
refreshAppsIfStale,
writeAppManifest, writeAppManifest,
findAppDirectoryByRepo, findAppDirectoryByRepo,
findAppByRepo, findAppByRepo,

View File

@@ -10,12 +10,24 @@ const { checkAllApps, addDeploymentLog, updateDeploymentLog, getDeploymentLogs,
const { createApplication, getAvailableStacks, initDynamicApps } = require('./app-creator'); const { createApplication, getAvailableStacks, initDynamicApps } = require('./app-creator');
const { readCiAlerts } = require('./ci-alerts'); const { readCiAlerts } = require('./ci-alerts');
const config = require('./config'); 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) // Nettoyer les déploiements orphelins au démarrage (entrées "running" restantes d'un crash précédent)
cleanupOrphanedDeployments(); cleanupOrphanedDeployments();
const router = express.Router(); 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 // Configuration multer pour l'upload de fichiers ZIP
const uploadDir = '/tmp/dashboard-uploads'; const uploadDir = '/tmp/dashboard-uploads';
if (!fs.existsSync(uploadDir)) { if (!fs.existsSync(uploadDir)) {

View File

@@ -7,7 +7,7 @@ const fs = require('node:fs');
const os = require('node:os'); const os = require('node:os');
const path = require('node:path'); 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) { function writeManifest(basePath, directory, manifest) {
const appPath = path.join(basePath, directory); const appPath = path.join(basePath, directory);
@@ -113,6 +113,33 @@ try {
'app.json doit prévaloir sur toute définition statique' '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'); console.log('OK app-registry');
} finally { } finally {
fs.rmSync(sandbox, { recursive: true, force: true }); fs.rmSync(sandbox, { recursive: true, force: true });