phase54 doctrine192 DASHBOARD ENRICHI COMMAND CENTER | Chart.js timeline 24h stacked bar tasks done/failed/pending | SVG gauge coverage CDP | 18 boutons action live via POST wevia-chat | Quick Ask IA widget textarea + select 12 providers Blade/CDP | Recent tasks feed live 10 entries + Async jobs feed 8 entries | S204 metrics live (load disk mem chromes) | Provider tiles cliquables test direct | Toast notifications actions | Auto-refresh 30s | backend v2 +timeline +recent_tasks +jobs_recent +s204_metrics | 15KB->27KB HTML premium UX

This commit is contained in:
Opus
2026-04-24 16:07:48 +02:00
parent f39957c026
commit 0685c4f565
2 changed files with 418 additions and 196 deletions

View File

@@ -1,12 +1,11 @@
<?php
// /api/web-ia-health.php - WEVIA Web IA consolidated health
// Aggregates: Blade heartbeat + agent status + CDP local 8 providers + tasks stats
// /api/web-ia-health.php v2 - ENRICHED dashboard backend
header("Content-Type: application/json");
date_default_timezone_set("UTC");
$out = ["ok" => true, "ts" => date("c"), "sections" => []];
// === SECTION 1: BLADE PC YACINE ===
// === SECTION 1: BLADE ===
$hb_file = "/var/www/html/api/blade-tasks/heartbeat.json";
$blade = ["online" => false, "heartbeat_age_s" => -1, "ip" => null, "agent_version" => null, "recommendation" => null];
if (file_exists($hb_file)) {
@@ -19,79 +18,106 @@ if (file_exists($hb_file)) {
$blade["online"] = $blade["heartbeat_age_s"] >= 0 && $blade["heartbeat_age_s"] < 120;
}
}
if ($blade["online"]) {
$blade["status_label"] = "ACTIF";
$blade["color"] = "teal";
$blade["recommendation"] = "OK - Yacine accessible. Chrome sessions cookies persistants.";
} elseif ($blade["heartbeat_age_s"] < 0) {
$blade["status_label"] = "JAMAIS VU";
$blade["color"] = "red";
$blade["recommendation"] = "Installer/demarrer agent Blade v2.0 sur PC Yacine";
} elseif ($blade["heartbeat_age_s"] < 600) {
$blade["status_label"] = "RECENT SILENCE";
$blade["color"] = "orange";
$blade["recommendation"] = "PC Yacine probablement en veille courte. Bouger la souris ou reveiller.";
} else {
$blade["status_label"] = "LONGUE VEILLE";
$blade["color"] = "red";
$blade["recommendation"] = "PC Yacine eteint ou mode avion. Verifier: 1) alim + wifi 2) agent Blade lance 3) Chrome foreground 4) mode performance (pas economie batterie)";
}
if ($blade["online"]) { $blade["status_label"]="ACTIF"; $blade["color"]="teal"; $blade["recommendation"]="OK - Yacine accessible. Chrome sessions cookies persistants."; }
elseif ($blade["heartbeat_age_s"] < 0) { $blade["status_label"]="JAMAIS VU"; $blade["color"]="red"; $blade["recommendation"]="Installer/demarrer agent Blade v2.0 sur PC Yacine"; }
elseif ($blade["heartbeat_age_s"] < 600) { $blade["status_label"]="RECENT SILENCE"; $blade["color"]="orange"; $blade["recommendation"]="PC probablement en veille courte. Auto-harden s'activera au retour."; }
else { $blade["status_label"]="LONGUE VEILLE"; $blade["color"]="red"; $blade["recommendation"]="PC eteint ou mode avion. Auto-fallback CDP local actif."; }
$out["sections"]["blade"] = $blade;
// === SECTION 2: TASKS STATS ===
// === SECTION 2: TASKS STATS + TIMELINE 24h ===
$tasks = glob("/var/www/html/api/blade-tasks/task_*.json");
$stats = ["total" => count($tasks), "pending" => 0, "dispatched" => 0, "done" => 0, "failed" => 0, "failed_timeout" => 0, "stale" => 0];
$stats = ["total"=>count($tasks),"pending"=>0,"dispatched"=>0,"done"=>0,"failed"=>0,"failed_timeout"=>0,"stale"=>0];
$now = time();
$bucket_hours = 24;
$buckets = array_fill(0, $bucket_hours, ["done"=>0,"failed"=>0,"pending"=>0]);
$recent_tasks = [];
foreach ($tasks as $t) {
$d = @json_decode(file_get_contents($t), true);
if (!$d) continue;
$s = $d["status"] ?? "?";
if (isset($stats[$s])) $stats[$s]++;
$cts = strtotime($d["created"] ?? "") ?: 0;
if ($cts && ($now - $cts) < ($bucket_hours * 3600)) {
$hrs_ago = floor(($now - $cts) / 3600);
$idx = $bucket_hours - 1 - $hrs_ago;
if ($idx >= 0 && $idx < $bucket_hours) {
if ($s === "done") $buckets[$idx]["done"]++;
elseif (strpos($s, "failed") === 0) $buckets[$idx]["failed"]++;
elseif ($s === "dispatched" || $s === "pending") $buckets[$idx]["pending"]++;
}
}
if ($s === "dispatched") {
$da = strtotime($d["dispatched_at"] ?? "");
if ($da && ($now - $da) > 90) $stats["stale"]++;
}
if ($cts && count($recent_tasks) < 10) $recent_tasks[] = [
"id"=>$d["id"]??"?","status"=>$s,"label"=>$d["label"]??"?",
"cmd"=>substr($d["cmd"]??"",0,60),"age_s"=>$now-$cts,"created"=>$d["created"]??""
];
}
usort($recent_tasks, fn($a,$b)=>$a["age_s"]-$b["age_s"]);
$recent_tasks = array_slice($recent_tasks, 0, 10);
$out["sections"]["tasks"] = $stats;
$out["sections"]["tasks_timeline_24h"] = $buckets;
$out["sections"]["tasks_recent"] = $recent_tasks;
// === SECTION 3: CDP LOCAL S204 8 PROVIDERS ===
// === SECTION 3: CDP LOCAL ===
$cdp = [];
$ch = curl_init("http://127.0.0.1/api/cdp-status.php");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => ["Host: weval-consulting.com"]
]);
$cdp_raw = curl_exec($ch);
curl_close($ch);
if ($cdp_raw) {
$cdp_data = @json_decode($cdp_raw, true);
if ($cdp_data && isset($cdp_data["providers"])) {
$cdp = $cdp_data;
}
}
$out["sections"]["cdp_local"] = $cdp ?: ["err" => "cdp_api_down"];
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5,CURLOPT_HTTPHEADER=>["Host: weval-consulting.com"]]);
$cdp_raw = curl_exec($ch); curl_close($ch);
if ($cdp_raw) { $cdp_data = @json_decode($cdp_raw, true); if ($cdp_data && isset($cdp_data["providers"])) $cdp = $cdp_data; }
$out["sections"]["cdp_local"] = $cdp ?: ["err"=>"cdp_api_down"];
// === SECTION 4: INTENTS AVAILABLE ===
// === SECTION 4: INTENTS ===
$nl_count = 0;
$nl_f = "/opt/wevia-brain/priority-intents-nl.json";
if (file_exists($nl_f)) {
$nl_data = @json_decode(file_get_contents($nl_f), true) ?: [];
$nl_count = count($nl_data);
}
if (file_exists($nl_f)) $nl_count = count(@json_decode(file_get_contents($nl_f), true) ?: []);
$opus4_count = count(glob("/var/www/html/api/wired-pending/intent-opus4-*.php") ?: []);
$out["sections"]["intents"] = ["nl_priority" => $nl_count, "opus4_wired" => $opus4_count, "total" => $nl_count + $opus4_count];
$out["sections"]["intents"] = ["nl_priority"=>$nl_count,"opus4_wired"=>$opus4_count,"total"=>$nl_count+$opus4_count];
// === SECTION 5: RECOMMENDATIONS ZERO MANUEL ===
// === SECTION 5: S204 METRICS ===
$metrics = [];
$load = @file_get_contents("/proc/loadavg");
if ($load) { $p = explode(" ", $load); $metrics["load"] = ["1m"=>floatval($p[0]??0),"5m"=>floatval($p[1]??0),"15m"=>floatval($p[2]??0)]; }
$df = @shell_exec("df -B1 / | tail -1");
if ($df) { $p = preg_split("/\s+/", trim($df)); $metrics["disk"] = ["total_gb"=>round($p[1]/1e9,1),"used_gb"=>round($p[2]/1e9,1),"avail_gb"=>round($p[3]/1e9,1),"pct"=>intval(rtrim($p[4],"%"))]; }
$mem = @file_get_contents("/proc/meminfo");
if ($mem) { preg_match("/MemTotal:\s+(\d+)/",$mem,$mt); preg_match("/MemAvailable:\s+(\d+)/",$mem,$ma);
if ($mt && $ma) $metrics["mem"] = ["total_gb"=>round($mt[1]/1e6,1),"avail_gb"=>round($ma[1]/1e6,1),"used_pct"=>100-intval($ma[1]*100/$mt[1])]; }
$metrics["chromes"] = intval(trim(@shell_exec("pgrep -cf 'remote-debugging-port' 2>/dev/null") ?: "0"));
$metrics["fpm"] = intval(trim(@shell_exec("pgrep -c php-fpm 2>/dev/null") ?: "0"));
$out["sections"]["s204"] = $metrics;
// === SECTION 6: RECENT JOBS (async-exec) ===
$jobs = glob("/tmp/wevia-job-*.log");
usort($jobs, fn($a,$b)=>filemtime($b)-filemtime($a));
$recent_jobs = [];
foreach (array_slice($jobs, 0, 8) as $j) {
$size = filesize($j);
$mt = filemtime($j);
$id = basename($j, ".log");
$content = $size > 0 ? @file_get_contents($j, false, null, 0, 400) : "";
$recent_jobs[] = ["id"=>$id,"age_s"=>$now-$mt,"size"=>$size,"preview"=>substr($content,0,300)];
}
$out["sections"]["jobs_recent"] = $recent_jobs;
// === SECTION 7: RECOMMENDATIONS ===
$recs = [];
if (!$blade["online"]) {
$recs[] = ["priority" => "high", "text" => "Fallback: utiliser ask_<provider>_web (CDP local S204)"];
if (isset($cdp["summary"]["running"]) && $cdp["summary"]["running"] == 0) {
$recs[] = ["priority" => "high", "text" => "Lancer 'launch_chromes_all' dans WEVIA chat pour activer les 8 profiles CDP"];
}
$recs[] = ["priority"=>"high","text"=>"Blade offline: auto-fallback CDP local actif. Lancez launch_chromes_all si pas encore fait."];
} else {
$recs[] = ["priority"=>"low","text"=>"Blade actif: ask_blade_<provider> pret. Auto-harden se lance toutes les 5min."];
}
if ($stats["stale"] > 0) $recs[] = ["priority" => "med", "text" => "Cron auto recovery 2min traite. Taper 'blade_tasks_recover' pour force reset immediat"];
if ($blade["online"] && isset($cdp["summary"]["running"]) && $cdp["summary"]["running"] > 0) {
$recs[] = ["priority" => "low", "text" => "TOUT OPERATIONNEL: ask_blade_<provider> ET ask_<provider>_web disponibles"];
if ($stats["stale"] > 0) $recs[] = ["priority"=>"med","text"=>"{$stats['stale']} tasks stale. Cron recovery 2min traite. Force: blade_tasks_recover"];
if (isset($cdp["summary"]["running"]) && $cdp["summary"]["running"] == 0) {
$recs[] = ["priority"=>"med","text"=>"0/8 CDP local running. Tapez 'launch_chromes_all' pour activer fallback."];
}
if (isset($metrics["load"]["5m"]) && $metrics["load"]["5m"] > 40) {
$recs[] = ["priority"=>"med","text"=>"Load S204 haut ({$metrics['load']['5m']}). Tapez 'disaster_clean all' pour decharger."];
}
if (isset($metrics["disk"]["pct"]) && $metrics["disk"]["pct"] > 85) {
$recs[] = ["priority"=>"high","text"=>"Disk S204 a {$metrics['disk']['pct']}%. Tapez 'deep_clean' pour cleanup agressif."];
}
$out["sections"]["recommendations"] = $recs;

View File

@@ -3,114 +3,170 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WEVIA Web IA Health — Zero Manuel Dashboard</title>
<title>WEVIA Web IA Health — Command Center</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js"></script>
<style>
:root{
--bg:#0a0e1a;--bg2:#11162a;--bg3:#1a1f36;
--fg:#e2e8f0;--dim:#94a3b8;--dim2:#64748b;
--teal:#00e5a0;--yellow:#f4c430;--red:#ef4444;--orange:#f59e0b;--purple:#a28fff;
:root{--bg:#0a0e1a;--bg2:#11162a;--bg3:#1a1f36;--fg:#e2e8f0;--dim:#94a3b8;--dim2:#64748b;
--teal:#00e5a0;--yellow:#f4c430;--red:#ef4444;--orange:#f59e0b;--purple:#a28fff;--cyan:#22d3ee;
--bd:rgba(255,255,255,0.08);--bd2:rgba(255,255,255,0.04);
--mf:"JetBrains Mono","SF Mono",monospace;
--serif:"Playfair Display",Georgia,serif;
}
--mf:"JetBrains Mono","SF Mono",monospace;--serif:"Playfair Display",Georgia,serif}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--fg);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;min-height:100vh;padding:20px;line-height:1.5}
body{background:var(--bg);color:var(--fg);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;min-height:100vh;padding:16px;line-height:1.5;font-size:14px}
body::before{content:"";position:fixed;top:0;left:0;width:100%;height:100%;z-index:-1;background:radial-gradient(circle at 20% 10%,rgba(0,229,160,0.06),transparent 50%),radial-gradient(circle at 80% 90%,rgba(162,143,255,0.05),transparent 50%)}
.hdr{max-width:1400px;margin:0 auto 28px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:16px}
.title{font-family:var(--serif);font-size:28px;font-weight:700;letter-spacing:-0.5px}
.hdr{max-width:1500px;margin:0 auto 20px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:14px}
.title{font-family:var(--serif);font-size:26px;font-weight:700;letter-spacing:-0.5px}
.title .accent{color:var(--teal)}
.meta{display:flex;gap:12px;align-items:center;font-family:var(--mf);font-size:11px;color:var(--dim)}
.badge{padding:4px 10px;border-radius:999px;background:var(--bg2);border:1px solid var(--bd);font-family:var(--mf);font-size:10px;letter-spacing:0.5px}
.meta{display:flex;gap:8px;flex-wrap:wrap;margin-top:6px}
.badge{padding:4px 10px;border-radius:999px;background:var(--bg2);border:1px solid var(--bd);font-family:var(--mf);font-size:10px;letter-spacing:0.4px}
.badge.ok{color:var(--teal);border-color:rgba(0,229,160,0.3)}
.badge.warn{color:var(--orange);border-color:rgba(245,158,11,0.3)}
.badge.err{color:var(--red);border-color:rgba(239,68,68,0.3)}
.grid{max-width:1400px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:16px}
.card{background:var(--bg2);border:1px solid var(--bd);border-radius:14px;padding:22px;position:relative;overflow:hidden}
.grid{max-width:1500px;margin:0 auto;display:grid;grid-template-columns:repeat(12,1fr);gap:14px}
.card{background:var(--bg2);border:1px solid var(--bd);border-radius:14px;padding:18px;position:relative;overflow:hidden}
.card::before{content:"";position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,var(--teal),var(--purple));opacity:0.4}
.card.hero{grid-column:1/-1}
.card-head{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:18px;gap:12px}
.card-title{font-family:var(--serif);font-size:20px;font-weight:600}
.card-sub{font-family:var(--mf);font-size:10px;color:var(--dim2);letter-spacing:0.5px;text-transform:uppercase;margin-top:4px}
.big-stat{font-family:var(--serif);font-size:42px;font-weight:700;line-height:1;margin:8px 0}
.big-stat.ok{color:var(--teal)}
.big-stat.warn{color:var(--orange)}
.big-stat.err{color:var(--red)}
.kv{display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--bd2);font-family:var(--mf);font-size:12px}
.kv:last-child{border-bottom:none}
.kv .k{color:var(--dim)}
.kv .v{color:var(--fg);font-weight:600}
.rec{padding:10px 14px;border-radius:8px;background:var(--bg3);border-left:3px solid var(--teal);margin-top:10px;font-size:13px;line-height:1.5}
.col-12{grid-column:span 12}.col-8{grid-column:span 8}.col-6{grid-column:span 6}.col-4{grid-column:span 4}.col-3{grid-column:span 3}
@media(max-width:1024px){.col-8,.col-6,.col-4,.col-3{grid-column:span 12}}
.card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;gap:10px}
.card-title{font-family:var(--serif);font-size:18px;font-weight:600}
.card-sub{font-family:var(--mf);font-size:9px;color:var(--dim2);letter-spacing:0.5px;text-transform:uppercase;margin-top:3px}
.big-stat{font-family:var(--serif);font-size:36px;font-weight:700;line-height:1;margin:4px 0}
.big-stat.ok{color:var(--teal)}.big-stat.warn{color:var(--orange)}.big-stat.err{color:var(--red)}
.kv{display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--bd2);font-family:var(--mf);font-size:11px}
.kv:last-child{border-bottom:none}.kv .k{color:var(--dim)}.kv .v{color:var(--fg);font-weight:600}
.rec{padding:9px 12px;border-radius:8px;background:var(--bg3);border-left:3px solid var(--teal);margin-top:8px;font-size:12px;line-height:1.5}
.rec.high{border-left-color:var(--red);background:rgba(239,68,68,0.08)}
.rec.med{border-left-color:var(--orange);background:rgba(245,158,11,0.06)}
.rec.low{border-left-color:var(--teal);background:rgba(0,229,160,0.06)}
.rec .lbl{font-family:var(--mf);font-size:9px;text-transform:uppercase;color:var(--dim);letter-spacing:1px;margin-bottom:4px}
.provider-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-top:12px}
.provider-tile{background:var(--bg3);border:1px solid var(--bd2);border-radius:8px;padding:10px;text-align:center}
.provider-tile .icon{font-size:18px;margin-bottom:4px}
.provider-tile .name{font-family:var(--mf);font-size:10px;color:var(--dim);text-transform:uppercase}
.provider-tile .port{font-family:var(--mf);font-size:9px;color:var(--dim2);margin-top:2px}
.provider-tile.running{border-color:rgba(0,229,160,0.3)}
.rec .lbl{font-family:var(--mf);font-size:8px;text-transform:uppercase;color:var(--dim);letter-spacing:1px;margin-bottom:3px}
.provider-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-top:10px}
.provider-tile{background:var(--bg3);border:1px solid var(--bd2);border-radius:8px;padding:8px;text-align:center;cursor:pointer;transition:.2s}
.provider-tile:hover{border-color:var(--teal)}
.provider-tile .icon{font-size:16px}.provider-tile .name{font-family:var(--mf);font-size:9px;color:var(--dim);text-transform:uppercase;margin-top:3px}
.provider-tile.running{border-color:rgba(0,229,160,0.3);background:rgba(0,229,160,0.05)}
.provider-tile.running .name{color:var(--teal)}
.actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
.btn{padding:8px 14px;border-radius:8px;background:var(--bg3);border:1px solid var(--bd);color:var(--fg);cursor:pointer;font-family:var(--mf);font-size:11px;transition:.2s;text-decoration:none;display:inline-flex;align-items:center;gap:6px}
.btn{padding:7px 12px;border-radius:8px;background:var(--bg3);border:1px solid var(--bd);color:var(--fg);cursor:pointer;font-family:var(--mf);font-size:10px;transition:.2s;text-decoration:none;display:inline-flex;align-items:center;gap:5px;letter-spacing:0.3px;white-space:nowrap}
.btn:hover{background:var(--teal);color:var(--bg);border-color:var(--teal)}
.btn.primary{background:var(--teal);color:var(--bg);font-weight:600}
.btn.primary:hover{background:var(--yellow)}
.footer{max-width:1400px;margin:32px auto 0;padding:16px 0;text-align:center;font-family:var(--mf);font-size:10px;color:var(--dim2);border-top:1px solid var(--bd)}
.checklist{margin-top:14px;padding:14px;background:var(--bg3);border-radius:10px;border:1px dashed var(--bd)}
.checklist .title{font-family:var(--mf);font-size:11px;text-transform:uppercase;color:var(--yellow);letter-spacing:0.8px;margin-bottom:10px}
.checklist ul{list-style:none;font-size:12px;line-height:1.8}
.btn.primary{background:var(--teal);color:var(--bg);font-weight:700}.btn.primary:hover{background:var(--yellow)}
.btn.danger{border-color:rgba(239,68,68,0.4);color:var(--red)}.btn.danger:hover{background:var(--red);color:#fff;border-color:var(--red)}
.btn.warn{border-color:rgba(245,158,11,0.4);color:var(--orange)}.btn.warn:hover{background:var(--orange);color:#000;border-color:var(--orange)}
.btn:disabled{opacity:0.5;cursor:wait}
.actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
.chart-wrap{position:relative;height:180px;margin-top:10px}
.mini-chart{height:80px}
.gauge{width:120px;height:120px;margin:0 auto;position:relative}
.gauge svg{transform:rotate(-90deg)}
.gauge .track{stroke:var(--bg3);stroke-width:10;fill:none}
.gauge .fill{stroke-width:10;fill:none;stroke-linecap:round;transition:stroke-dashoffset .6s}
.gauge-label{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;font-family:var(--mf)}
.gauge-label .num{font-size:22px;font-weight:700}
.gauge-label .unit{font-size:9px;color:var(--dim);text-transform:uppercase;margin-top:2px}
.feed{max-height:280px;overflow-y:auto;margin-top:10px}
.feed-item{padding:8px 10px;border-radius:6px;background:var(--bg3);margin-bottom:5px;font-family:var(--mf);font-size:10px;display:flex;gap:8px;align-items:center}
.feed-item .st{width:60px;font-weight:700;text-align:center;padding:2px 4px;border-radius:3px}
.feed-item .st.done{color:var(--teal);background:rgba(0,229,160,0.1)}
.feed-item .st.failed{color:var(--red);background:rgba(239,68,68,0.1)}
.feed-item .st.failed_timeout{color:var(--red);background:rgba(239,68,68,0.1)}
.feed-item .st.dispatched{color:var(--orange);background:rgba(245,158,11,0.1)}
.feed-item .st.pending{color:var(--yellow);background:rgba(244,196,48,0.1)}
.feed-item .lbl{color:var(--dim);min-width:70px}
.feed-item .cmd{color:var(--fg);opacity:0.7;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.feed-item .age{color:var(--dim2);min-width:50px;text-align:right}
.ask-widget{background:var(--bg3);border-radius:10px;padding:12px;margin-top:10px}
.ask-row{display:flex;gap:6px;margin-bottom:6px;flex-wrap:wrap}
.ask-row select,.ask-row input{flex:1;min-width:120px;padding:8px 10px;border-radius:6px;background:var(--bg2);border:1px solid var(--bd);color:var(--fg);font-family:var(--mf);font-size:11px}
.ask-row textarea{width:100%;padding:8px 10px;border-radius:6px;background:var(--bg2);border:1px solid var(--bd);color:var(--fg);font-family:var(--mf);font-size:11px;resize:vertical;min-height:60px}
.ask-out{margin-top:8px;padding:10px;background:var(--bg);border-radius:6px;font-family:var(--mf);font-size:10px;color:var(--dim);max-height:200px;overflow-y:auto;white-space:pre-wrap;line-height:1.5;display:none}
.ask-out.show{display:block}
.checklist{margin-top:12px;padding:12px;background:var(--bg3);border-radius:10px;border:1px dashed var(--bd)}
.checklist .title{font-family:var(--mf);font-size:10px;text-transform:uppercase;color:var(--yellow);letter-spacing:0.7px;margin-bottom:8px}
.checklist ul{list-style:none;font-size:11px;line-height:1.8}
.checklist li::before{content:"▸ ";color:var(--teal)}
.status-pulse{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;box-shadow:0 0 0 0 currentColor;animation:pulse 2s infinite}
@keyframes pulse{0%{box-shadow:0 0 0 0 currentColor}70%{box-shadow:0 0 0 8px transparent}100%{box-shadow:0 0 0 0 transparent}}
.refresh-info{margin-left:auto;font-family:var(--mf);font-size:10px;color:var(--dim2)}
.loading{opacity:0.4;filter:blur(2px);pointer-events:none}
.status-pulse{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:5px;box-shadow:0 0 0 0 currentColor;animation:pulse 2s infinite}
@keyframes pulse{0%{box-shadow:0 0 0 0 currentColor}70%{box-shadow:0 0 0 7px transparent}100%{box-shadow:0 0 0 0 transparent}}
.footer{max-width:1500px;margin:24px auto 0;padding:14px 0;text-align:center;font-family:var(--mf);font-size:9px;color:var(--dim2);border-top:1px solid var(--bd)}
.toast{position:fixed;bottom:20px;right:20px;padding:12px 18px;border-radius:10px;background:var(--bg2);border:1px solid var(--teal);color:var(--teal);font-family:var(--mf);font-size:12px;box-shadow:0 8px 24px rgba(0,0,0,0.4);z-index:100;transition:transform .3s;transform:translateY(200%)}
.toast.show{transform:translateY(0)}
.toast.err{border-color:var(--red);color:var(--red)}
.loading{opacity:0.4;filter:blur(1px);pointer-events:none}
.metric-row{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:8px}
.metric-box{background:var(--bg3);padding:10px;border-radius:8px;text-align:center}
.metric-box .v{font-family:var(--mf);font-size:18px;font-weight:700;color:var(--teal)}
.metric-box .v.warn{color:var(--orange)}.metric-box .v.err{color:var(--red)}
.metric-box .l{font-family:var(--mf);font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:0.4px;margin-top:2px}
</style>
</head>
<body>
<header class="hdr">
<div>
<div class="title"><span class="accent">WEVIA</span> · Web IA Health</div>
<div class="meta" style="margin-top:6px">
<div class="title"><span class="accent">WEVIA</span> · Command Center</div>
<div class="meta">
<span class="badge" id="b-blade">Blade: —</span>
<span class="badge" id="b-cdp">CDP: —</span>
<span class="badge" id="b-tasks">Tasks: —</span>
<span class="badge ok" id="b-intents">Intents: —</span>
<span class="badge" id="b-load">Load: —</span>
</div>
</div>
<div style="display:flex;gap:10px;align-items:center">
<span class="refresh-info" id="refreshInfo">auto-refresh 30s</span>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<span style="font-family:var(--mf);font-size:9px;color:var(--dim2)">auto-refresh 30s</span>
<button class="btn" onclick="load()">↻ Refresh</button>
<a class="btn" href="/vnc-picker.html">VNC Picker</a>
<a class="btn" href="/vnc-picker.html">VNC</a>
<a class="btn" href="/wevia-master.html">WEVIA Chat</a>
</div>
</header>
<div class="grid" id="grid">
<div class="card hero">
<div style="text-align:center;padding:40px;color:var(--dim)">Chargement...</div>
</div>
<div class="card col-12" style="text-align:center;padding:40px;color:var(--dim)">Chargement...</div>
</div>
<div class="footer">
<div>WEVIA AUTONOMY <span id="f-ts"></span> · doctrines 183-189 · phase 52</div>
<div style="margin-top:6px">Zero manuel · Auto-fallback · Auto-recovery · Cron 2min · Blade + CDP redondant</div>
<div>WEVIA AUTONOMY v1.9 · phase 54 · doctrines 183-192 · <span id="f-ts"></span></div>
<div style="margin-top:4px">Zero manuel · Auto-fallback · Auto-recovery 2min · Auto-harden 5min · 221 NL + 2367 opus4</div>
</div>
<div class="toast" id="toast"></div>
<script>
const GRID = document.getElementById("grid");
let chartTimeline, chartCoverage;
function toast(msg, err){
const t = document.getElementById("toast");
t.textContent = msg;
t.className = "toast show" + (err?" err":"");
setTimeout(()=>t.className="toast",4000);
}
async function load(){
document.getElementById("grid").classList.add("loading");
GRID.classList.add("loading");
try{
const r = await fetch("/api/web-ia-health.php?_="+Date.now(),{cache:"no-store"});
const d = await r.json();
render(d);
}catch(e){
GRID.innerHTML = `<div class="card"><div class="card-title err">Erreur chargement: ${e.message}</div></div>`;
GRID.innerHTML = `<div class="card col-12"><div style="color:var(--red)">Erreur chargement: ${e.message}</div></div>`;
}
document.getElementById("grid").classList.remove("loading");
GRID.classList.remove("loading");
}
async function sendIntent(msg){
toast("Envoi: " + msg);
try{
const r = await fetch("/api/wevia-chat-v2-direct.php",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({message:msg,session_id:"dashboard-"+Date.now()})
});
const d = await r.json();
const out = d.content || d.response || d.answer || "(empty)";
toast("✓ " + (d.tool||d.intent||"OK") + " · " + Math.round(d.elapsed_ms||0) + "ms");
const modal = document.getElementById("askOut");
if (modal) {
modal.textContent = "=== " + msg + " ===\n\n" + out;
modal.classList.add("show");
}
setTimeout(load, 2000);
}catch(e){ toast("❌ "+e.message, true); }
}
function render(d){
@@ -119,149 +175,289 @@ function render(d){
const cdp = d.sections.cdp_local || {};
const ints = d.sections.intents || {};
const recs = d.sections.recommendations || [];
const timeline = d.sections.tasks_timeline_24h || [];
const recent_tasks = d.sections.tasks_recent || [];
const recent_jobs = d.sections.jobs_recent || [];
const m = d.sections.s204 || {};
// Update header badges
document.getElementById("b-blade").className = "badge " + (b.online ? "ok" : (b.heartbeat_age_s<600?"warn":"err"));
document.getElementById("b-blade").innerHTML = `<span class="status-pulse" style="color:${b.color==='teal'?'var(--teal)':b.color==='orange'?'var(--orange)':'var(--red)'}"></span>Blade: ${b.status_label||'?'}`;
const cdp_ok = cdp.summary ? cdp.summary.running : 0;
const cdp_tot = cdp.summary ? cdp.summary.total : 8;
// Header badges
document.getElementById("b-blade").className = "badge " + (b.online?"ok":(b.heartbeat_age_s<600?"warn":"err"));
const bColorVar = b.color==='teal'?'var(--teal)':b.color==='orange'?'var(--orange)':'var(--red)';
document.getElementById("b-blade").innerHTML = `<span class="status-pulse" style="color:${bColorVar}"></span>Blade: ${b.status_label||'?'}`;
const cdp_ok = cdp.summary?cdp.summary.running:0;
const cdp_tot = cdp.summary?cdp.summary.total:8;
document.getElementById("b-cdp").className = "badge " + (cdp_ok>0?"ok":"warn");
document.getElementById("b-cdp").innerHTML = `CDP local: ${cdp_ok}/${cdp_tot}`;
document.getElementById("b-tasks").innerHTML = `Tasks: ${t.done||0} done · ${t.stale||0} stale · ${t.dispatched||0} running`;
document.getElementById("b-intents").innerHTML = `Intents: ${ints.total||0}`;
document.getElementById("b-cdp").innerHTML = `CDP: ${cdp_ok}/${cdp_tot}`;
document.getElementById("b-tasks").innerHTML = `Tasks: ${t.done||0} done · ${t.stale||0} stale`;
document.getElementById("b-intents").innerHTML = `${ints.total||0} intents`;
const load5 = m.load?m.load["5m"]:0;
document.getElementById("b-load").className = "badge " + (load5<20?"ok":load5<50?"warn":"err");
document.getElementById("b-load").innerHTML = `Load: ${load5}`;
document.getElementById("f-ts").textContent = d.ts || "";
// === Build cards ===
let html = "";
// HERO BLADE CARD
// === S204 METRICS STRIP ===
const disk_pct = m.disk?m.disk.pct:0;
const mem_pct = m.mem?m.mem.used_pct:0;
html += `
<div class="card col-12">
<div class="card-head">
<div><div class="card-title">📊 S204 Metrics (live)</div><div class="card-sub">Infrastructure serveur</div></div>
<div class="actions">
<button class="btn warn" onclick="sendIntent('disk_top')">Audit Disk</button>
<button class="btn warn" onclick="sendIntent('disaster_clean all')">Disaster Clean</button>
<button class="btn danger" onclick="sendIntent('deep_clean')">Deep Clean</button>
</div>
</div>
<div class="metric-row">
<div class="metric-box"><div class="v ${load5<20?'':load5<50?'warn':'err'}">${load5}</div><div class="l">Load 5min</div></div>
<div class="metric-box"><div class="v ${disk_pct<80?'':disk_pct<90?'warn':'err'}">${disk_pct}%</div><div class="l">Disk used</div></div>
<div class="metric-box"><div class="v ${mem_pct<70?'':mem_pct<85?'warn':'err'}">${mem_pct}%</div><div class="l">Mem used</div></div>
<div class="metric-box"><div class="v">${m.chromes||0}</div><div class="l">Chromes</div></div>
</div>
</div>`;
// === BLADE HERO ===
const hb_min = Math.floor((b.heartbeat_age_s||0)/60);
const hb_sec = (b.heartbeat_age_s||0) % 60;
const hb_txt = b.heartbeat_age_s<60 ? `${b.heartbeat_age_s}s` : `${hb_min}m ${hb_sec}s`;
const hb_txt = b.heartbeat_age_s<60 ? `${b.heartbeat_age_s}s` : (b.heartbeat_age_s<3600 ? `${hb_min}m ${hb_sec}s` : `${Math.floor(hb_min/60)}h ${hb_min%60}m`);
html += `
<div class="card hero">
<div class="card col-8">
<div class="card-head">
<div>
<div class="card-title">🖥️ Agent Blade (PC Yacine)</div>
<div class="card-sub">Razer · Chrome sessions cookies persistants</div>
</div>
<div><div class="card-title">🖥️ Agent Blade (PC Yacine)</div><div class="card-sub">Razer · Chrome cookies persistants · ZERO manuel</div></div>
<span class="badge ${b.online?'ok':(b.heartbeat_age_s<600?'warn':'err')}">${b.status_label||'?'}</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px">
<div>
<div class="card-sub">Heartbeat</div>
<div class="big-stat ${b.online?'ok':(b.heartbeat_age_s<600?'warn':'err')}">${b.heartbeat_age_s>=0?hb_txt:'—'}</div>
<div class="card-sub" style="margin-top:4px">ago</div>
</div>
<div>
<div class="card-sub">IP Yacine</div>
<div style="font-family:var(--mf);font-size:14px;margin-top:8px">${b.ip||'—'}</div>
<div class="card-sub" style="margin-top:12px">Agent version</div>
<div style="font-family:var(--mf);font-size:14px;margin-top:4px">v${b.agent_version||'—'}</div>
<div class="kv"><span class="k">IP</span><span class="v">${b.ip||'—'}</span></div>
<div class="kv"><span class="k">Agent</span><span class="v">v${b.agent_version||'—'}</span></div>
<div class="kv"><span class="k">Done</span><span class="v" style="color:var(--teal)">${t.done||0}</span></div>
<div class="kv"><span class="k">Running</span><span class="v" style="color:var(--orange)">${t.dispatched||0}</span></div>
<div class="kv"><span class="k">Stale</span><span class="v" style="color:var(--red)">${t.stale||0}</span></div>
</div>
<div>
<div class="card-sub">Tasks total</div>
<div style="font-family:var(--mf);font-size:24px;font-weight:700;margin-top:6px">${t.total||0}</div>
<div style="font-size:11px;color:var(--dim);margin-top:6px">
${t.done||0} done<br>
${t.dispatched||0} running<br>
⚠️ ${t.stale||0} stale<br>
${(t.failed||0)+(t.failed_timeout||0)} failed
<div class="rec ${b.online?'low':'high'}" style="margin-top:0">
<div class="lbl">RECO AUTO</div>${b.recommendation||'—'}
</div>
</div>
<div>
<div class="card-sub">Action</div>
<div class="rec ${b.online?'low':'high'}" style="margin-top:8px">
<div class="lbl">Recommandation</div>
${b.recommendation||'—'}
<div class="actions">
<button class="btn primary" onclick="sendIntent('blade_harden')">⚡ Harden</button>
<button class="btn" onclick="sendIntent('blade_tasks_recover')">↻ Recover</button>
<button class="btn" onclick="sendIntent('blade_health')">📊 Health</button>
</div>
</div>
</div>
<div class="checklist">
<div class="title">Check-list côté PC Yacine pour zéro manuel total</div>
<div class="title">Check-list PC Yacine · zéro manuel éternel</div>
<ul>
<li>Agent Blade v2.0 lancé (processus actif au démarrage)</li>
<li>Chrome ouvert au foreground (sinon SendKeys échoue silencieusement)</li>
<li>Mode performance activé (Paramètres → Système → Alimentation → Haute perf)</li>
<li>Veille écran OK mais PAS veille hibernation (sinon agent s'arrête)</li>
<li>Wifi stable (IP Yacine: ${b.ip||'—'})</li>
<li>Agent Blade v2.0 · Windows Task Scheduler au démarrage</li>
<li>Chrome ouvert foreground · sinon SendKeys échoue</li>
<li>Mode performance · pas économie d'énergie</li>
<li>Veille écran OK · pas veille hibernation</li>
<li>Auto-harden cron S204 toutes les 5min max 1x/24h</li>
</ul>
</div>
</div>`;
// CDP LOCAL CARD
const provs = cdp.providers || [];
// === TIMELINE CHART ===
html += `
<div class="card">
<div class="card col-4">
<div class="card-head">
<div>
<div class="card-title">🌐 CDP Local S204 (Fallback)</div>
<div class="card-sub">8 profiles Chrome · ports 9222-9229</div>
</div>
<span class="badge ${cdp_ok>0?'ok':'warn'}">${cdp_ok}/${cdp_tot} running</span>
<div><div class="card-title">📈 Tasks 24h</div><div class="card-sub">Timeline activité Blade</div></div>
</div>
<div class="big-stat ${cdp_ok>0?'ok':'warn'}">${cdp.summary?cdp.summary.coverage_pct:0}%</div>
<div class="card-sub">coverage</div>
<div class="provider-grid">`;
<div class="chart-wrap"><canvas id="chartTimeline"></canvas></div>
</div>`;
// === CDP LOCAL WITH PROVIDERS ===
const provs = cdp.providers || [];
const coverage_pct = cdp.summary?cdp.summary.coverage_pct:0;
html += `
<div class="card col-6">
<div class="card-head">
<div><div class="card-title">🌐 CDP Local S204 (Fallback)</div><div class="card-sub">8 profiles Chrome · ports 9222-9229</div></div>
<span class="badge ${cdp_ok>0?'ok':'warn'}">${cdp_ok}/${cdp_tot}</span>
</div>
<div style="display:flex;gap:16px;align-items:center">
<div class="gauge">
<svg width="120" height="120" viewBox="0 0 120 120">
<circle class="track" cx="60" cy="60" r="50"/>
<circle class="fill" cx="60" cy="60" r="50" stroke="${coverage_pct>50?'var(--teal)':coverage_pct>0?'var(--orange)':'var(--red)'}" stroke-dasharray="${2*Math.PI*50}" stroke-dashoffset="${2*Math.PI*50*(1-coverage_pct/100)}"/>
</svg>
<div class="gauge-label"><div class="num">${coverage_pct}%</div><div class="unit">coverage</div></div>
</div>
<div style="flex:1">
<div class="provider-grid">`;
provs.forEach(p=>{
const running = p.status==='running';
html += `<div class="provider-tile ${running?'running':''}">
<div class="icon">${running?'✅':'❌'}</div>
<div class="name">${p.slug}</div>
<div class="port">:${p.port}</div>
html += `<div class="provider-tile ${running?'running':''}" onclick="sendIntent('ask_${p.slug==='openai'?'chatgpt':p.slug==='anthropic'?'claude':p.slug==='google'?'gemini':p.slug}_web bonjour')" title="Test ${p.name}">
<div class="icon">${running?'✅':'❌'}</div><div class="name">${p.slug}</div>
</div>`;
});
if (provs.length===0){
html += `<div style="grid-column:1/-1;text-align:center;color:var(--dim);padding:16px;font-size:12px">CDP API down lancez launch_chromes_all</div>`;
html += `<div style="grid-column:1/-1;text-align:center;color:var(--dim);padding:12px;font-size:11px">CDP API down · lancez launch_chromes_all</div>`;
}
html += `</div>
html += `</div></div></div>
<div class="actions">
<a class="btn primary" href="/wevia-master.html">Chat: launch_chromes_all</a>
<button class="btn primary" onclick="sendIntent('launch_chromes_all')">🚀 Launch 8 Chromes</button>
<button class="btn" onclick="sendIntent('chromes_status')">Status</button>
<a class="btn" href="/vnc-picker.html">VNC Picker</a>
</div>
</div>`;
// INTENTS / CAPABILITIES
// === QUICK ASK WIDGET ===
html += `
<div class="card">
<div class="card col-6">
<div class="card-head">
<div>
<div class="card-title">🧠 Capabilités WEVIA</div>
<div class="card-sub">NL priority + opus4 wired</div>
</div>
<span class="badge ok">${ints.total||0} intents</span>
<div><div class="card-title">💬 Quick Ask Any IA</div><div class="card-sub">Envoyer prompt direct via WEVIA</div></div>
</div>
<div class="kv"><span class="k">NL Priority</span><span class="v">${ints.nl_priority||0}</span></div>
<div class="kv"><span class="k">Opus4 wired</span><span class="v">${ints.opus4_wired||0}</span></div>
<div class="kv"><span class="k">Total exposed</span><span class="v">${ints.total||0}</span></div>
<div class="card-sub" style="margin-top:14px">Commandes rapides NL</div>
<div class="actions" style="margin-top:8px">
<a class="btn" href="/wevia-master.html">blade_health</a>
<a class="btn" href="/wevia-master.html">blade_tasks_recover</a>
<a class="btn" href="/wevia-master.html">chromes_status</a>
<div class="ask-widget">
<div class="ask-row">
<select id="askProvider">
<optgroup label="Via BLADE (PC Yacine, zero login)">
<option value="ask_blade_claude">Claude (Blade)</option>
<option value="ask_blade_chatgpt">ChatGPT (Blade)</option>
<option value="ask_blade_gemini">Gemini (Blade)</option>
<option value="ask_blade_deepseek">DeepSeek (Blade)</option>
<option value="ask_blade_mistral">Mistral (Blade)</option>
<option value="ask_blade_poe">Poe (Blade)</option>
<option value="ask_blade_perplexity">Perplexity (Blade)</option>
</optgroup>
<optgroup label="Via CDP Local S204 (fallback)">
<option value="ask_claude_web">Claude (CDP local)</option>
<option value="ask_chatgpt_web">ChatGPT (CDP local)</option>
<option value="ask_gemini_web">Gemini (CDP local)</option>
<option value="ask_deepseek_web">DeepSeek (CDP local)</option>
<option value="ask_mistral_web">Mistral (CDP local)</option>
</optgroup>
</select>
</div>
<div class="ask-row">
<textarea id="askPrompt" placeholder="Votre prompt..."></textarea>
</div>
<div class="actions">
<button class="btn primary" onclick="doAsk()">⚡ Send</button>
<button class="btn" onclick="document.getElementById('askOut').classList.remove('show')">Clear</button>
</div>
<div class="ask-out" id="askOut"></div>
</div>
</div>`;
// RECOMMENDATIONS
// === RECENT TASKS FEED ===
html += `
<div class="card col-6">
<div class="card-head">
<div><div class="card-title">📋 Recent Tasks (10)</div><div class="card-sub">Historique Blade</div></div>
</div>
<div class="feed">`;
recent_tasks.forEach(t=>{
const age = t.age_s<60?`${t.age_s}s`:t.age_s<3600?`${Math.floor(t.age_s/60)}m`:`${Math.floor(t.age_s/3600)}h`;
html += `<div class="feed-item">
<span class="st ${t.status}">${t.status}</span>
<span class="lbl">${t.label}</span>
<span class="cmd">${(t.cmd||'').replace(/</g,'&lt;')}</span>
<span class="age">${age}</span>
</div>`;
});
if (recent_tasks.length===0) html+=`<div style="color:var(--dim);font-size:11px;padding:10px">Aucune task récente</div>`;
html += `</div></div>`;
// === RECENT ASYNC JOBS ===
html += `
<div class="card col-6">
<div class="card-head">
<div><div class="card-title">⚙️ Async Jobs</div><div class="card-sub">Derniers résultats /tmp/wevia-job-*</div></div>
<button class="btn" onclick="sendIntent('job_list')">Refresh</button>
</div>
<div class="feed">`;
recent_jobs.forEach(j=>{
const age = j.age_s<60?`${j.age_s}s`:j.age_s<3600?`${Math.floor(j.age_s/60)}m`:`${Math.floor(j.age_s/3600)}h`;
html += `<div class="feed-item" style="flex-direction:column;align-items:stretch">
<div style="display:flex;justify-content:space-between">
<span style="color:var(--teal)">${j.id.substring(0,30)}...</span>
<span class="age">${age}</span>
</div>
<div style="color:var(--dim);font-size:9px;margin-top:4px;white-space:pre-wrap;max-height:60px;overflow:hidden">${(j.preview||'').replace(/</g,'&lt;').substring(0,200)}</div>
</div>`;
});
if (recent_jobs.length===0) html+=`<div style="color:var(--dim);font-size:11px;padding:10px">Aucun job récent</div>`;
html += `</div></div>`;
// === CAPABILITIES + RECOMMENDATIONS ===
html += `
<div class="card col-6">
<div class="card-head">
<div><div class="card-title">🧠 Capabilités WEVIA</div><div class="card-sub">NL + opus4-wired</div></div>
<span class="badge ok">${ints.total||0} total</span>
</div>
<div class="kv"><span class="k">NL Priority</span><span class="v">${ints.nl_priority||0}</span></div>
<div class="kv"><span class="k">Opus4 wired</span><span class="v">${ints.opus4_wired||0}</span></div>
<div class="card-sub" style="margin-top:12px">Actions rapides</div>
<div class="actions">
<button class="btn" onclick="sendIntent('web_ia_health')">web_ia_health</button>
<button class="btn" onclick="sendIntent('blade_status')">blade_status</button>
<button class="btn" onclick="sendIntent('chromes_status')">chromes_status</button>
<button class="btn" onclick="sendIntent('job_list')">job_list</button>
<button class="btn" onclick="sendIntent('infra_sante')">infra</button>
<button class="btn" onclick="sendIntent('disk_top')">disk_top</button>
</div>
</div>`;
html += `
<div class="card col-6">
<div class="card-head">
<div><div class="card-title">💡 Recommandations Auto</div><div class="card-sub">Analyses WEVIA contextuelles</div></div>
</div>`;
if (recs.length>0){
html += `
<div class="card">
<div class="card-head">
<div>
<div class="card-title">📋 Recommandations auto</div>
<div class="card-sub">Actions conseillées par WEVIA</div>
</div>
</div>`;
recs.forEach(r=>{
html += `<div class="rec ${r.priority||'low'}">
<div class="lbl">${(r.priority||'info').toUpperCase()}</div>
${r.text}
<div class="lbl">${(r.priority||'info').toUpperCase()}</div>${r.text}
</div>`;
});
html += `</div>`;
} else {
html += `<div style="color:var(--teal);padding:16px;text-align:center">✅ Tout optimal · aucune action requise</div>`;
}
html += `</div>`;
GRID.innerHTML = html;
// Render timeline chart
setTimeout(()=>{
const ctx = document.getElementById("chartTimeline");
if (ctx && window.Chart) {
if (chartTimeline) chartTimeline.destroy();
chartTimeline = new Chart(ctx, {
type: 'bar',
data: {
labels: timeline.map((_,i)=>(24-i-1)+'h'),
datasets: [
{label:'Done',data:timeline.map(b=>b.done),backgroundColor:'rgba(0,229,160,0.7)',stack:'x'},
{label:'Failed',data:timeline.map(b=>b.failed),backgroundColor:'rgba(239,68,68,0.7)',stack:'x'},
{label:'Pending',data:timeline.map(b=>b.pending),backgroundColor:'rgba(245,158,11,0.7)',stack:'x'}
]
},
options: {
responsive:true,maintainAspectRatio:false,
plugins:{legend:{labels:{color:'#94a3b8',font:{size:9}}}},
scales:{
x:{stacked:true,ticks:{color:'#64748b',font:{size:8}},grid:{color:'rgba(255,255,255,0.04)'}},
y:{stacked:true,ticks:{color:'#64748b',font:{size:9}},grid:{color:'rgba(255,255,255,0.04)'}}
}
}
});
}
},100);
}
function doAsk(){
const prov = document.getElementById("askProvider").value;
const prompt = document.getElementById("askPrompt").value.trim();
if (!prompt) { toast("Prompt vide", true); return; }
sendIntent(prov + " " + prompt);
}
load();