feat: découverte dynamique des apps via app.json + endpoint /public/apps

This commit is contained in:
Manus
2026-07-24 10:25:18 +00:00
parent 6bfad4df8a
commit 85bd5209c7
3 changed files with 70 additions and 33 deletions

View File

@@ -434,6 +434,7 @@ function addDynamicApp(appDef) {
} }
function initDynamicApps() { function initDynamicApps() {
// 1. Charger les apps depuis .dashboard-apps.json (créées via l'interface)
const dynamicApps = loadDynamicApps(); const dynamicApps = loadDynamicApps();
for (const app of dynamicApps) { for (const app of dynamicApps) {
const existingInConfig = config.apps.findIndex((a) => a.id === app.id); const existingInConfig = config.apps.findIndex((a) => a.id === app.id);
@@ -441,7 +442,39 @@ function initDynamicApps() {
config.apps.push(app); config.apps.push(app);
} }
} }
console.log(`Apps dynamiques chargées: ${dynamicApps.length}`);
// 2. Découverte automatique : scanner les dossiers apps/*.json
let discovered = 0;
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);
}
}
}
} catch (err) {
console.error('[discovery] Erreur scan apps:', err.message);
}
console.log(`Apps chargées: ${dynamicApps.length} dynamiques + ${discovered} découvertes (total: ${config.apps.length})`);
} }
// ============ CREATION D'APPLICATION ============ // ============ CREATION D'APPLICATION ============

View File

@@ -225,22 +225,5 @@ module.exports = {
category: 'SANTINOVA', category: 'SANTINOVA',
status: 'recette', 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',
},
], ],
}; };

View File

@@ -417,6 +417,33 @@ router.get('/public/status', async (req, res) => {
} }
}); });
// ============ PUBLIC APPS ROUTE (sans authentification) ============
// Utilisé par le portail applicatif pour construire les tuiles dynamiquement
router.get('/public/apps', async (req, res) => {
try {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
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 ============ // ============ DOCKER ROUTES ============
router.get('/docker/containers', authMiddleware, async (req, res) => { router.get('/docker/containers', authMiddleware, async (req, res) => {
@@ -505,21 +532,14 @@ const GITEA_PASS_REC = process.env.GITEA_PASSWORD || 'Itinova69!';
const GITEA_PASS_PRD = process.env.GITEA_PASSWORD_PROD || 'ManusGitea2026!'; 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 GITEA_TOKEN_REC = process.env.GITEA_RECETTE_TOKEN || process.env.GITEA_TOKEN || null;
const INVENTORY_APPS = [ // INVENTORY_APPS est généré dynamiquement depuis config.apps (lui-même alimenté par la découverte des app.json)
{ id: 'itinova-contacts', name: 'Itinova Contacts', repoName: 'itinova-contacts' }, function getInventoryApps() {
{ id: 'itinova-podcasts', name: 'Itinova Podcasts', repoName: 'itinova-podcasts' }, return config.apps.map((a) => ({
{ id: 'veille-reglementaire', name: 'Veille Réglementaire', repoName: 'veille-reglementaire' }, id: a.id,
{ id: 'itinova-vehicle-exchange', name: 'Itinova Gestion de Flotte', repoName: 'itinova-vehicle-exchange' }, name: a.name,
{ id: 'sonum', name: 'SONUM', repoName: 'sonum' }, repoName: a.giteaRepo || a.id,
{ 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' },
];
function curlGitea(baseUrl, owner, repo, pass, publicBaseUrl, token) { function curlGitea(baseUrl, owner, repo, pass, publicBaseUrl, token) {
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -559,6 +579,7 @@ function curlGitea(baseUrl, owner, repo, pass, publicBaseUrl, token) {
router.get('/inventory', authMiddleware, async (req, res) => { router.get('/inventory', authMiddleware, async (req, res) => {
try { try {
const owner = GITEA_USER_INV; const owner = GITEA_USER_INV;
const INVENTORY_APPS = getInventoryApps();
const results = await Promise.all( const results = await Promise.all(
INVENTORY_APPS.map(async (app) => { INVENTORY_APPS.map(async (app) => {
const [repoRecette, repoProd] = await Promise.all([ const [repoRecette, repoProd] = await Promise.all([