Compare commits

...

50 Commits

Author SHA1 Message Date
1c9a9eff8f fix: nommer le dashboard selon l environnement 2026-09-05 02:38:02 +00:00
2d92846f04 chore: intégrer les références système production 2026-09-05 02:37:53 +00:00
428e614c87 fix: adapter l inventaire à l environnement courant 2026-09-05 02:35:02 +00:00
69b5ab7462 chore: intégrer le correctif de libellé production 2026-09-05 02:34:35 +00:00
9b75964c29 chore: déclencher la validation du commit commun 2026-09-05 02:32:14 +00:00
6dd9a09a58 chore: réunifier les historiques dashboard recette et production 2026-09-05 02:28:52 +00:00
0572c4dff6 fix: partager le même commit entre recette et production 2026-09-05 02:25:03 +00:00
Manus Déploiement Santinova
9289011b3c feat: exposer les services infra dans le portail 2026-09-04 15:57:37 +00:00
Manus Déploiement Santinova
9867adbcc2 feat: placer les services infra en tête des listes 2026-09-04 15:51:49 +00:00
Manus Déploiement Santinova
4a2be26a10 fix: préserver la catégorie dans les statuts applicatifs 2026-09-04 15:37:19 +00:00
Manus Déploiement Santinova
5c3d644ab8 Ajouter le dispatcher e-mail des alertes CI 2026-08-21 20:28:13 +00:00
Manus Déploiement Santinova
c693da6273 Ignorer les demandes de déploiement hôte 2026-08-21 20:27:18 +00:00
Manus Déploiement Santinova
c0944cfb13 Corriger la préparation de rotation Gitea 2026-08-21 16:50:17 +00:00
Manus Déploiement Santinova
06fad063a4 Préserver le tableau de bord CI existant 2026-08-21 16:31:13 +00:00
Manus Déploiement Santinova
a37e29e42a Sécuriser Gitea et alerter sur les échecs CI 2026-08-21 16:16:46 +00:00
Santinova Dashboard Automation
1f1d525ea1 Merge branch 'ci-trends' 2026-08-20 17:56:30 +02:00
Manus AI
3da5fd104c feat(ci): afficher tendances et médianes des workflows 2026-08-20 15:52:56 +00:00
Santinova Dashboard Automation
2991c7f068 feat(dashboard): distinguer visuellement les apps INFRA dans le tableau de bord 2026-08-20 16:32:37 +02:00
Manus AI
37f699fcc2 fix(ci): lire les horodatages Gitea 1.25 2026-08-18 17:48:18 +00:00
Manus AI
3eae610c5e fix(deploy): construire le dashboard depuis les sources versionnées 2026-08-18 17:38:13 +00:00
Manus AI
57ef1785aa feat(ci): conditionner les déploiements et exposer les métriques 2026-08-18 17:12:57 +00:00
Manus AI
cbc8eee818 fix(ci): utiliser le binaire tar Alpine sans lien circulaire 2026-08-18 12:54:21 +00:00
Manus AI
666e0be2a2 fix(ci): inclure GNU tar pour le cache pnpm 2026-08-18 12:50:48 +00:00
Manus AI
35206fb14b perf(ci): ajouter cache et image Node 22 au runner recette 2026-08-18 12:33:40 +00:00
Manus AI
d553cc4278 chore(ops): versionner les scripts de supervision comme exécutables 2026-08-18 09:00:05 +00:00
Manus AI
5105123ec4 fix(deploy): déléguer le redéploiement dashboard recette à l hôte 2026-08-18 08:59:23 +00:00
Manus AI
87cc629d81 fix(ci): fournir bash au runner recette local 2026-08-18 08:49:16 +00:00
Manus AI
d8b24f8968 chore(ci): versionner le script d installation du runner comme exécutable 2026-08-18 08:39:26 +00:00
Manus AI
f692be04d2 fix(ci): utiliser une image runner locale compatible 2026-08-18 08:38:17 +00:00
Manus AI
7270e1a89a fix(ci): migrer l enregistrement runner existant 2026-08-18 08:32:45 +00:00
Manus AI
7defcabdc3 fix(ci): conserver l enregistrement du runner Gitea 2026-08-18 08:32:17 +00:00
Manus AI
b006caa3df ci(ops): ajouter runner Gitea recette versionné 2026-08-18 08:30:37 +00:00
Santinova Dashboard Automation
64f3233e47 Merge remote-tracking branch 'origin/main' 2026-08-18 10:06:53 +02:00
Manus AI
bf032cc26a fix(ops): réduire les faux positifs de supervision 2026-08-18 08:04:56 +00:00
Manus AI
392782a0d2 fix(ops): ne pas bloquer l installation sur une alerte 2026-08-18 07:59:53 +00:00
Manus AI
b44cdd5f11 fix(ops): rendre le healthcheck non intrusif 2026-08-18 07:59:27 +00:00
Manus
e168f86578 fix: adapter le webhook au déploiement par environnement 2026-08-18 07:34:15 +00:00
Manus AI
5e8d4796d4 feat(ops): ajouter supervision automatique des applications 2026-08-18 07:31:02 +00:00
Manus
9500ca2d70 refactor: app.json comme source de vérité du dashboard 2026-08-17 20:20:44 +00:00
e451892860 fix(portail): renommer Gestion des AT 2026-07-30 09:51:53 +02:00
Santinova Dashboard Automation
5776b15bb5 merge: intégrer le dashboard recette refondu et les améliorations robustes 2026-07-24 16:08:39 +02:00
Santinova Dashboard Automation
f412839e6e backup: préserver les améliorations robustes de recette 2026-07-24 16:04:15 +02:00
Manus Deploy
6f9ee7fc5c fix: config.js - pilotage image, portail category INFRA, demat image jpg 2026-07-24 13:49:18 +00:00
Manus Deploy
fe6ab4a2ff fix: startContainer crée le conteneur via docker compose up si inexistant 2026-07-24 13:37:10 +00:00
Manus Deploy
03a6d55571 feat: scan automatique des app.json au démarrage (découverte FALC et autres) 2026-07-24 13:15:43 +00:00
Manus Deploy
7a7b9fde47 feat: découverte dynamique apps - portail dynamique - fix URLs 2026-07-24 13:04:21 +00:00
Manus AI
120567a6e5 fix: version webhook deployment flow 2026-07-24 12:32:43 +00:00
Manus AI
9d1db4d035 feat: register gestion at v2 dashboard deployment 2026-07-24 12:12:43 +00:00
Manus
eb07a99e5e fix: /public/apps ne bloque plus sur getAllStatuses (async -> sync) 2026-07-24 10:40:45 +00:00
Manus
85bd5209c7 feat: découverte dynamique des apps via app.json + endpoint /public/apps 2026-07-24 10:25:18 +00:00
17 changed files with 152 additions and 43 deletions

View File

@@ -22,4 +22,5 @@ jobs:
working-directory: src/frontend working-directory: src/frontend
run: | run: |
npm ci npm ci
npm test
npm run build npm run build

View File

@@ -1,6 +1,6 @@
{ {
"id": "manus-dashboard", "id": "manus-dashboard",
"name": "Dashboard Production", "name": "Dashboard",
"category": "INFRA", "category": "INFRA",
"urls": { "urls": {
"recette": "https://dashboard.recette.santinova-soft.org", "recette": "https://dashboard.recette.santinova-soft.org",

View File

@@ -1,47 +1,58 @@
services: services:
dashboard: dashboard:
build: 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 context: ./src
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: manus-dashboard container_name: manus-dashboard
# Redémarrage automatique après un arrêt anormal ; le processus reste borné. # Le dashboard doit repartir seul après un arrêt anormal du processus.
restart: always restart: always
mem_limit: 512m mem_limit: 512m
privileged: true
env_file: env_file:
- .env - .env
environment: environment:
- NODE_ENV=${NODE_ENV:-production} - NODE_ENV=${NODE_ENV:-production}
- DASHBOARD_ENV=prod - DASHBOARD_ENV=${DASHBOARD_ENV:-recette}
- PORT=${PORT:-3001} - PORT=${PORT:-3001}
- JWT_SECRET=${JWT_SECRET} - JWT_SECRET=${JWT_SECRET:?JWT_SECRET requis dans le fichier .env serveur}
- ADMIN_USERNAME=${ADMIN_USERNAME} - ADMIN_USERNAME=${ADMIN_USERNAME:?ADMIN_USERNAME requis dans le fichier .env serveur}
- ADMIN_PASSWORD=${ADMIN_PASSWORD} - ADMIN_PASSWORD=${ADMIN_PASSWORD:?ADMIN_PASSWORD requis dans le fichier .env serveur}
- GITEA_URL=${GITEA_URL} - GITEA_URL=${GITEA_URL:?GITEA_URL requis dans le fichier .env serveur}
- GITEA_USERNAME=${GITEA_USERNAME} - GITEA_USERNAME=${GITEA_USERNAME:-manus-admin}
- GITEA_PASSWORD=${GITEA_PASSWORD} - GITEA_TOKEN=${GITEA_TOKEN:?GITEA_TOKEN requis dans le fichier .env serveur}
- GITEA_TOKEN=${GITEA_TOKEN:?GITEA_TOKEN requis} - GITEA_RECETTE_URL=${GITEA_RECETTE_URL:-http://gitea:3000}
- WEBHOOK_SECRET=${WEBHOOK_SECRET:?WEBHOOK_SECRET requis} - GITEA_RECETTE_PUBLIC_URL=${GITEA_RECETTE_PUBLIC_URL:-https://git.recette.santinova-soft.org}
- GITEA_RECETTE_URL=${GITEA_RECETTE_URL} - GITEA_RECETTE_TOKEN=${GITEA_RECETTE_TOKEN:-}
- GITEA_RECETTE_TOKEN=${GITEA_RECETTE_TOKEN} - GITEA_PROD_URL=${GITEA_PROD_URL:-https://git.santinova-soft.org}
- GITEA_PASSWORD_PROD=${GITEA_PASSWORD} - GITEA_TOKEN_PROD=${GITEA_TOKEN_PROD:-}
- APPS_BASE_PATH=${APPS_BASE_PATH:-/opt/manus-deploy/apps} - WEBHOOK_SECRET=${WEBHOOK_SECRET:?WEBHOOK_SECRET requis dans le fichier .env serveur}
- INFRA_BASE_PATH=${INFRA_BASE_PATH:-/opt/manus-deploy/infrastructure} - 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
# Contrôles bornés et dédoublonnés par healthcheck.js. # Contrôles bornés et dédoublonnés par healthcheck.js.
- HEALTH_CHECK_INTERVAL=${HEALTH_CHECK_INTERVAL:-60000} - HEALTH_CHECK_INTERVAL=60000
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- /opt/manus-deploy:/opt/manus-deploy - /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: networks:
- web - web
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.manus-dashboard.rule=Host(`dashboard.santinova-soft.org`)" # 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.entrypoints=websecure" - "traefik.http.routers.manus-dashboard.entrypoints=websecure"
- "traefik.http.routers.manus-dashboard.tls=true" - "traefik.http.routers.manus-dashboard.tls=true"
- "traefik.http.routers.manus-dashboard.tls.certresolver=letsencrypt" - "traefik.http.routers.manus-dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.manus-dashboard.service=manus-dashboard-svc" - "traefik.http.routers.manus-dashboard.service=manus-dashboard-svc"
- "traefik.http.routers.manus-dashboard.priority=100" - "traefik.http.routers.manus-dashboard.priority=100"
# Service
- "traefik.http.services.manus-dashboard-svc.loadbalancer.server.port=3001" - "traefik.http.services.manus-dashboard-svc.loadbalancer.server.port=3001"
- "traefik.docker.network=web" - "traefik.docker.network=web"

View File

@@ -51,9 +51,13 @@ function normalizeManifest(manifest, directory, environment) {
throw new Error('ci doit être un objet avec une propriété required booléenne'); throw new Error('ci doit être un objet avec une propriété required booléenne');
} }
const displayName = manifest.id === 'manus-dashboard'
? `Dashboard ${environment === 'prod' ? 'Production' : 'Recette'}`
: manifest.name.trim();
return { return {
...manifest, ...manifest,
name: manifest.name.trim(), name: displayName,
directory, directory,
containerName: manifest.containerName || manifest.id, containerName: manifest.containerName || manifest.id,
healthCheckUrl: manifest.urls[environment], healthCheckUrl: manifest.urls[environment],

View File

@@ -27,7 +27,7 @@ module.exports = {
username: process.env.GITEA_USERNAME || 'manus-admin', username: process.env.GITEA_USERNAME || 'manus-admin',
token: process.env.GITEA_TOKEN || null, token: process.env.GITEA_TOKEN || null,
productionUrl: process.env.GITEA_PROD_URL || 'https://git.santinova-soft.org', productionUrl: process.env.GITEA_PROD_URL || 'https://git.santinova-soft.org',
productionToken: process.env.GITEA_TOKEN_PROD || process.env.GITEA_TOKEN || null, productionToken: process.env.GITEA_TOKEN_PROD || null,
}, },
dsiAlerts: { dsiAlerts: {

View File

@@ -30,6 +30,16 @@ try {
}, },
}); });
writeManifest(sandbox, 'dashboard', {
id: 'manus-dashboard',
name: 'Dashboard',
category: 'INFRA',
urls: {
recette: 'https://dashboard.recette.santinova-soft.org',
prod: 'https://dashboard.santinova-soft.org',
},
});
writeManifest(sandbox, 'invalide', { writeManifest(sandbox, 'invalide', {
id: 'app-invalide', id: 'app-invalide',
name: 'Application invalide', name: 'Application invalide',
@@ -38,22 +48,32 @@ try {
}); });
const discovery = discoverApps({ appsBasePath: sandbox, environment: 'recette' }); const discovery = discoverApps({ appsBasePath: sandbox, environment: 'recette' });
assert.equal(discovery.apps.length, 1, 'un manifeste invalide doit être exclu'); 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.errors.length, 1, 'une erreur explicite doit être retournée');
const app = discovery.apps[0]; 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.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.healthCheckUrl, app.urls.recette, 'la vérification doit cibler l’environnement courant');
assert.equal(app.containerName, 'itinova-contacts'); assert.equal(app.containerName, 'itinova-contacts');
const dashboardRecette = discovery.apps.find((candidate) => candidate.id === 'manus-dashboard');
assert.equal(dashboardRecette.name, 'Dashboard Recette');
const productionDiscovery = discoverApps({ appsBasePath: sandbox, environment: 'prod' });
const dashboardProduction = productionDiscovery.apps.find((candidate) => candidate.id === 'manus-dashboard');
assert.equal(dashboardProduction.name, 'Dashboard Production');
const config = { const config = {
appsBasePath: sandbox, appsBasePath: sandbox,
environment: 'recette', environment: 'recette',
apps: [{ id: 'itinova-contacts', name: 'ancienne valeur statique' }], apps: [{ id: 'itinova-contacts', name: 'ancienne valeur statique' }],
}; };
const refresh = refreshApps(config); const refresh = refreshApps(config);
assert.equal(refresh.apps.length, 1, 'les applications non déployées ne doivent pas rester en cache'); assert.equal(refresh.apps.length, 2, 'les applications non déployées ne doivent pas rester en cache');
assert.equal(config.apps[0].name, 'Itinova Contacts', 'app.json doit prévaloir sur toute définition statique'); assert.equal(
config.apps.find((candidate) => candidate.id === 'itinova-contacts').name,
'Itinova Contacts',
'app.json doit prévaloir sur toute définition statique'
);
console.log('OK app-registry'); console.log('OK app-registry');
} finally { } finally {

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dashboard Production - Gestion des Applications</title> <title>Dashboard - Gestion des Applications</title>
</head> </head>
<body class="bg-dark-900 text-white"> <body class="bg-dark-900 text-white">
<div id="root"></div> <div id="root"></div>

View File

@@ -5,6 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"test": "node --test test/*.test.mjs",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview"
}, },

View File

@@ -14,6 +14,7 @@ import {
ChevronDown, ChevronDown,
} from 'lucide-react'; } from 'lucide-react';
import { getAvailableStacks, createApp, getGiteaRepos } from '../utils/api'; import { getAvailableStacks, createApp, getGiteaRepos } from '../utils/api';
import { APP_DOMAIN_SUFFIX, ENVIRONMENT_LABEL_LOWER } from '../utils/environment';
const STEPS = [ const STEPS = [
{ id: 1, title: 'Informations', icon: FileText }, { id: 1, title: 'Informations', icon: FileText },
@@ -180,7 +181,7 @@ export default function AddAppModal({ isOpen, onClose, onSuccess }) {
Ajouter une application Ajouter une application
</h2> </h2>
<p className="text-sm text-gray-400 mt-0.5"> <p className="text-sm text-gray-400 mt-0.5">
Déployez une nouvelle application sur le serveur de recette Déployez une nouvelle application sur le serveur de {ENVIRONMENT_LABEL_LOWER}
</p> </p>
</div> </div>
<button <button
@@ -271,7 +272,7 @@ export default function AddAppModal({ isOpen, onClose, onSuccess }) {
}`} }`}
/> />
<span className="px-3 py-2.5 bg-dark-600 border border-dark-500 border-l-0 rounded-r-lg text-gray-400 text-sm whitespace-nowrap"> <span className="px-3 py-2.5 bg-dark-600 border border-dark-500 border-l-0 rounded-r-lg text-gray-400 text-sm whitespace-nowrap">
.recette.santinova-soft.org .{APP_DOMAIN_SUFFIX}
</span> </span>
</div> </div>
{errors.subdomain && ( {errors.subdomain && (
@@ -280,7 +281,7 @@ export default function AddAppModal({ isOpen, onClose, onSuccess }) {
{formData.subdomain && !errors.subdomain && ( {formData.subdomain && !errors.subdomain && (
<p className="text-gray-500 text-xs mt-1 flex items-center gap-1"> <p className="text-gray-500 text-xs mt-1 flex items-center gap-1">
<Globe className="w-3 h-3" /> <Globe className="w-3 h-3" />
URL: https://{formData.subdomain}.recette.santinova-soft.org URL: https://{formData.subdomain}.{APP_DOMAIN_SUFFIX}
</p> </p>
)} )}
</div> </div>
@@ -545,7 +546,7 @@ export default function AddAppModal({ isOpen, onClose, onSuccess }) {
L'application est en cours de déploiement. Suivez la progression dans l'onglet Déploiements. L'application est en cours de déploiement. Suivez la progression dans l'onglet Déploiements.
</p> </p>
<p className="text-gray-500 mt-2 text-xs"> <p className="text-gray-500 mt-2 text-xs">
URL: https://{formData.subdomain}.recette.santinova-soft.org URL: https://{formData.subdomain}.{APP_DOMAIN_SUFFIX}
</p> </p>
</div> </div>
) : ( ) : (
@@ -573,7 +574,7 @@ export default function AddAppModal({ isOpen, onClose, onSuccess }) {
<div className="flex justify-between items-center px-4 py-3"> <div className="flex justify-between items-center px-4 py-3">
<span className="text-gray-400 text-sm">URL</span> <span className="text-gray-400 text-sm">URL</span>
<span className="text-primary-400 text-sm"> <span className="text-primary-400 text-sm">
https://{formData.subdomain}.recette.santinova-soft.org https://{formData.subdomain}.{APP_DOMAIN_SUFFIX}
</span> </span>
</div> </div>
<div className="flex justify-between items-center px-4 py-3"> <div className="flex justify-between items-center px-4 py-3">

View File

@@ -12,6 +12,7 @@ import { LayoutList, TerminalSquare, ShieldCheck,
Activity, Activity,
BookOpen, BookOpen,
} from 'lucide-react'; } from 'lucide-react';
import { ENVIRONMENT_LABEL } from '../utils/environment';
const navItems = [ const navItems = [
{ id: 'dashboard', label: 'Tableau de bord', icon: LayoutDashboard }, { id: 'dashboard', label: 'Tableau de bord', icon: LayoutDashboard },
{ id: 'apps', label: 'Applications', icon: Box }, { id: 'apps', label: 'Applications', icon: Box },
@@ -40,7 +41,7 @@ export default function Sidebar({
</div> </div>
<div> <div>
<h1 className="text-lg font-bold text-white">Dashboard</h1> <h1 className="text-lg font-bold text-white">Dashboard</h1>
<p className="text-xs text-gray-500">Production</p> <p className="text-xs text-gray-500">{ENVIRONMENT_LABEL}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -13,6 +13,7 @@ import {
import { getApps, getSystemInfo } from '../utils/api'; import { getApps, getSystemInfo } from '../utils/api';
import StatusBadge from '../components/StatusBadge'; import StatusBadge from '../components/StatusBadge';
import { sortAppsWithInfraFirst } from '../utils/appSorting'; 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 }) { function StatCard({ icon: Icon, label, value, color, subtext }) {
return ( return (
@@ -101,7 +102,7 @@ export default function DashboardPage({ apps }) {
<div className="flex items-center justify-between py-2 border-b border-dark-700"> <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-400">Serveur</span>
<span className="text-sm text-gray-200"> <span className="text-sm text-gray-200">
180.149.196.138 (Production) {SERVER_PUBLIC_IP} ({ENVIRONMENT_LABEL})
</span> </span>
</div> </div>
<div className="flex items-center justify-between py-2 border-b border-dark-700"> <div className="flex items-center justify-between py-2 border-b border-dark-700">
@@ -114,12 +115,12 @@ export default function DashboardPage({ apps }) {
<div className="flex items-center justify-between py-2 border-b border-dark-700"> <div className="flex items-center justify-between py-2 border-b border-dark-700">
<span className="text-sm text-gray-400">Gitea</span> <span className="text-sm text-gray-400">Gitea</span>
<a <a
href="https://git.santinova-soft.org" href={GITEA_URL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-sm text-primary-400 hover:text-primary-300" className="text-sm text-primary-400 hover:text-primary-300"
> >
git.santinova-soft.org {GITEA_HOST}
</a> </a>
</div> </div>
<div className="flex items-center justify-between py-2 border-b border-dark-700"> <div className="flex items-center justify-between py-2 border-b border-dark-700">

View File

@@ -19,6 +19,7 @@ import {
ShieldCheck, ShieldCheck,
} from 'lucide-react'; } from 'lucide-react';
import { approveGiteaTokenRotation, getGiteaRepos, getGiteaCommits, getGiteaTokenRotation, getGiteaWorkflowRuns, getGiteaWorkflowSummary, prepareGiteaTokenRotation } from '../utils/api'; import { approveGiteaTokenRotation, getGiteaRepos, getGiteaCommits, getGiteaTokenRotation, getGiteaWorkflowRuns, getGiteaWorkflowSummary, prepareGiteaTokenRotation } from '../utils/api';
import { GITEA_HOST, GITEA_URL } from '../utils/environment';
function formatDate(dateStr) { function formatDate(dateStr) {
if (!dateStr) return 'N/A'; if (!dateStr) return 'N/A';
@@ -137,12 +138,12 @@ export default function GiteaPage() {
<p className="text-gray-400 mt-1"> <p className="text-gray-400 mt-1">
Explorez les dépôts sur{' '} Explorez les dépôts sur{' '}
<a <a
href="https://git.recette.santinova-soft.org" href={GITEA_URL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-primary-400 hover:text-primary-300" className="text-primary-400 hover:text-primary-300"
> >
git.recette.santinova-soft.org {GITEA_HOST}
</a> </a>
</p> </p>
</div> </div>

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Table, RefreshCw, GitBranch, CheckCircle, XCircle, ExternalLink, Tag, AlertCircle, ArrowUp, ArrowDown, Minus, AlertTriangle } from 'lucide-react'; import { Table, RefreshCw, GitBranch, CheckCircle, XCircle, ExternalLink, Tag, AlertCircle, ArrowUp, ArrowDown, Minus, AlertTriangle } from 'lucide-react';
import api from '../utils/api'; import api from '../utils/api';
import { ENVIRONMENT_LABEL } from '../utils/environment';
const APPS_CONFIG = [ const APPS_CONFIG = [
{ {
@@ -89,7 +90,7 @@ const APPS_CONFIG = [
}, },
{ {
id: 'manus-dashboard', id: 'manus-dashboard',
name: 'Dashboard Production', name: `Dashboard ${ENVIRONMENT_LABEL}`,
repoName: 'manus-dashboard', repoName: 'manus-dashboard',
urlRecette: 'https://dashboard.recette.santinova-soft.org', urlRecette: 'https://dashboard.recette.santinova-soft.org',
urlProd: 'https://dashboard.santinova-soft.org', urlProd: 'https://dashboard.santinova-soft.org',

View File

@@ -1,6 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Lock, User, AlertCircle, Server } from 'lucide-react'; import { Lock, User, AlertCircle, Server } from 'lucide-react';
import { login } from '../utils/api'; import { login } from '../utils/api';
import { ENVIRONMENT_LABEL, ENVIRONMENT_LABEL_LOWER } from '../utils/environment';
export default function LoginPage({ onLogin }) { export default function LoginPage({ onLogin }) {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
@@ -34,7 +35,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"> <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" /> <Server className="w-8 h-8 text-white" />
</div> </div>
<h1 className="text-2xl font-bold text-white">Dashboard Production</h1> <h1 className="text-2xl font-bold text-white">Dashboard {ENVIRONMENT_LABEL}</h1>
<p className="text-gray-400 mt-2">Gestion des applications</p> <p className="text-gray-400 mt-2">Gestion des applications</p>
</div> </div>
@@ -130,7 +131,7 @@ export default function LoginPage({ onLogin }) {
</div> </div>
<p className="text-center text-gray-600 text-sm mt-6"> <p className="text-center text-gray-600 text-sm mt-6">
Santinova Soft &mdash; Serveur de production Santinova Soft &mdash; Serveur de {ENVIRONMENT_LABEL_LOWER}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Terminal, Wifi, WifiOff, X, Plus, ChevronDown } from 'lucide-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 // Serveurs SSH prédéfinis
const PRESET_SERVERS = [ const PRESET_SERVERS = [
@@ -30,8 +31,8 @@ export default function TerminalPage() {
// Formulaire de connexion // Formulaire de connexion
const [form, setForm] = useState({ const [form, setForm] = useState({
preset: 'recette', preset: DEFAULT_SSH_PRESET,
host: '78.138.58.109', host: SERVER_PUBLIC_IP,
port: 22, port: 22,
username: 'root', username: 'root',
password: '', password: '',
@@ -116,7 +117,7 @@ export default function TerminalPage() {
fitAddonRef.current = fitAddon; fitAddonRef.current = fitAddon;
term.writeln('\x1b[1;34m╔══════════════════════════════════════════════════╗\x1b[0m'); term.writeln('\x1b[1;34m╔══════════════════════════════════════════════════╗\x1b[0m');
term.writeln('\x1b[1;34m║ Terminal SSH — Dashboard Production Santinova ║\x1b[0m'); term.writeln(`\x1b[1;34m║ Terminal SSH — Dashboard ${ENVIRONMENT_LABEL} Santinova ║\x1b[0m`);
term.writeln('\x1b[1;34m╚══════════════════════════════════════════════════╝\x1b[0m'); term.writeln('\x1b[1;34m╚══════════════════════════════════════════════════╝\x1b[0m');
term.writeln(''); term.writeln('');
term.writeln('\x1b[90mSélectionnez un serveur et connectez-vous pour démarrer.\x1b[0m'); term.writeln('\x1b[90mSélectionnez un serveur et connectez-vous pour démarrer.\x1b[0m');

View File

@@ -0,0 +1,38 @@
/**
* Identité de l'environnement courant, déterminée par le domaine public.
* Le même commit peut ainsi être construit et exécuté en recette comme en
* production, les secrets et domaines restant fournis par chaque serveur.
*/
export function getEnvironmentConfig(hostname) {
const normalizedHostname = hostname.toLowerCase();
const isRecette =
normalizedHostname.includes('recette') ||
normalizedHostname === 'localhost' ||
normalizedHostname === '127.0.0.1';
return {
isRecette,
label: isRecette ? 'Recette' : 'Production',
labelLower: isRecette ? 'recette' : 'production',
serverPublicIp: isRecette ? '78.138.58.109' : '180.149.196.138',
giteaUrl: isRecette
? 'https://git.recette.santinova-soft.org'
: 'https://git.santinova-soft.org',
appDomainSuffix: isRecette
? 'recette.santinova-soft.org'
: 'santinova-soft.org',
defaultSshPreset: isRecette ? 'recette' : 'production',
};
}
const currentHostname = typeof window === 'undefined' ? 'localhost' : window.location.hostname;
const environment = getEnvironmentConfig(currentHostname);
export const IS_RECETTE = environment.isRecette;
export const ENVIRONMENT_LABEL = environment.label;
export const ENVIRONMENT_LABEL_LOWER = environment.labelLower;
export const SERVER_PUBLIC_IP = environment.serverPublicIp;
export const GITEA_URL = environment.giteaUrl;
export const GITEA_HOST = new URL(GITEA_URL).hostname;
export const APP_DOMAIN_SUFFIX = environment.appDomainSuffix;
export const DEFAULT_SSH_PRESET = environment.defaultSshPreset;

View File

@@ -0,0 +1,27 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { getEnvironmentConfig } from '../src/utils/environment.js';
test('détecte le dashboard de recette depuis son domaine public', () => {
const config = getEnvironmentConfig('dashboard.recette.santinova-soft.org');
assert.equal(config.isRecette, true);
assert.equal(config.label, 'Recette');
assert.equal(config.serverPublicIp, '78.138.58.109');
assert.equal(config.giteaUrl, 'https://git.recette.santinova-soft.org');
assert.equal(config.appDomainSuffix, 'recette.santinova-soft.org');
});
test('détecte le dashboard de production depuis son domaine public', () => {
const config = getEnvironmentConfig('dashboard.santinova-soft.org');
assert.equal(config.isRecette, false);
assert.equal(config.label, 'Production');
assert.equal(config.serverPublicIp, '180.149.196.138');
assert.equal(config.giteaUrl, 'https://git.santinova-soft.org');
assert.equal(config.appDomainSuffix, 'santinova-soft.org');
});
test('utilise la recette pour le développement local', () => {
assert.equal(getEnvironmentConfig('localhost').isRecette, true);
assert.equal(getEnvironmentConfig('127.0.0.1').isRecette, true);
});