feat: synchroniser la configuration IA active du dashboard
All checks were successful
Validation Dashboard / Tests backend et build frontend (push) Successful in 43s

This commit is contained in:
Manus Deploy
2026-09-10 00:28:08 +00:00
parent 7d7006ab29
commit 85a1415e05
12 changed files with 326 additions and 40 deletions

View File

@@ -222,7 +222,7 @@ export default function AppCard({ app, commits, onRefresh }) {
{/* Moteurs IA déclarés dans app.json */}
<div>
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">
Moteur IA
{app.ai?.runtimeStatus === 'active' ? 'Moteur IA actif' : 'Moteur IA'}
</span>
<div className="mt-1.5 space-y-2">
{aiEngines.length > 0 ? aiEngines.map((engine, index) => (
@@ -242,15 +242,22 @@ export default function AppCard({ app, commits, onRefresh }) {
<p className="text-xs text-primary-400 break-words">
{engine.model}{engine.role ? ` — ${engine.role}` : ''}
</p>
<p className="mt-0.5 text-[11px] leading-snug text-gray-500">
<p
className={`mt-0.5 text-[11px] leading-snug ${
engine.source === 'referential' ? 'text-emerald-300' : 'text-orange-300'
}`}
>
{engine.source === 'referential'
? 'Utilisation du référentiel IA'
: "Moteur IA configuré dans l'applicatif"}
</p>
</div>
)) : (
<p className="text-sm text-gray-600">Non déclaré</p>
<p className="text-sm text-gray-600">
{app.ai?.activeConfigurationPath ? 'Configuration active indisponible' : 'Non déclaré'}
</p>
)}
</div>
</div>

View File

@@ -19,6 +19,16 @@ import {
Clock,
Zap,
Package,
Bot,
BrainCircuit,
Workflow,
ShieldCheck,
Route,
ArrowDown,
CircleAlert,
LockKeyhole,
ServerCog,
Sparkles,
} from 'lucide-react';
/* ─────────────────────────────────────────────
@@ -95,6 +105,22 @@ const RECETTE = {
url: 'contacts.recette.santinova-soft.org',
href: 'https://contacts.recette.santinova-soft.org',
},
{
icon: BrainCircuit,
color: 'purple',
name: 'Référentiel IA Santinova',
url: 'referentiel-ia.recette.santinova-soft.org',
desc: 'Décisions, contrats sécurisés et budgets IA',
href: 'https://referentiel-ia.recette.santinova-soft.org',
},
{
icon: ServerCog,
color: 'cyan',
name: 'Moteur IA local',
url: 'llm-local.recette.santinova-soft.org',
desc: 'Ollama privé · modèle local disponible',
href: 'https://llm-local.recette.santinova-soft.org',
},
{
icon: '🚗',
color: 'yellow',
@@ -248,6 +274,74 @@ function SectionTitle({ children, color = 'amber' }) {
return <h3 className={`text-xs font-bold uppercase tracking-widest ${cls} mb-3`}>{children}</h3>;
}
function WorkflowNode({ icon: Icon, eyebrow, title, description, tone = 'blue' }) {
const tones = {
blue: 'border-blue-500/30 bg-blue-500/10 text-blue-300',
purple: 'border-purple-500/30 bg-purple-500/10 text-purple-300',
teal: 'border-teal-500/30 bg-teal-500/10 text-teal-300',
emerald: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-300',
};
return (
<div className={`rounded-2xl border p-4 ${tones[tone]}`}>
<div className="flex items-start gap-3">
<div className="rounded-xl bg-dark-950/35 p-2.5"><Icon className="h-5 w-5" aria-hidden="true" /></div>
<div className="min-w-0">
<p className="text-[10px] font-bold uppercase tracking-[0.16em] opacity-70">{eyebrow}</p>
<h4 className="mt-1 text-sm font-bold text-white">{title}</h4>
<p className="mt-1 text-xs leading-5 text-gray-300">{description}</p>
</div>
</div>
</div>
);
}
function AiDecisionWorkflow() {
const modes = [
{ icon: Sparkles, title: 'Recommandation', description: 'Le Référentiel retient le moteur adapté au cas d’usage et aux règles de coût.', tone: 'blue' },
{ icon: Route, title: 'Choix manuel', description: 'L’administrateur sélectionne un moteur externe parmi ceux autorisés.', tone: 'purple' },
{ icon: ServerCog, title: 'Moteur IA local', description: 'L’application utilise le modèle Ollama privé disponible.', tone: 'teal' },
];
return (
<section className="rounded-2xl border border-cyan-500/25 bg-gradient-to-br from-cyan-500/[0.07] via-dark-900 to-purple-500/[0.06] p-6" aria-labelledby="ia-workflow-title">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="flex gap-3">
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-cyan-500/15 text-cyan-300"><Workflow className="h-5 w-5" aria-hidden="true" /></div>
<div>
<p className="text-xs font-bold uppercase tracking-[0.16em] text-cyan-400">Parcours applicatif</p>
<h2 id="ia-workflow-title" className="mt-1 text-xl font-bold text-white">Quel moteur IA utilise chaque application ?</h2>
<p className="mt-2 max-w-3xl text-sm leading-6 text-gray-300">Le Référentiel IA décide et protège le parcours ; le Moteur IA local exécute les demandes privées sur Ollama lorsque ce mode est retenu.</p>
</div>
</div>
<div className="flex items-center gap-2 rounded-xl border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-300"><ShieldCheck className="h-4 w-4" aria-hidden="true" />Aucune clé fournisseur ni URL Ollama privée</div>
</div>
<div className="mt-7 flex flex-col items-stretch gap-3 lg:flex-row lg:items-center lg:justify-center">
<div className="w-full max-w-sm lg:w-72"><WorkflowNode icon={Bot} eyebrow="1 · demande" title="Application cliente" description="Envoie sa demande à la passerelle du Référentiel IA." tone="blue" /></div>
<ArrowRight className="mx-auto h-5 w-5 rotate-90 text-cyan-400 lg:rotate-0" aria-hidden="true" />
<div className="w-full max-w-sm lg:w-80"><WorkflowNode icon={BrainCircuit} eyebrow="2 · décision" title="Référentiel IA Santinova" description="Applique le choix, les autorisations, les budgets et la traçabilité." tone="purple" /></div>
</div>
<div className="mx-auto flex h-8 w-px items-center justify-center bg-gradient-to-b from-purple-400 to-cyan-400" aria-hidden="true"><ArrowDown className="mt-6 h-4 w-4 text-cyan-300" /></div>
<div className="grid gap-3 md:grid-cols-3" role="list" aria-label="Modes de décision du moteur IA">
{modes.map((mode) => <div key={mode.title} role="listitem"><WorkflowNode icon={mode.icon} eyebrow="3 · mode de décision" title={mode.title} description={mode.description} tone={mode.tone} /></div>)}
</div>
<div className="mt-4 grid gap-4 lg:grid-cols-[1fr_auto_1fr] lg:items-stretch">
<WorkflowNode icon={ServerCog} eyebrow="4a · exécution privée" title="Moteur IA local" description="Le service exécute localement sur Ollama. Les requêtes et jetons locaux disposent de plafonds dédiés." tone="teal" />
<div className="flex items-center justify-center" aria-hidden="true"><ArrowRight className="h-5 w-5 rotate-90 text-gray-500 lg:rotate-0" /></div>
<WorkflowNode icon={Globe} eyebrow="4b · exécution externe" title="Moteur externe autorisé" description="La passerelle appelle exclusivement le fournisseur prévu par la décision ou le choix manuel." tone="emerald" />
</div>
<div className="mt-4 grid gap-3 lg:grid-cols-[1fr_1.35fr]">
<div className="flex gap-3 rounded-xl border border-amber-500/25 bg-amber-500/[0.08] p-4"><CircleAlert className="h-5 w-5 flex-shrink-0 text-amber-400" aria-hidden="true" /><div><p className="text-sm font-semibold text-amber-200">Basculement externe autorisé</p><p className="mt-1 text-xs leading-5 text-gray-300">Uniquement si le moteur local est indisponible, si l’option est activée et vers le moteur externe autorisé choisi par l’administrateur.</p></div></div>
<div className="flex gap-3 rounded-xl border border-dark-700 bg-dark-950/40 p-4"><LockKeyhole className="h-5 w-5 flex-shrink-0 text-cyan-300" aria-hidden="true" /><div><p className="text-sm font-semibold text-white">Plafonds séparés et traces de décision</p><p className="mt-1 text-xs leading-5 text-gray-400">Les consommations locales et externes restent séparées. Une limite locale atteinte bloque l’appel local sans déclencher de repli.</p></div></div>
</div>
</section>
);
}
/* ─────────────────────────────────────────────
Page principale
───────────────────────────────────────────── */
@@ -267,7 +361,7 @@ export default function DocumentationPage() {
</div>
<div className="ml-auto flex items-center gap-2 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20">
<Clock className="w-3.5 h-3.5 text-emerald-400" />
<span className="text-xs text-emerald-400">Mis à jour le 04 Mai 2026</span>
<span className="text-xs text-emerald-400">Mis à jour le 05 Septembre 2026</span>
</div>
</div>
@@ -300,6 +394,8 @@ export default function DocumentationPage() {
)}
</div>
<AiDecisionWorkflow />
{/* ── Grille Recette / CI-CD / Production ── */}
<div className="grid grid-cols-1 xl:grid-cols-[1fr_auto_1fr] gap-6">

View File

@@ -0,0 +1,28 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFile } from 'node:fs/promises';
const sourcePath = new URL('../src/pages/DocumentationPage.jsx', import.meta.url);
test('la documentation présente les deux applications IA et les trois modes de décision', async () => {
const source = await readFile(sourcePath, 'utf8');
assert.match(source, /Référentiel IA Santinova/);
assert.match(source, /Moteur IA local/);
assert.match(source, /Recommandation/);
assert.match(source, /Choix manuel/);
assert.match(source, /Moteur IA local/);
assert.match(source, /Basculement externe autorisé/);
});
test('le workflow rappelle la séparation des budgets et des accès internes', async () => {
const source = await readFile(sourcePath, 'utf8');
assert.match(source, /Plafonds séparés/);
assert.match(source, /Aucune clé fournisseur ni URL Ollama privée/);
assert.match(source, /aria-labelledby="ia-workflow-title"/);
assert.match(source, /aria-label="Modes de décision du moteur IA"/);
assert.match(source, /referentiel-ia\.recette\.santinova-soft\.org/);
assert.match(source, /llm-local\.recette\.santinova-soft\.org/);
assert.doesNotMatch(source, /http:\/\/ollama:11434/);
});