fix: rétablir le dashboard production et ses moteurs IA
All checks were successful
Validation Dashboard / Tests backend et build frontend (push) Successful in 1m7s
All checks were successful
Validation Dashboard / Tests backend et build frontend (push) Successful in 1m7s
This commit is contained in:
@@ -19,6 +19,15 @@ Le dashboard et le portail applicatif utilisent exclusivement le manifeste plac
|
||||
"giteaRepo": "itinova-contacts",
|
||||
"giteaOwner": "manus-admin",
|
||||
"image": "images/itinova-contacts.png",
|
||||
"ai": {
|
||||
"engines": [
|
||||
{
|
||||
"provider": "Google Gemini",
|
||||
"model": "gemini-2.5-flash",
|
||||
"role": "Assistant conversationnel"
|
||||
}
|
||||
]
|
||||
},
|
||||
"urls": {
|
||||
"recette": "https://contacts.recette.santinova-soft.org",
|
||||
"prod": "https://contacts.santinova-soft.org"
|
||||
@@ -35,11 +44,12 @@ Le dashboard et le portail applicatif utilisent exclusivement le manifeste plac
|
||||
| `giteaRepo` | Dépôt source | Sert au webhook CI/CD et à l’inventaire. |
|
||||
| `giteaOwner` | Organisation/propriétaire Gitea | Facultatif si la valeur par défaut suffit. |
|
||||
| `image` | Image locale du portail | Chemin relatif à `portail-santinova/images/`. |
|
||||
| `ai.engines` | Moteurs IA déclarés | Tableau optionnel de métadonnées publiques : `provider`, `model` et `role` optionnel. Ne jamais y mettre une clé, un jeton ni un endpoint privé. |
|
||||
| `urls.recette` | URL de recette | Obligatoire et HTTPS pour un dashboard recette. |
|
||||
| `urls.prod` | URL de production | Obligatoire et HTTPS pour un dashboard de production. |
|
||||
|
||||
## Règles de déploiement
|
||||
|
||||
Chaque premier déploiement doit créer ou mettre à jour le manifeste dans le même commit que le `docker-compose.yml` et les éléments nécessaires à l’application. Lorsqu’une application est promue, son manifeste de production doit contenir l’URL de production avant le démarrage du dashboard.
|
||||
Chaque premier déploiement doit créer ou mettre à jour le manifeste dans le même commit que le `docker-compose.yml` et les éléments nécessaires à l’application. Lorsqu’une application est promue, son manifeste de production doit contenir l’URL de production avant le démarrage du dashboard. Lorsque l’application utilise un moteur IA, le même commit renseigne `ai.engines` avec des informations descriptives vérifiées, jamais avec des secrets.
|
||||
|
||||
Le dashboard ne maintient plus de liste statique d’applications, et le portail ne maintient plus de tuiles codées en dur. Cette règle élimine les écarts de version, d’URL, de catégorie et de statut entre les deux interfaces.
|
||||
|
||||
2
app.json
2
app.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "manus-dashboard",
|
||||
"name": "Dashboard",
|
||||
"name": "Dashboard Production",
|
||||
"category": "INFRA",
|
||||
"urls": {
|
||||
"recette": "https://dashboard.recette.santinova-soft.org",
|
||||
|
||||
@@ -1,58 +1,47 @@
|
||||
services:
|
||||
dashboard:
|
||||
build:
|
||||
# Les sources actives sont versionnées sous src/. Le contexte racine
|
||||
# pouvait réutiliser d’anciens dossiers backend/frontend restés sur l’hôte.
|
||||
context: ./src
|
||||
dockerfile: Dockerfile
|
||||
container_name: manus-dashboard
|
||||
# Le dashboard doit repartir seul après un arrêt anormal du processus.
|
||||
# Redémarrage automatique après un arrêt anormal ; le processus reste borné.
|
||||
restart: always
|
||||
mem_limit: 512m
|
||||
privileged: true
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-production}
|
||||
- DASHBOARD_ENV=${DASHBOARD_ENV:-recette}
|
||||
- DASHBOARD_ENV=prod
|
||||
- PORT=${PORT:-3001}
|
||||
- JWT_SECRET=${JWT_SECRET:?JWT_SECRET requis dans le fichier .env serveur}
|
||||
- ADMIN_USERNAME=${ADMIN_USERNAME:?ADMIN_USERNAME requis dans le fichier .env serveur}
|
||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD:?ADMIN_PASSWORD requis dans le fichier .env serveur}
|
||||
- GITEA_URL=${GITEA_URL:?GITEA_URL requis dans le fichier .env serveur}
|
||||
- GITEA_USERNAME=${GITEA_USERNAME:-manus-admin}
|
||||
- GITEA_TOKEN=${GITEA_TOKEN:?GITEA_TOKEN requis dans le fichier .env serveur}
|
||||
- GITEA_RECETTE_URL=${GITEA_RECETTE_URL:-http://gitea:3000}
|
||||
- GITEA_RECETTE_PUBLIC_URL=${GITEA_RECETTE_PUBLIC_URL:-https://git.recette.santinova-soft.org}
|
||||
- GITEA_RECETTE_TOKEN=${GITEA_RECETTE_TOKEN:-}
|
||||
- GITEA_PROD_URL=${GITEA_PROD_URL:-https://git.santinova-soft.org}
|
||||
- GITEA_TOKEN_PROD=${GITEA_TOKEN_PROD:-}
|
||||
- WEBHOOK_SECRET=${WEBHOOK_SECRET:?WEBHOOK_SECRET requis dans le fichier .env serveur}
|
||||
- IA_REFERENTIEL_URL=${IA_REFERENTIEL_URL:-https://referentiel-ia.recette.santinova-soft.org}
|
||||
- IA_REFERENTIEL_TOKEN=${IA_REFERENTIEL_TOKEN:-}
|
||||
- DSI_ALERT_EMAIL=${DSI_ALERT_EMAIL:-}
|
||||
- CI_ALERT_EMAIL_QUEUE=/opt/manus-deploy/secrets/ci-alerts/outbox
|
||||
- APPS_BASE_PATH=/opt/manus-deploy/apps
|
||||
- INFRA_BASE_PATH=/opt/manus-deploy/infrastructure
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- ADMIN_USERNAME=${ADMIN_USERNAME}
|
||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
||||
- GITEA_URL=${GITEA_URL}
|
||||
- GITEA_USERNAME=${GITEA_USERNAME}
|
||||
- GITEA_PASSWORD=${GITEA_PASSWORD}
|
||||
- GITEA_TOKEN=${GITEA_TOKEN:?GITEA_TOKEN requis}
|
||||
- WEBHOOK_SECRET=${WEBHOOK_SECRET:?WEBHOOK_SECRET requis}
|
||||
- GITEA_RECETTE_URL=${GITEA_RECETTE_URL}
|
||||
- GITEA_RECETTE_TOKEN=${GITEA_RECETTE_TOKEN}
|
||||
- GITEA_PASSWORD_PROD=${GITEA_PASSWORD}
|
||||
- APPS_BASE_PATH=${APPS_BASE_PATH:-/opt/manus-deploy/apps}
|
||||
- INFRA_BASE_PATH=${INFRA_BASE_PATH:-/opt/manus-deploy/infrastructure}
|
||||
# Contrôles bornés et dédoublonnés par healthcheck.js.
|
||||
- HEALTH_CHECK_INTERVAL=60000
|
||||
- HEALTH_CHECK_INTERVAL=${HEALTH_CHECK_INTERVAL:-60000}
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /opt/manus-deploy:/opt/manus-deploy
|
||||
extra_hosts:
|
||||
- "git.santinova-soft.org:180.149.196.138"
|
||||
- "git.recette.santinova-soft.org:78.138.58.109"
|
||||
networks:
|
||||
- web
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# Le domaine public est spécifique au serveur, mais le commit reste commun.
|
||||
- "traefik.http.routers.manus-dashboard.rule=Host(`${DASHBOARD_DOMAIN:?DASHBOARD_DOMAIN requis dans le fichier .env serveur}`)"
|
||||
- "traefik.http.routers.manus-dashboard.rule=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"
|
||||
- "traefik.http.routers.manus-dashboard.service=manus-dashboard-svc"
|
||||
- "traefik.http.routers.manus-dashboard.priority=100"
|
||||
# Service
|
||||
- "traefik.http.services.manus-dashboard-svc.loadbalancer.server.port=3001"
|
||||
- "traefik.docker.network=web"
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "nodemon src/index.js",
|
||||
"test": "node test/app-registry.test.js && node test/healthcheck.test.js && node test/webhook.test.js && node test/gitea.test.js && node test/gitea-auth.test.js && node test/token-rotation.test.js && node test/ci-alerts.test.js && node test/ci-email-alert.test.js && node test/webhook-setup.test.js && node test/dashboard-alert-token.test.js"
|
||||
"test": "node test/app-registry.test.js && node test/healthcheck.test.js && node test/healthcheck-category.test.js && node test/applications-ui-contract.test.js && node test/webhook.test.js && node test/gitea.test.js && node test/gitea-auth.test.js && node test/token-rotation.test.js && node test/ci-alerts.test.js && node test/ci-email-alert.test.js && node test/webhook-setup.test.js && node test/dashboard-alert-token.test.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.0",
|
||||
|
||||
70
src/backend/src/ai-catalog.js
Normal file
70
src/backend/src/ai-catalog.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Moteurs IA vérifiés pour les applications historiques dont le manifeste ne
|
||||
* contient pas encore ai.engines. Un champ ai.engines dans app.json reste
|
||||
* prioritaire et permet à chaque application de remplacer cette valeur.
|
||||
*
|
||||
* Ce catalogue contient uniquement des informations descriptives publiques :
|
||||
* aucun secret, endpoint privé ou paramètre d'exécution n'y figure.
|
||||
*/
|
||||
const AI_CATALOG = Object.freeze({
|
||||
'demat-facturation-dsi': [
|
||||
{ provider: 'Mistral AI', model: 'mistral-large-latest', role: 'Extraction de factures' },
|
||||
{ provider: 'Google Gemini', model: 'gemini-2.0-flash', role: 'Relève de traitement' },
|
||||
],
|
||||
'facturation-santinova': [
|
||||
{ provider: 'OpenAI', model: 'gpt-4.1-mini', role: 'Traitement documentaire' },
|
||||
],
|
||||
'falc-generator': [
|
||||
{ provider: 'Manus Forge', model: 'gemini-2.5-flash', role: 'Transformation FALC' },
|
||||
{ provider: 'Google Gemini', model: 'gemini-3.1-pro-preview', role: 'Récits de vie' },
|
||||
],
|
||||
'formation-manager-itinova': [
|
||||
{ provider: 'Manus Forge', model: 'gemini-2.5-flash', role: 'Assistant conversationnel' },
|
||||
],
|
||||
'gestion-at-v2': [
|
||||
{ provider: 'Manus Forge', model: 'gemini-3-flash-preview', role: 'Analyse CERFA' },
|
||||
{ provider: 'OpenAI', model: 'gpt-image-2', role: 'Génération d’images' },
|
||||
],
|
||||
'gestion-llm-locaux': [
|
||||
{ provider: 'Ollama local', model: 'llama3.2:1b', role: 'Moteur privé approuvé' },
|
||||
],
|
||||
'itinova-budget-si': [
|
||||
{ provider: 'Manus Forge', model: 'gemini-2.5-flash', role: 'Assistant conversationnel' },
|
||||
],
|
||||
'itinova-contacts': [
|
||||
{ provider: 'Manus Forge', model: 'gemini-2.5-flash', role: 'Assistant conversationnel' },
|
||||
{ provider: 'Anthropic Claude', model: 'claude-sonnet-4-6', role: 'Connexion configurée' },
|
||||
],
|
||||
'itinova-podcasts': [
|
||||
{ provider: 'Manus Forge', model: 'gemini-2.5-flash', role: 'Assistant conversationnel' },
|
||||
],
|
||||
'itinova-vehicle-exchange': [
|
||||
{ provider: 'Manus Forge', model: 'gemini-2.5-flash', role: 'Assistant conversationnel' },
|
||||
],
|
||||
'pilotage-masse-salariale': [
|
||||
{ provider: 'Manus Forge', model: 'gemini-2.5-flash', role: 'Assistant conversationnel' },
|
||||
],
|
||||
portee: [
|
||||
{ provider: 'OpenAI', model: 'gpt-image-2', role: 'Génération d’images' },
|
||||
{ provider: 'Manus Forge', model: 'gemini-2.5-flash', role: 'Assistant conversationnel' },
|
||||
],
|
||||
'referentiel-ia': [
|
||||
{ provider: 'Google Gemini', model: 'gemini-2.5-flash', role: 'Moteur référencé' },
|
||||
{ provider: 'OpenAI', model: 'gpt-5.4-mini', role: 'Moteur référencé' },
|
||||
{ provider: 'Anthropic Claude', model: 'claude-sonnet-4-6', role: 'Moteur référencé' },
|
||||
{ provider: 'Mistral AI', model: 'mistral-large', role: 'Moteur référencé' },
|
||||
{ provider: 'Ollama local', model: 'llama3.2:1b', role: 'Moteur référencé' },
|
||||
],
|
||||
sonum: [
|
||||
{ provider: 'Manus Forge', model: 'gemini-2.5-flash', role: 'Assistant conversationnel' },
|
||||
],
|
||||
'veille-reglementaire': [
|
||||
{ provider: 'Manus Forge', model: 'gemini-2.5-flash', role: 'Classification réglementaire' },
|
||||
],
|
||||
});
|
||||
|
||||
function getAiCatalogEntry(appId) {
|
||||
return AI_CATALOG[appId] || [];
|
||||
}
|
||||
|
||||
module.exports = { AI_CATALOG, getAiCatalogEntry };
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getAiCatalogEntry } = require('./ai-catalog');
|
||||
|
||||
const CATEGORIES = new Set(['ITINOVA', 'SANTINOVA', 'INFRA']);
|
||||
const ENVIRONMENTS = new Set(['recette', 'prod']);
|
||||
@@ -20,6 +21,40 @@ function isHttpsUrl(value) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise les moteurs IA déclarés dans app.json. Seules des métadonnées
|
||||
* descriptives (fournisseur, modèle, rôle) sont acceptées : aucune clé ni URL
|
||||
* technique ne doit être publiée par le dashboard.
|
||||
*/
|
||||
function normalizeAi(ai, appId) {
|
||||
if (ai === undefined) return { engines: getAiCatalogEntry(appId) };
|
||||
if (!ai || typeof ai !== 'object' || Array.isArray(ai) || !Array.isArray(ai.engines)) {
|
||||
throw new Error('ai doit être un objet avec un tableau engines');
|
||||
}
|
||||
|
||||
return {
|
||||
engines: ai.engines.map((engine, index) => {
|
||||
if (!engine || typeof engine !== 'object' || Array.isArray(engine)) {
|
||||
throw new Error(`ai.engines[${index}] doit être un objet`);
|
||||
}
|
||||
if (typeof engine.provider !== 'string' || engine.provider.trim().length === 0) {
|
||||
throw new Error(`ai.engines[${index}].provider absent ou invalide`);
|
||||
}
|
||||
if (typeof engine.model !== 'string' || engine.model.trim().length === 0) {
|
||||
throw new Error(`ai.engines[${index}].model absent ou invalide`);
|
||||
}
|
||||
if (engine.role !== undefined && (typeof engine.role !== 'string' || engine.role.trim().length === 0)) {
|
||||
throw new Error(`ai.engines[${index}].role invalide`);
|
||||
}
|
||||
return {
|
||||
provider: engine.provider.trim(),
|
||||
model: engine.model.trim(),
|
||||
...(engine.role ? { role: engine.role.trim() } : {}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide puis normalise un manifeste app.json.
|
||||
* Les champs dérivés (directory et healthCheckUrl) ne sont pas écrits dans le
|
||||
@@ -54,10 +89,12 @@ function normalizeManifest(manifest, directory, environment) {
|
||||
const displayName = manifest.id === 'manus-dashboard'
|
||||
? `Dashboard ${environment === 'prod' ? 'Production' : 'Recette'}`
|
||||
: manifest.name.trim();
|
||||
const ai = normalizeAi(manifest.ai, manifest.id);
|
||||
|
||||
return {
|
||||
...manifest,
|
||||
name: displayName,
|
||||
ai,
|
||||
directory,
|
||||
containerName: manifest.containerName || manifest.id,
|
||||
healthCheckUrl: manifest.urls[environment],
|
||||
|
||||
@@ -27,7 +27,7 @@ module.exports = {
|
||||
username: process.env.GITEA_USERNAME || 'manus-admin',
|
||||
token: process.env.GITEA_TOKEN || null,
|
||||
productionUrl: process.env.GITEA_PROD_URL || 'https://git.santinova-soft.org',
|
||||
productionToken: process.env.GITEA_TOKEN_PROD || null,
|
||||
productionToken: process.env.GITEA_TOKEN_PROD || process.env.GITEA_TOKEN || null,
|
||||
},
|
||||
|
||||
dsiAlerts: {
|
||||
|
||||
@@ -63,8 +63,9 @@ async function checkApp(appConfig) {
|
||||
id: appConfig.id,
|
||||
name: appConfig.name,
|
||||
description: appConfig.description || '',
|
||||
// Préserve la catégorie du manifeste pour le rendu différencié des services INFRA.
|
||||
// Préserve la catégorie et les métadonnées IA du manifeste pour l’interface.
|
||||
category: appConfig.category || 'SANTINOVA',
|
||||
ai: appConfig.ai || { engines: [] },
|
||||
urls: appConfig.urls,
|
||||
containerName: appConfig.containerName,
|
||||
lastCheck: new Date().toISOString(),
|
||||
|
||||
@@ -24,6 +24,9 @@ try {
|
||||
category: 'ITINOVA',
|
||||
containerName: 'itinova-contacts',
|
||||
image: 'images/itinova-contacts.png',
|
||||
ai: {
|
||||
engines: [{ provider: 'Google Gemini', model: 'gemini-2.5-flash', role: 'Assistant conversationnel' }],
|
||||
},
|
||||
urls: {
|
||||
recette: 'https://contacts.recette.santinova-soft.org',
|
||||
prod: 'https://contacts.santinova-soft.org',
|
||||
@@ -40,21 +43,46 @@ try {
|
||||
},
|
||||
});
|
||||
|
||||
writeManifest(sandbox, 'falc', {
|
||||
id: 'falc-generator',
|
||||
name: 'FALC Generator',
|
||||
category: 'ITINOVA',
|
||||
urls: { recette: 'https://falc.recette.santinova-soft.org' },
|
||||
});
|
||||
|
||||
writeManifest(sandbox, 'invalide', {
|
||||
id: 'app-invalide',
|
||||
name: 'Application invalide',
|
||||
category: 'INCONNUE',
|
||||
urls: { recette: 'https://invalide.recette.santinova-soft.org' },
|
||||
});
|
||||
writeManifest(sandbox, 'ia-invalide', {
|
||||
id: 'app-ia-invalide',
|
||||
name: 'Application IA invalide',
|
||||
category: 'ITINOVA',
|
||||
ai: { engines: [{ provider: 'Google Gemini' }] },
|
||||
urls: { recette: 'https://ia-invalide.recette.santinova-soft.org' },
|
||||
});
|
||||
|
||||
const discovery = discoverApps({ appsBasePath: sandbox, environment: 'recette' });
|
||||
assert.equal(discovery.apps.length, 2, 'un manifeste invalide doit être exclu');
|
||||
assert.equal(discovery.errors.length, 1, 'une erreur explicite doit être retournée');
|
||||
assert.equal(discovery.apps.length, 3, 'un manifeste invalide doit être exclu');
|
||||
assert.equal(discovery.errors.length, 2, 'une erreur explicite doit être retournée pour chaque manifeste invalide');
|
||||
|
||||
const app = discovery.apps.find((candidate) => candidate.id === 'itinova-contacts');
|
||||
assert.equal(app.directory, 'contacts', 'le dossier doit être déduit du chemin du manifeste');
|
||||
assert.equal(app.healthCheckUrl, app.urls.recette, 'la vérification doit cibler l’environnement courant');
|
||||
assert.equal(app.containerName, 'itinova-contacts');
|
||||
assert.deepEqual(app.ai, {
|
||||
engines: [{ provider: 'Google Gemini', model: 'gemini-2.5-flash', role: 'Assistant conversationnel' }],
|
||||
}, 'la métadonnée IA descriptive doit être conservée');
|
||||
|
||||
const falc = discovery.apps.find((candidate) => candidate.id === 'falc-generator');
|
||||
assert.deepEqual(falc.ai, {
|
||||
engines: [
|
||||
{ provider: 'Manus Forge', model: 'gemini-2.5-flash', role: 'Transformation FALC' },
|
||||
{ provider: 'Google Gemini', model: 'gemini-3.1-pro-preview', role: 'Récits de vie' },
|
||||
],
|
||||
}, 'le catalogue IA doit compléter un manifeste historique sans moteur déclaré');
|
||||
|
||||
const dashboardRecette = discovery.apps.find((candidate) => candidate.id === 'manus-dashboard');
|
||||
assert.equal(dashboardRecette.name, 'Dashboard Recette');
|
||||
@@ -68,7 +96,7 @@ try {
|
||||
apps: [{ id: 'itinova-contacts', name: 'ancienne valeur statique' }],
|
||||
};
|
||||
const refresh = refreshApps(config);
|
||||
assert.equal(refresh.apps.length, 2, 'les applications non déployées ne doivent pas rester en cache');
|
||||
assert.equal(refresh.apps.length, 3, 'les applications non déployées ne doivent pas rester en cache');
|
||||
assert.equal(
|
||||
config.apps.find((candidate) => candidate.id === 'itinova-contacts').name,
|
||||
'Itinova Contacts',
|
||||
|
||||
21
src/backend/test/applications-ui-contract.test.js
Normal file
21
src/backend/test/applications-ui-contract.test.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const appCard = fs.readFileSync(
|
||||
path.resolve(__dirname, '../../frontend/src/components/AppCard.jsx'),
|
||||
'utf8'
|
||||
);
|
||||
const appSorting = fs.readFileSync(
|
||||
path.resolve(__dirname, '../../frontend/src/utils/appSorting.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
assert.match(appCard, /const isInfra = app\.category === 'INFRA';/);
|
||||
assert.doesNotMatch(appCard, /\['portail-santinova', 'manus-dashboard'\]\.includes\(app\.id\)/);
|
||||
assert.match(appCard, /const aiEngines = Array\.isArray\(app\.ai\?\.engines\) \? app\.ai\.engines : \[\];/);
|
||||
assert.match(appCard, />\s*Moteur IA\s*</);
|
||||
assert.match(appCard, /Non déclaré/);
|
||||
assert.match(appSorting, /app\?\.category === 'INFRA' \? 0 : 1/);
|
||||
|
||||
console.log('applications-ui-contract: OK');
|
||||
@@ -26,11 +26,13 @@ const { checkApp } = require('../src/healthcheck');
|
||||
id: 'referentiel-ia',
|
||||
name: 'Référentiel IA Santinova',
|
||||
category: 'INFRA',
|
||||
ai: { engines: [{ provider: 'Ollama', model: 'llama3.2:1b' }] },
|
||||
urls: {},
|
||||
containerName: 'referentiel-ia',
|
||||
});
|
||||
|
||||
assert.equal(result.category, 'INFRA');
|
||||
assert.deepEqual(result.ai, { engines: [{ provider: 'Ollama', model: 'llama3.2:1b' }] });
|
||||
console.log('healthcheck-category: OK');
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dashboard - Gestion des Applications</title>
|
||||
<title>Dashboard Production - Gestion des Applications</title>
|
||||
</head>
|
||||
<body class="bg-dark-900 text-white">
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -118,8 +118,9 @@ export default function AppCard({ app, commits, onRefresh }) {
|
||||
|
||||
const latestCommit = commits && commits.length > 0 ? commits[0] : null;
|
||||
|
||||
// Apps d'infrastructure : portail et dashboard
|
||||
const isInfra = ['portail-santinova', 'manus-dashboard'].includes(app.id);
|
||||
// Le manifeste est la seule source de vérité pour le type et les moteurs IA.
|
||||
const isInfra = app.category === 'INFRA';
|
||||
const aiEngines = Array.isArray(app.ai?.engines) ? app.ai.engines : [];
|
||||
|
||||
return (
|
||||
<div className={`card overflow-hidden${isInfra ? ' border border-amber-500/30 bg-amber-950/10' : ''}`}>
|
||||
@@ -217,7 +218,26 @@ export default function AppCard({ app, commits, onRefresh }) {
|
||||
)}
|
||||
|
||||
{/* Info Grid */}
|
||||
<div className="p-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="p-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
{/* Moteurs IA déclarés dans app.json */}
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Moteur IA
|
||||
</span>
|
||||
<div className="mt-1.5 space-y-2">
|
||||
{aiEngines.length > 0 ? aiEngines.map((engine, index) => (
|
||||
<div key={`${engine.provider}-${engine.model}-${index}`}>
|
||||
<p className="text-sm text-gray-200">{engine.provider}</p>
|
||||
<p className="text-xs text-primary-400 break-words">
|
||||
{engine.model}{engine.role ? ` — ${engine.role}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
)) : (
|
||||
<p className="text-sm text-gray-600">Non déclaré</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* URLs */}
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
|
||||
@@ -12,7 +12,6 @@ import { LayoutList, TerminalSquare, ShieldCheck,
|
||||
Activity,
|
||||
BookOpen,
|
||||
} from 'lucide-react';
|
||||
import { ENVIRONMENT_LABEL } from '../utils/environment';
|
||||
const navItems = [
|
||||
{ id: 'dashboard', label: 'Tableau de bord', icon: LayoutDashboard },
|
||||
{ id: 'apps', label: 'Applications', icon: Box },
|
||||
@@ -41,7 +40,7 @@ export default function Sidebar({
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-white">Dashboard</h1>
|
||||
<p className="text-xs text-gray-500">{ENVIRONMENT_LABEL}</p>
|
||||
<p className="text-xs text-gray-500">Production</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import { getApps, getSystemInfo } from '../utils/api';
|
||||
import StatusBadge from '../components/StatusBadge';
|
||||
import { sortAppsWithInfraFirst } from '../utils/appSorting';
|
||||
import { ENVIRONMENT_LABEL, GITEA_HOST, GITEA_URL, SERVER_PUBLIC_IP } from '../utils/environment';
|
||||
|
||||
function StatCard({ icon: Icon, label, value, color, subtext }) {
|
||||
return (
|
||||
@@ -102,7 +101,7 @@ export default function DashboardPage({ apps }) {
|
||||
<div className="flex items-center justify-between py-2 border-b border-dark-700">
|
||||
<span className="text-sm text-gray-400">Serveur</span>
|
||||
<span className="text-sm text-gray-200">
|
||||
{SERVER_PUBLIC_IP} ({ENVIRONMENT_LABEL})
|
||||
180.149.196.138 (Production)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2 border-b border-dark-700">
|
||||
@@ -115,12 +114,12 @@ export default function DashboardPage({ apps }) {
|
||||
<div className="flex items-center justify-between py-2 border-b border-dark-700">
|
||||
<span className="text-sm text-gray-400">Gitea</span>
|
||||
<a
|
||||
href={GITEA_URL}
|
||||
href="https://git.santinova-soft.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-primary-400 hover:text-primary-300"
|
||||
>
|
||||
{GITEA_HOST}
|
||||
git.santinova-soft.org
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2 border-b border-dark-700">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, RefreshCw, GitBranch, CheckCircle, XCircle, ExternalLink, Tag, AlertCircle, ArrowUp, ArrowDown, Minus, AlertTriangle } from 'lucide-react';
|
||||
import api from '../utils/api';
|
||||
import { ENVIRONMENT_LABEL } from '../utils/environment';
|
||||
|
||||
const APPS_CONFIG = [
|
||||
{
|
||||
@@ -90,7 +89,7 @@ const APPS_CONFIG = [
|
||||
},
|
||||
{
|
||||
id: 'manus-dashboard',
|
||||
name: `Dashboard ${ENVIRONMENT_LABEL}`,
|
||||
name: 'Dashboard Production',
|
||||
repoName: 'manus-dashboard',
|
||||
urlRecette: 'https://dashboard.recette.santinova-soft.org',
|
||||
urlProd: 'https://dashboard.santinova-soft.org',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Lock, User, AlertCircle, Server } from 'lucide-react';
|
||||
import { login } from '../utils/api';
|
||||
import { ENVIRONMENT_LABEL, ENVIRONMENT_LABEL_LOWER } from '../utils/environment';
|
||||
|
||||
export default function LoginPage({ onLogin }) {
|
||||
const [username, setUsername] = useState('');
|
||||
@@ -35,7 +34,7 @@ export default function LoginPage({ onLogin }) {
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-600 mb-4">
|
||||
<Server className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white">Dashboard {ENVIRONMENT_LABEL}</h1>
|
||||
<h1 className="text-2xl font-bold text-white">Dashboard Production</h1>
|
||||
<p className="text-gray-400 mt-2">Gestion des applications</p>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +130,7 @@ export default function LoginPage({ onLogin }) {
|
||||
</div>
|
||||
|
||||
<p className="text-center text-gray-600 text-sm mt-6">
|
||||
Santinova Soft — Serveur de {ENVIRONMENT_LABEL_LOWER}
|
||||
Santinova Soft — Serveur de production
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Terminal, Wifi, WifiOff, X, Plus, ChevronDown } from 'lucide-react';
|
||||
import { DEFAULT_SSH_PRESET, ENVIRONMENT_LABEL, SERVER_PUBLIC_IP } from '../utils/environment';
|
||||
|
||||
// Serveurs SSH prédéfinis
|
||||
const PRESET_SERVERS = [
|
||||
@@ -31,8 +30,8 @@ export default function TerminalPage() {
|
||||
|
||||
// Formulaire de connexion
|
||||
const [form, setForm] = useState({
|
||||
preset: DEFAULT_SSH_PRESET,
|
||||
host: SERVER_PUBLIC_IP,
|
||||
preset: 'recette',
|
||||
host: '78.138.58.109',
|
||||
port: 22,
|
||||
username: 'root',
|
||||
password: '',
|
||||
@@ -117,7 +116,7 @@ export default function TerminalPage() {
|
||||
fitAddonRef.current = fitAddon;
|
||||
|
||||
term.writeln('\x1b[1;34m╔══════════════════════════════════════════════════╗\x1b[0m');
|
||||
term.writeln(`\x1b[1;34m║ Terminal SSH — Dashboard ${ENVIRONMENT_LABEL} Santinova ║\x1b[0m`);
|
||||
term.writeln('\x1b[1;34m║ Terminal SSH — Dashboard Production Santinova ║\x1b[0m');
|
||||
term.writeln('\x1b[1;34m╚══════════════════════════════════════════════════╝\x1b[0m');
|
||||
term.writeln('');
|
||||
term.writeln('\x1b[90mSélectionnez un serveur et connectez-vous pour démarrer.\x1b[0m');
|
||||
|
||||
Reference in New Issue
Block a user