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:
@@ -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