diff --git a/api/blade-tasks/task_20260418120501_a73eed.json b/api/blade-tasks/task_20260418120501_a73eed.json
new file mode 100644
index 000000000..436775375
--- /dev/null
+++ b/api/blade-tasks/task_20260418120501_a73eed.json
@@ -0,0 +1,11 @@
+{
+ "id": "task_20260418120501_a73eed",
+ "name": "Blade self-heal 14:05",
+ "type": "powershell",
+ "command": "\n# Blade self-heal\nWrite-Host \"Self-heal triggered $(Get-Date)\"\n$agentProc = Get-Process powershell | Where-Object { $_.CommandLine -match 'sentinel-agent' }\nif (!$agentProc) {\n Write-Host \"Agent not running, starting...\"\n Start-Process powershell -ArgumentList \"-ExecutionPolicy\",\"Bypass\",\"-File\",\"C:\\ProgramData\\WEVAL\\sentinel-agent.ps1\" -WindowStyle Hidden\n}\n# Clear stale tasks > 3 days locally\n$cutoff = (Get-Date).AddDays(-3)\nGet-ChildItem \"C:\\ProgramData\\WEVAL\\tasks\\*.json\" -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $cutoff } | Move-Item -Destination \"C:\\ProgramData\\WEVAL\\tasks\\archived\\\" -Force -ErrorAction SilentlyContinue\nWrite-Host \"Self-heal complete\"\n",
+ "cmd": "\n# Blade self-heal\nWrite-Host \"Self-heal triggered $(Get-Date)\"\n$agentProc = Get-Process powershell | Where-Object { $_.CommandLine -match 'sentinel-agent' }\nif (!$agentProc) {\n Write-Host \"Agent not running, starting...\"\n Start-Process powershell -ArgumentList \"-ExecutionPolicy\",\"Bypass\",\"-File\",\"C:\\ProgramData\\WEVAL\\sentinel-agent.ps1\" -WindowStyle Hidden\n}\n# Clear stale tasks > 3 days locally\n$cutoff = (Get-Date).AddDays(-3)\nGet-ChildItem \"C:\\ProgramData\\WEVAL\\tasks\\*.json\" -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $cutoff } | Move-Item -Destination \"C:\\ProgramData\\WEVAL\\tasks\\archived\\\" -Force -ErrorAction SilentlyContinue\nWrite-Host \"Self-heal complete\"\n",
+ "priority": "high",
+ "status": "pending",
+ "created": "2026-04-18T12:05:01+00:00",
+ "created_by": "blade-control-ui"
+}
\ No newline at end of file
diff --git a/api/em-kpi-cache.json b/api/em-kpi-cache.json
index c68d7ebf4..aada94287 100644
--- a/api/em-kpi-cache.json
+++ b/api/em-kpi-cache.json
@@ -1,281 +1,7 @@
-{
- "ts": "2026-04-18T12:00:01+00:00",
- "server": "s204",
- "s204": {
- "load": 0.8,
- "uptime": "2026-04-14 11:51:24",
- "ram_total_mb": 31335,
- "ram_used_mb": 5999,
- "ram_free_mb": 25335,
- "disk_total": "150G",
- "disk_used": "114G",
- "disk_free": "31G",
- "disk_pct": "79%",
- "fpm_workers": 100,
- "docker_containers": 19,
- "cpu_cores": 8
- },
- "s95": {
- "load": 1.86,
- "disk_pct": "88%",
- "status": "UP",
- "ram_total_mb": 15610,
- "ram_free_mb": 11449
- },
- "pmta": [
- {
- "name": "SER6",
- "ip": "110.239.84.121",
- "status": "DOWN"
- },
- {
- "name": "SER7",
- "ip": "110.239.65.64",
- "status": "DOWN"
- },
- {
- "name": "SER8",
- "ip": "182.160.55.107",
- "status": "DOWN"
- },
- {
- "name": "SER9",
- "ip": "110.239.86.68",
- "status": "DOWN"
- }
- ],
- "assets": {
- "html_pages": 241,
- "php_apis": 626,
- "wiki_entries": 1534,
- "vault_doctrines": 58,
- "vault_sessions": 14,
- "vault_decisions": 12
- },
- "tools": {
- "total": 617,
- "registry_version": "?"
- },
- "sovereign": {
- "status": "UP",
- "providers": [
- "Cerebras-fast",
- "Cerebras-think",
- "Groq",
- "Cloudflare-AI",
- "Gemini",
- "SambaNova",
- "NVIDIA-NIM",
- "Mistral",
- "Groq-OSS",
- "HF-Space",
- "HF-Router",
- "OpenRouter",
- "GitHub-Models"
- ],
- "active": 13,
- "total": 13,
- "primary": "Cerebras-fast",
- "cost": "0€"
- },
- "ethica": {
- "total_hcps": 151709,
- "with_email": 110210,
- "with_phone": 145787,
- "gap_email": 41499,
- "pct_email": 72.6,
- "pct_phone": 96.1,
- "by_country": [
- {
- "country": "DZ",
- "hcps": 112324,
- "with_email": 78181,
- "with_tel": 110053,
- "pct_email": 69.6,
- "pct_tel": 98
- },
- {
- "country": "MA",
- "hcps": 19709,
- "with_email": 15045,
- "with_tel": 18717,
- "pct_email": 76.3,
- "pct_tel": 95
- },
- {
- "country": "TN",
- "hcps": 17797,
- "with_email": 15105,
- "with_tel": 17017,
- "pct_email": 84.9,
- "pct_tel": 95.6
- },
- {
- "country": "INTL",
- "hcps": 1879,
- "with_email": 1879,
- "with_tel": 0,
- "pct_email": 100,
- "pct_tel": 0
- }
- ]
- },
- "docker": [
- {
- "name": "loki",
- "status": "Up 2 days",
- "ports": ""
- },
- {
- "name": "listmonk",
- "status": "Up 2 days",
- "ports": ""
- },
- {
- "name": "plausible-plausible-1",
- "status": "Up 21 hours",
- "ports": ""
- },
- {
- "name": "plausible-plausible-db-1",
- "status": "Up 21 hours",
- "ports": ""
- },
- {
- "name": "plausible-plausible-events-db-1",
- "status": "Up 21 hours",
- "ports": ""
- },
- {
- "name": "n8n-docker-n8n-1",
- "status": "Up 2 days",
- "ports": ""
- },
- {
- "name": "mattermost-docker-mm-db-1",
- "status": "Up 2 days",
- "ports": ""
- },
- {
- "name": "mattermost-docker-mattermost-1",
- "status": "Up 2 days (healthy)",
- "ports": ""
- },
- {
- "name": "twenty",
- "status": "Up 2 days",
- "ports": ""
- },
- {
- "name": "twenty-redis",
- "status": "Up 2 days",
- "ports": ""
- },
- {
- "name": "langfuse",
- "status": "Up 2 days",
- "ports": ""
- },
- {
- "name": "redis-weval",
- "status": "Up 3 days",
- "ports": ""
- },
- {
- "name": "gitea",
- "status": "Up 3 days",
- "ports": ""
- },
- {
- "name": "node-exporter",
- "status": "Up 3 days",
- "ports": ""
- },
- {
- "name": "prometheus",
- "status": "Up 3 days",
- "ports": ""
- },
- {
- "name": "searxng",
- "status": "Up 3 days",
- "ports": ""
- },
- {
- "name": "uptime-kuma",
- "status": "Up 3 days (healthy)",
- "ports": ""
- },
- {
- "name": "vaultwarden",
- "status": "Up 3 days (healthy)",
- "ports": ""
- },
- {
- "name": "qdrant",
- "status": "Up 3 days",
- "ports": ""
- }
- ],
- "crons": {
- "active": 14
- },
- "git": {
- "head": "eee80eae AUTO-BACKUP 20260418-1400",
- "dirty": 4,
- "status": "DIRTY"
- },
- "nonreg": {
- "total": 153,
- "passed": 153,
- "score": "100%"
- },
- "services": [
- {
- "name": "DeerFlow",
- "port": 3002,
- "status": "UP"
- },
- {
- "name": "DeerFlow API",
- "port": 8001,
- "status": "UP"
- },
- {
- "name": "Qdrant",
- "port": 6333,
- "status": "UP"
- },
- {
- "name": "Ollama",
- "port": 11434,
- "status": "UP"
- },
- {
- "name": "Redis",
- "port": 6379,
- "status": "UP"
- },
- {
- "name": "Sovereign",
- "port": 4000,
- "status": "UP"
- },
- {
- "name": "SearXNG",
- "port": 8080,
- "status": "UP"
- }
- ],
- "whisper": {
- "binary": "COMPILED",
- "model": "142MB"
- },
- "grand_total": 3095,
- "health": {
- "score": 5,
- "max": 6,
- "pct": 83
- },
- "elapsed_ms": 9026
-}
\ No newline at end of file
+
+
500 Internal Server Error
+
+500 Internal Server Error
+
nginx/1.24.0 (Ubuntu)
+
+
diff --git a/api/v83-business-kpi-latest.json b/api/v83-business-kpi-latest.json
index 79bc4e104..3566862d8 100644
--- a/api/v83-business-kpi-latest.json
+++ b/api/v83-business-kpi-latest.json
@@ -1,7 +1,7 @@
{
"ok": true,
"version": "V83-business-kpi",
- "ts": "2026-04-18T12:00:17+00:00",
+ "ts": "2026-04-18T12:03:22+00:00",
"summary": {
"total_categories": 7,
"total_kpis": 56,
diff --git a/api/wevia-dynamic-resolver.php b/api/wevia-dynamic-resolver.php
index 078ffef62..517758520 100644
--- a/api/wevia-dynamic-resolver.php
+++ b/api/wevia-dynamic-resolver.php
@@ -24,7 +24,7 @@ function wevia_resolve($msg) {
if (@preg_match('/' . $tool['kw'] . '/i', $msg_lower)) {
$score = 10;
} else {
- $keywords = preg_split('/[|.*()]+/', $tool['kw']);
+ $keywords = preg_split('/[|.*()]+/', $tool['kw']); $keywords = array_unique(array_filter(array_map(function($k){ return trim(mb_strtolower($k)); }, $keywords))); /* V84_DEDUPE_FUZZY */
$matched_keywords = 0;
foreach ($keywords as $kw) {
$kw = trim($kw);
diff --git a/api/wevia-dynamic-resolver.php.GOLD-20260418-140228-pre-v84-dedupe b/api/wevia-dynamic-resolver.php.GOLD-20260418-140228-pre-v84-dedupe
new file mode 100644
index 000000000..078ffef62
--- /dev/null
+++ b/api/wevia-dynamic-resolver.php.GOLD-20260418-140228-pre-v84-dedupe
@@ -0,0 +1,91 @@
+ 0 && mb_strlen($msg_lower) > 60 && preg_match("/reconcil|diagnostic|bilan|test_global/", $tool["id"] ?? "")) $score += 1;
+
+ if ($score > $best_score || ($score == $best_score && !empty($tool["cmd"]) && empty($best["cmd"]))) {
+ $second_best_score = $best_score;
+ $best_score = $score;
+ $best = $tool;
+ } elseif ($score > $second_best_score) {
+ $second_best_score = $score;
+ }
+ }
+
+ // HARDENING: seuil relevé de 3 à 6 (exige regex exact score=10 OU 3 keywords matches score=6)
+ // Évite fuzzy-match abusif type "plus" → ports.sh
+ if (!$best || $best_score < 6) {
+ // Retourner suggestion au lieu de null si score "moyen" (3-5)
+ if ($best && $best_score >= 3) {
+ return [
+ 'provider' => 'dynamic-resolver',
+ 'content' => "Je n'ai pas bien compris votre demande. Vous vouliez peut-être :\n • " . ($best['id'] ?? 'unknown') . " (demandez : \"" . ($best['kw'] ?? '') . "\")\n\nSi ce n'est pas ça, essayez :\n • \"candidats dashboard\" — stats recrutement\n • \"facturation mission\" — missions & TJM\n • \"etat du systeme\" — vue globale\n • \"andons actifs\" — alertes en cours",
+ 'tool' => 'disambiguation'
+ ];
+ }
+ return null;
+ }
+
+ // HARDENING: si 2 tools ont scores très proches (diff <= 1) ET regex non-exact, demander clarification
+ if ($best_score < 10 && $second_best_score >= $best_score - 1 && $second_best_score >= 4) {
+ return [
+ 'provider' => 'dynamic-resolver',
+ 'content' => "Votre demande est ambiguë (plusieurs interprétations possibles). Précisez svp :\n • Pour la situation business → \"etat du systeme\", \"candidats dashboard\", \"facturation mission\"\n • Pour l'infra → \"etat global\", \"andons actifs\", \"verify l99\"",
+ 'tool' => 'ambiguous'
+ ];
+ }
+
+ $result = '';
+ if (!empty($best['cmd'])) {
+ $result = shell_exec('sudo timeout 15 bash -c ' . escapeshellarg($best['cmd']) . ' 2>&1') ?? '';
+ if (trim($result) === '') $result = '[timeout 15s] Commande lente. Essayez plus ciblé.';
+ } elseif (strpos($best['api'] ?? '', 'GET:') === 0) {
+ $ctx = stream_context_create(['http' => ['timeout' => 4]]);
+ $result = @file_get_contents('http://127.0.0.1' . substr($best['api'], 4), false, $ctx) ?? '';
+ } elseif (strpos($best['api'] ?? '', 'POST:') === 0) {
+ $ctx = stream_context_create(['http' => ['method' => 'POST', 'timeout' => 4]]);
+ $result = @file_get_contents('http://127.0.0.1' . substr($best['api'], 5), false, $ctx) ?? '';
+ }
+ if (!$result) return ["provider"=>"dynamic-resolver","content"=>"[" . ($best["id"] ?? "tool") . "] pas de reponse (timeout ou service down)","tool"=>$best["id"]??"unknown"];
+ return ['provider' => 'dynamic-resolver', 'content' => trim($result), 'tool' => $best['id'] ?? 'unknown'];
+}
+function wevia_dynamic_resolve($msg) { return wevia_resolve($msg); }
\ No newline at end of file
diff --git a/api/wevia-dynamic-resolver.php.GOLD-20260418-140305-pre-v84-dedupe b/api/wevia-dynamic-resolver.php.GOLD-20260418-140305-pre-v84-dedupe
new file mode 100644
index 000000000..078ffef62
--- /dev/null
+++ b/api/wevia-dynamic-resolver.php.GOLD-20260418-140305-pre-v84-dedupe
@@ -0,0 +1,91 @@
+ 0 && mb_strlen($msg_lower) > 60 && preg_match("/reconcil|diagnostic|bilan|test_global/", $tool["id"] ?? "")) $score += 1;
+
+ if ($score > $best_score || ($score == $best_score && !empty($tool["cmd"]) && empty($best["cmd"]))) {
+ $second_best_score = $best_score;
+ $best_score = $score;
+ $best = $tool;
+ } elseif ($score > $second_best_score) {
+ $second_best_score = $score;
+ }
+ }
+
+ // HARDENING: seuil relevé de 3 à 6 (exige regex exact score=10 OU 3 keywords matches score=6)
+ // Évite fuzzy-match abusif type "plus" → ports.sh
+ if (!$best || $best_score < 6) {
+ // Retourner suggestion au lieu de null si score "moyen" (3-5)
+ if ($best && $best_score >= 3) {
+ return [
+ 'provider' => 'dynamic-resolver',
+ 'content' => "Je n'ai pas bien compris votre demande. Vous vouliez peut-être :\n • " . ($best['id'] ?? 'unknown') . " (demandez : \"" . ($best['kw'] ?? '') . "\")\n\nSi ce n'est pas ça, essayez :\n • \"candidats dashboard\" — stats recrutement\n • \"facturation mission\" — missions & TJM\n • \"etat du systeme\" — vue globale\n • \"andons actifs\" — alertes en cours",
+ 'tool' => 'disambiguation'
+ ];
+ }
+ return null;
+ }
+
+ // HARDENING: si 2 tools ont scores très proches (diff <= 1) ET regex non-exact, demander clarification
+ if ($best_score < 10 && $second_best_score >= $best_score - 1 && $second_best_score >= 4) {
+ return [
+ 'provider' => 'dynamic-resolver',
+ 'content' => "Votre demande est ambiguë (plusieurs interprétations possibles). Précisez svp :\n • Pour la situation business → \"etat du systeme\", \"candidats dashboard\", \"facturation mission\"\n • Pour l'infra → \"etat global\", \"andons actifs\", \"verify l99\"",
+ 'tool' => 'ambiguous'
+ ];
+ }
+
+ $result = '';
+ if (!empty($best['cmd'])) {
+ $result = shell_exec('sudo timeout 15 bash -c ' . escapeshellarg($best['cmd']) . ' 2>&1') ?? '';
+ if (trim($result) === '') $result = '[timeout 15s] Commande lente. Essayez plus ciblé.';
+ } elseif (strpos($best['api'] ?? '', 'GET:') === 0) {
+ $ctx = stream_context_create(['http' => ['timeout' => 4]]);
+ $result = @file_get_contents('http://127.0.0.1' . substr($best['api'], 4), false, $ctx) ?? '';
+ } elseif (strpos($best['api'] ?? '', 'POST:') === 0) {
+ $ctx = stream_context_create(['http' => ['method' => 'POST', 'timeout' => 4]]);
+ $result = @file_get_contents('http://127.0.0.1' . substr($best['api'], 5), false, $ctx) ?? '';
+ }
+ if (!$result) return ["provider"=>"dynamic-resolver","content"=>"[" . ($best["id"] ?? "tool") . "] pas de reponse (timeout ou service down)","tool"=>$best["id"]??"unknown"];
+ return ['provider' => 'dynamic-resolver', 'content' => trim($result), 'tool' => $best['id'] ?? 'unknown'];
+}
+function wevia_dynamic_resolve($msg) { return wevia_resolve($msg); }
\ No newline at end of file
diff --git a/oss-discovery.html b/oss-discovery.html
index c05d20ab6..1589d2cfe 100644
--- a/oss-discovery.html
+++ b/oss-discovery.html
@@ -164,7 +164,7 @@ function render(d,tools,sk){
🧩 All Injected Skills
${sk.total||0}
${(sk.path||"skills")+"/"}
-
${Array.isArray(sk.skills)?sk.skills.map(s=>`
${s.slug||s.name||s}
`).join(''):(sk.total>0||sk.skills)?`
📁 ${sk.total||(sk.skills&&sk.skills.total)||0} skills disponibles dans /skills/ —
parcourir`:'
Chargement...
'}
+
${Array.isArray(sk.skills)?sk.skills.map(s=>`
${s.slug||s.name||s}
`).join(''):(sk.total>0||sk.skills)?`
📁 ${sk.total||(sk.skills&&sk.skills.total)||0} skills disponibles dans /skills/ —
parcourir`:`
📁 ${(sk&&sk.total)||0} skills disponibles dans /skills/ —
parcourir`}
`;
diff --git a/oss-discovery.html.GOLD-20260418-140415-pre-v84-defensive b/oss-discovery.html.GOLD-20260418-140415-pre-v84-defensive
new file mode 100644
index 000000000..c05d20ab6
--- /dev/null
+++ b/oss-discovery.html.GOLD-20260418-140415-pre-v84-defensive
@@ -0,0 +1,300 @@
+
+
+
+
+
+WEVAL — OSS Discovery
+
+
+
+
+#9889; 669 Agents
#127970; 22 Depts
#128051; 20 Docker
#129302; 10 Ollama
#128200; 153/153
#128274; SSO OK
+
+
+
🔍
+
OSS Discovery
Sovereign Skill Learning Engine v2.0
+
+
+
+
+ ● Live
+
+
+
+
+
+
+