596 lines
32 KiB
HTML
596 lines
32 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>WEVAL · DG Command Center — Real-time Pilotage</title>
|
||
<style>
|
||
:root {
|
||
--bg-0:#05060a; --bg-1:#0b0d15; --bg-2:#11141f; --bg-3:#171b2a;
|
||
--border:rgba(99,102,241,0.15); --border-h:rgba(99,102,241,0.35);
|
||
--text:#e2e8f0; --dim:#94a3b8; --mute:#64748b;
|
||
--accent:#14b8a6; --accent2:#6366f1; --purple:#a855f7; --cyan:#06b6d4;
|
||
--ok:#22c55e; --warn:#f59e0b; --err:#ef4444; --rose:#f43f5e; --gold:#eab308;
|
||
}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font-family:'Inter',system-ui,sans-serif;background:radial-gradient(ellipse at top,#0f1420,#05060a 60%);color:var(--text);min-height:100vh;font-size:13px;line-height:1.5}
|
||
.container{max-width:1760px;margin:0 auto;padding:24px 28px 80px}
|
||
|
||
/* HEADER */
|
||
header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--border)}
|
||
header h1{font-size:26px;font-weight:800;background:linear-gradient(90deg,#22d3ee,#a855f7,#eab308);-webkit-background-clip:text;background-clip:text;color:transparent;letter-spacing:-0.4px;display:flex;align-items:center;gap:10px}
|
||
header .sub{color:var(--dim);font-size:12.5px;margin-top:5px}
|
||
header .clock{font-family:'JetBrains Mono',monospace;color:var(--accent);font-size:11px;margin-top:4px}
|
||
.actions{display:flex;gap:8px}
|
||
.btn{padding:7px 13px;background:var(--bg-2);border:1px solid var(--border);color:var(--text);border-radius:8px;font-size:11.5px;cursor:pointer;text-decoration:none;font-family:inherit;transition:all .2s}
|
||
.btn:hover{border-color:var(--accent);color:var(--accent)}
|
||
.pulse{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 0 rgba(34,197,94,.7);animation:pulse 2s infinite}
|
||
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(34,197,94,.7)}70%{box-shadow:0 0 0 8px rgba(34,197,94,0)}}
|
||
|
||
/* LAYOUT GRID */
|
||
.row{display:grid;gap:14px;margin-bottom:14px}
|
||
.row-4{grid-template-columns:repeat(4,1fr)}
|
||
.row-3{grid-template-columns:repeat(3,1fr)}
|
||
.row-2{grid-template-columns:2fr 1fr}
|
||
.row-2e{grid-template-columns:1fr 1fr}
|
||
@media(max-width:1200px){.row-4,.row-3,.row-2,.row-2e{grid-template-columns:1fr 1fr}}
|
||
@media(max-width:720px){.row-4,.row-3,.row-2,.row-2e{grid-template-columns:1fr}}
|
||
|
||
/* CARDS */
|
||
.card{background:var(--bg-1);border:1px solid var(--border);border-radius:12px;padding:16px;position:relative;overflow:hidden}
|
||
.card.span-2{grid-column:span 2}
|
||
.card.span-3{grid-column:span 3}
|
||
.card.span-4{grid-column:span 4}
|
||
.card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
|
||
.card-title{font-size:11.5px;color:var(--dim);text-transform:uppercase;letter-spacing:0.6px;font-weight:700;display:flex;align-items:center;gap:6px}
|
||
.card-badge{font-size:9.5px;padding:2px 7px;border-radius:8px;font-weight:700;letter-spacing:0.3px;background:rgba(20,184,166,0.15);color:#5eead4}
|
||
.card-badge.warn{background:rgba(245,158,11,.18);color:#fbbf24}
|
||
.card-badge.danger{background:rgba(239,68,68,.18);color:#fca5a5}
|
||
.card-badge.info{background:rgba(99,102,241,.18);color:#a5b4fc}
|
||
|
||
/* KPI big */
|
||
.kpi-big{font-size:32px;font-weight:800;letter-spacing:-0.5px;line-height:1}
|
||
.kpi-big.gold{background:linear-gradient(135deg,var(--gold),var(--warn));-webkit-background-clip:text;background-clip:text;color:transparent}
|
||
.kpi-big.ok{color:var(--ok)}
|
||
.kpi-big.warn{color:var(--warn)}
|
||
.kpi-big.danger{color:var(--err)}
|
||
.kpi-sub{color:var(--dim);font-size:11px;margin-top:4px}
|
||
|
||
/* ALERTS DG STRIP */
|
||
.alerts-strip{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:10px;margin-bottom:20px}
|
||
.alert-card{background:var(--bg-1);border:1px solid var(--border);border-radius:10px;padding:14px 16px;border-left:4px solid var(--warn);position:relative;transition:all .2s}
|
||
.alert-card:hover{border-color:var(--border-h);transform:translateY(-2px)}
|
||
.alert-card.critical{border-left-color:var(--err);background:linear-gradient(135deg,rgba(239,68,68,0.06),var(--bg-1))}
|
||
.alert-card.high{border-left-color:var(--warn)}
|
||
.alert-card.medium{border-left-color:var(--cyan)}
|
||
.alert-head{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:6px}
|
||
.alert-title{font-size:13px;font-weight:700;color:var(--text);display:flex;gap:7px;align-items:flex-start}
|
||
.alert-lvl{font-size:9px;padding:2px 6px;border-radius:6px;font-weight:700;letter-spacing:0.3px;text-transform:uppercase;flex-shrink:0}
|
||
.alert-lvl.critical{background:rgba(239,68,68,0.2);color:#fca5a5}
|
||
.alert-lvl.high{background:rgba(245,158,11,0.2);color:#fbbf24}
|
||
.alert-lvl.medium{background:rgba(6,182,212,0.18);color:#7dd3fc}
|
||
.alert-detail{font-size:11.5px;color:var(--dim);margin:6px 0;line-height:1.45}
|
||
.alert-foot{display:flex;justify-content:space-between;align-items:center;margin-top:8px;font-size:10.5px}
|
||
.alert-foot .deadline{color:var(--warn);font-weight:600}
|
||
.alert-foot a{color:var(--accent);text-decoration:none;font-weight:600}
|
||
|
||
/* TOC streams */
|
||
.toc-wrap{display:flex;flex-direction:column;gap:8px}
|
||
.toc-stream{display:grid;grid-template-columns:28px 1fr 60px 80px 1fr;gap:10px;align-items:center;padding:8px 10px;background:var(--bg-2);border-radius:8px;border-left:3px solid var(--dim)}
|
||
.toc-stream.bottleneck{border-left-color:var(--err);background:linear-gradient(135deg,rgba(239,68,68,0.06),var(--bg-2));box-shadow:0 0 0 1px rgba(239,68,68,0.25)}
|
||
.toc-stream.flow{border-left-color:var(--ok)}
|
||
.toc-stream.starved{border-left-color:var(--cyan)}
|
||
.toc-icon{font-size:18px;text-align:center}
|
||
.toc-label{font-size:12px;font-weight:600;color:var(--text)}
|
||
.toc-label .small{color:var(--dim);font-size:10px;font-weight:400;margin-top:2px}
|
||
.toc-throughput{font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:800;color:var(--text);text-align:center}
|
||
.toc-bar-wrap{background:var(--bg-3);height:10px;border-radius:5px;overflow:hidden}
|
||
.toc-bar-fill{height:100%;background:linear-gradient(90deg,#14b8a6,#6366f1);transition:width 1.2s cubic-bezier(.4,0,.2,1)}
|
||
.toc-bar-fill.bot{background:linear-gradient(90deg,#ef4444,#f59e0b)}
|
||
.toc-util{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--dim);text-align:right;font-weight:600}
|
||
.toc-constraint{font-size:10px;color:var(--mute);font-style:italic;grid-column:1/-1;padding-left:38px;margin-top:-3px}
|
||
|
||
/* Funnel */
|
||
.funnel-wrap{display:flex;flex-direction:column;gap:6px;align-items:center;padding:10px 0}
|
||
.funnel-row{display:grid;grid-template-columns:160px 1fr 70px 50px;gap:10px;align-items:center;width:100%;font-size:12px}
|
||
.funnel-label{color:var(--text);font-weight:500;font-size:11.5px}
|
||
.funnel-bar-wrap{background:var(--bg-3);height:28px;border-radius:4px;overflow:hidden;position:relative}
|
||
.funnel-bar{height:100%;transition:width 1.2s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;padding-left:10px;font-size:11.5px;font-weight:700;color:white}
|
||
.funnel-count{font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:700;color:var(--text);text-align:right}
|
||
.funnel-conv{font-family:'JetBrains Mono',monospace;font-size:10.5px;color:var(--dim);text-align:right}
|
||
.funnel-conv.warn{color:var(--warn)}
|
||
.funnel-conv.danger{color:var(--err)}
|
||
|
||
/* Data pipelines */
|
||
.dp-wrap{display:grid;grid-template-columns:1fr;gap:6px}
|
||
.dp-row{display:grid;grid-template-columns:1fr 80px 1fr 60px;gap:10px;align-items:center;padding:7px 10px;background:var(--bg-2);border-radius:6px;font-size:11.5px}
|
||
.dp-name{color:var(--text);font-weight:500}
|
||
.dp-vol{font-family:'JetBrains Mono',monospace;font-weight:700;text-align:right;color:var(--text)}
|
||
.dp-bar-wrap{background:var(--bg-3);height:8px;border-radius:4px;overflow:hidden}
|
||
.dp-bar-fill{height:100%;background:linear-gradient(90deg,var(--ok),var(--cyan));transition:width 1.2s}
|
||
.dp-status{font-size:10.5px;text-align:right;font-family:'JetBrains Mono',monospace}
|
||
.dp-status.ok{color:var(--ok)} .dp-status.warn{color:var(--warn)} .dp-status.danger{color:var(--err)}
|
||
|
||
/* Marketing grid */
|
||
.mkt-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
|
||
@media(max-width:900px){.mkt-grid{grid-template-columns:repeat(2,1fr)}}
|
||
.mkt-cell{background:var(--bg-2);border-radius:6px;padding:10px;border-left:2px solid var(--purple)}
|
||
.mkt-cell .l{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:0.4px;font-weight:600}
|
||
.mkt-cell .v{font-size:17px;font-weight:800;color:var(--text);font-family:'JetBrains Mono',monospace;margin-top:3px;line-height:1}
|
||
.mkt-cell .u{font-size:10.5px;color:var(--dim);margin-left:2px}
|
||
|
||
/* CRM view */
|
||
.crm-stage-row{display:grid;grid-template-columns:110px 50px 1fr 80px;gap:10px;align-items:center;padding:7px 10px;background:var(--bg-2);border-radius:6px;font-size:11.5px;margin-bottom:5px}
|
||
.stage-label{font-weight:600;color:var(--text)}
|
||
.stage-count{font-family:'JetBrains Mono',monospace;font-weight:700;color:var(--accent);text-align:center}
|
||
.stage-bar-wrap{background:var(--bg-3);height:10px;border-radius:4px;overflow:hidden}
|
||
.stage-bar-fill{height:100%;background:linear-gradient(90deg,var(--accent2),var(--purple));transition:width 1.2s}
|
||
.stage-val{font-family:'JetBrains Mono',monospace;font-weight:700;color:var(--gold);text-align:right;font-size:11px}
|
||
|
||
.accounts-wrap{display:flex;flex-direction:column;gap:5px}
|
||
.acc-row{display:grid;grid-template-columns:1fr 110px 60px;gap:10px;padding:7px 10px;background:var(--bg-2);border-radius:6px;font-size:11px;align-items:center;border-left:2px solid var(--accent2)}
|
||
.acc-row:hover{background:var(--bg-3)}
|
||
.acc-name{font-weight:600;color:var(--text)}
|
||
.acc-name .step{display:block;font-size:10px;color:var(--dim);font-weight:400;margin-top:2px}
|
||
.acc-stage{font-size:10px;color:var(--dim);font-family:'JetBrains Mono',monospace}
|
||
.acc-val{font-family:'JetBrains Mono',monospace;font-weight:700;color:var(--gold);text-align:right}
|
||
|
||
/* Risk matrix 5x5 */
|
||
.rm-wrap{display:grid;grid-template-columns:80px 1fr;gap:10px}
|
||
.rm-grid-5x5{display:grid;grid-template-columns:20px repeat(5,1fr);gap:3px}
|
||
.rm-cell{aspect-ratio:1.4;display:flex;align-items:center;justify-content:center;border-radius:4px;font-weight:800;font-size:14px;cursor:help;position:relative}
|
||
.rm-header{font-size:9px;color:var(--dim);text-align:center;display:flex;align-items:center;justify-content:center}
|
||
.rm-sev1{background:rgba(34,197,94,0.15);color:#86efac}
|
||
.rm-sev2{background:rgba(132,204,22,0.18);color:#d9f99d}
|
||
.rm-sev3{background:rgba(234,179,8,0.2);color:#fef08a}
|
||
.rm-sev4{background:rgba(249,115,22,0.22);color:#fed7aa}
|
||
.rm-sev5{background:rgba(239,68,68,0.3);color:#fca5a5}
|
||
.rm-sev-empty{background:var(--bg-3);color:var(--mute);font-weight:400;font-size:10px}
|
||
|
||
.risk-list{display:flex;flex-direction:column;gap:5px;margin-top:10px}
|
||
.risk-row{display:grid;grid-template-columns:40px 1fr auto;gap:8px;align-items:center;padding:6px 10px;background:var(--bg-2);border-radius:5px;font-size:11px;border-left:2px solid var(--warn)}
|
||
.risk-row.critical{border-left-color:var(--err)}
|
||
.risk-row.high{border-left-color:var(--warn)}
|
||
.risk-row.medium{border-left-color:var(--cyan)}
|
||
.risk-id{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--mute)}
|
||
.risk-title{color:var(--text);font-weight:500}
|
||
.risk-title .mit{display:block;font-size:10px;color:var(--dim);font-style:italic;margin-top:2px}
|
||
.risk-score{font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:800;color:var(--text);text-align:right}
|
||
|
||
.loading{text-align:center;padding:50px;color:var(--dim)}
|
||
.spinner{width:38px;height:38px;border:3px solid var(--bg-3);border-top-color:var(--accent);border-radius:50%;margin:0 auto 14px;animation:spin 1s linear infinite}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
|
||
/* === OPUS RESPONSIVE FIX v2 19avr — append-only, doctrine #14 === */
|
||
@media(max-width: 480px) {
|
||
html, body { overflow-x: hidden !important; max-width: 100vw; }
|
||
body, main, section, article { word-break: break-word; overflow-wrap: anywhere; }
|
||
img, video, iframe, canvas, svg, table, pre, code { max-width: 100% !important; }
|
||
pre, code { white-space: pre-wrap; word-break: break-all; }
|
||
table { display: block; overflow-x: auto; }
|
||
.container, [class*="container"], [class*="wrapper"] { max-width: 100vw !important; padding-left: 12px !important; padding-right: 12px !important; }
|
||
[class*="grid"], [class*="-grid"] { grid-template-columns: 1fr !important; gap: 10px !important; }
|
||
[class*="kpi"], [class*="stats"], [class*="-cards"] { grid-template-columns: 1fr !important; }
|
||
header, nav, footer { flex-wrap: wrap !important; }
|
||
header > *, nav > *, footer > * { max-width: 100%; }
|
||
h1 { font-size: 22px !important; word-break: break-word; }
|
||
h2 { font-size: 18px !important; }
|
||
.pitch, [class*="pitch"], [class*="hero"] { word-break: break-word; overflow-wrap: anywhere; }
|
||
}
|
||
/* === OPUS RESPONSIVE FIX v2 END === */
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
|
||
<header>
|
||
<div>
|
||
<h1><span>🎖️</span>DG Command Center <span class="pulse"></span></h1>
|
||
<div class="sub">Real-time pilotage — TOC · Conversion · Data · Marketing · CRM · Risk · Alertes</div>
|
||
<div class="clock" id="clock">—</div>
|
||
</div>
|
||
<div class="actions">
|
||
<a href="/weval-technology-platform.html" class="btn">🏠 WTP</a>
|
||
<a href="/agent-roi-simulator.html" class="btn">🧮 ROI Sim</a>
|
||
<a href="/crm.html" class="btn">💼 CRM</a>
|
||
<button class="btn" id="btn-refresh" onclick="load()">↻ Refresh</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- ALERTS DG TOP -->
|
||
<div class="card" style="margin-bottom:20px;border-left:4px solid var(--err)">
|
||
<div class="card-head">
|
||
<div class="card-title">🚨 Alertes DG — à traiter maintenant <span class="card-badge danger" id="alerts-count">— alertes</span></div>
|
||
<div class="card-badge" id="alerts-critical">—</div>
|
||
</div>
|
||
<div class="alerts-strip" id="alerts-strip"><div class="loading"><div class="spinner"></div></div></div>
|
||
</div>
|
||
|
||
<!-- ROW: TOC + Conversion Funnel -->
|
||
<div class="row row-2">
|
||
<div class="card" id="toc-card">
|
||
<div class="card-head">
|
||
<div class="card-title">🎯 TOC Theory of Constraints — Goldratt</div>
|
||
<div class="card-badge danger" id="toc-bot-badge">— bottleneck</div>
|
||
</div>
|
||
<div class="toc-wrap" id="toc-streams"><div class="loading"><div class="spinner"></div></div></div>
|
||
<div style="margin-top:12px;padding:10px 12px;background:var(--bg-2);border-radius:6px;font-size:10.5px;color:var(--dim);line-height:1.5">
|
||
<strong style="color:var(--accent)">5 Focusing Steps (Goldratt):</strong>
|
||
1. Identifier la contrainte · 2. Exploiter (max) · 3. Subordonner tout le reste · 4. Élever la contrainte · 5. Si brisée → reprendre au 1
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-head">
|
||
<div class="card-title">🎚️ Conversion Funnel</div>
|
||
<div class="card-badge info" id="conv-overall">— %</div>
|
||
</div>
|
||
<div class="funnel-wrap" id="funnel-wrap"><div class="loading"><div class="spinner"></div></div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ROW: Data pipelines + Marketing KPIs -->
|
||
<div class="row row-2e">
|
||
<div class="card">
|
||
<div class="card-head"><div class="card-title">🔌 Data Pipelines Health</div><div class="card-badge" id="dp-badge">live</div></div>
|
||
<div class="dp-wrap" id="dp-wrap"><div class="loading"><div class="spinner"></div></div></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-head"><div class="card-title">📣 Marketing KPIs</div><div class="card-badge info">WEVADS + Ethica</div></div>
|
||
<div class="mkt-grid" id="mkt-grid"><div class="loading"><div class="spinner"></div></div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ROW: CRM pipeline + Top accounts -->
|
||
<div class="row row-2e">
|
||
<div class="card">
|
||
<div class="card-head">
|
||
<div class="card-title">💼 CRM Pipeline by Stage</div>
|
||
<div class="card-badge info" id="pipe-val">— k€</div>
|
||
</div>
|
||
<div id="crm-stages"><div class="loading"><div class="spinner"></div></div></div>
|
||
<div style="margin-top:12px;padding-top:10px;border-top:1px dashed var(--border);display:grid;grid-template-columns:repeat(4,1fr);gap:8px;font-size:10.5px;color:var(--dim)">
|
||
<div><strong style="color:var(--text);display:block;font-size:14px;font-family:'JetBrains Mono',monospace" id="crm-opps">—</strong>Opps actives</div>
|
||
<div><strong style="color:var(--ok);display:block;font-size:14px;font-family:'JetBrains Mono',monospace" id="crm-won">—</strong>Won ce mois</div>
|
||
<div><strong style="color:var(--err);display:block;font-size:14px;font-family:'JetBrains Mono',monospace" id="crm-lost">—</strong>Lost</div>
|
||
<div><strong style="color:var(--purple);display:block;font-size:14px;font-family:'JetBrains Mono',monospace" id="crm-cycle">—</strong>Cycle (j)</div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-head"><div class="card-title">🎯 Top Accounts & Next Steps</div><div class="card-badge info" id="acc-badge">—</div></div>
|
||
<div class="accounts-wrap" id="accounts-wrap"><div class="loading"><div class="spinner"></div></div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ROW: Risk Management 5x5 + Risk list -->
|
||
<div class="row row-2e">
|
||
<div class="card">
|
||
<div class="card-head">
|
||
<div class="card-title">⚠️ Risk Management WEVAL — Matrice 5×5</div>
|
||
<div class="card-badge danger" id="risk-count">—</div>
|
||
</div>
|
||
<div class="rm-wrap">
|
||
<div style="display:flex;flex-direction:column;justify-content:space-around;font-size:10px;color:var(--dim);text-align:right;font-weight:600">
|
||
<div>Likelihood</div>
|
||
<div>L=5</div><div>L=4</div><div>L=3</div><div>L=2</div><div>L=1</div>
|
||
</div>
|
||
<div>
|
||
<div class="rm-grid-5x5" id="risk-matrix"></div>
|
||
<div style="display:grid;grid-template-columns:20px repeat(5,1fr);gap:3px;margin-top:4px">
|
||
<div></div>
|
||
<div class="rm-header">Impact 1</div><div class="rm-header">2</div><div class="rm-header">3</div><div class="rm-header">4</div><div class="rm-header">5</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-head"><div class="card-title">📋 Top 8 Risques à traiter</div><div class="card-badge danger" id="risks-prio">—</div></div>
|
||
<div class="risk-list" id="risk-list"><div class="loading"><div class="spinner"></div></div></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
function formatK(n){if(n==null)return '';if(n>=1000000)return (n/1000000).toFixed(1).replace('.0','')+'M';if(n>=1000)return (n/1000).toFixed(1).replace('.0','')+'K';return n.toLocaleString();}
|
||
|
||
const API = '/api/wevia-v69-dg-command-center.php';
|
||
let DATA = null;
|
||
|
||
function clockTick(){
|
||
const d = new Date();
|
||
document.getElementById('clock').textContent = d.toLocaleDateString('fr-FR') + ' · ' + d.toLocaleTimeString('fr-FR') + ' · auto-refresh 20s';
|
||
}
|
||
setInterval(clockTick, 1000); clockTick();
|
||
|
||
async function load(){
|
||
try {
|
||
const r = await fetch(API + '?t=' + Date.now());
|
||
DATA = await r.json();
|
||
render();
|
||
} catch(e) { console.error(e); }
|
||
}
|
||
|
||
function fmt(n){
|
||
if (!n && n !== 0) return '—';
|
||
if (Math.abs(n) >= 1000000) return (n/1000000).toFixed(2)+'M';
|
||
if (Math.abs(n) >= 1000) return (n/1000).toFixed(1)+'k';
|
||
return Math.round(n);
|
||
}
|
||
|
||
function render(){
|
||
if (!DATA) return;
|
||
const s = DATA.summary;
|
||
|
||
// Alerts
|
||
const alerts = DATA.alerts_dg || [];
|
||
document.getElementById('alerts-count').textContent = alerts.length + ' alertes';
|
||
document.getElementById('alerts-critical').textContent = s.alerts_critical + ' critical';
|
||
document.getElementById('alerts-critical').className = 'card-badge ' + (s.alerts_critical > 0 ? 'danger' : 'info');
|
||
document.getElementById('alerts-strip').innerHTML = alerts.map(a => `
|
||
<div class="alert-card ${a.level}">
|
||
<div class="alert-head">
|
||
<div class="alert-title"><span>${a.icon}</span>${a.title}</div>
|
||
<div class="alert-lvl ${a.level}">${a.level}</div>
|
||
</div>
|
||
<div class="alert-detail">${a.detail}</div>
|
||
<div class="alert-foot">
|
||
<span class="deadline">⏱ ${a.deadline}</span>
|
||
<a href="${a.action_link}">→ Action</a>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
// TOC
|
||
const streams = (DATA.toc && DATA.toc.streams) || [];
|
||
document.getElementById('toc-bot-badge').textContent = '🔴 ' + (s.toc_bottleneck_label || '—');
|
||
document.getElementById('toc-streams').innerHTML = streams.map(st => {
|
||
const isBot = st.id === DATA.toc.bottleneck;
|
||
const pct = Math.min(100, st.utilization_pct);
|
||
return `<div class="toc-stream ${st.status} ${isBot?'bottleneck':''}">
|
||
<div class="toc-icon">${st.icon}</div>
|
||
<div class="toc-label">${st.label}${isBot?' <span class="card-badge danger" style="margin-left:4px">GOULET</span>':''}<div class="small">${st.constraint}</div></div>
|
||
<div class="toc-throughput">${st.throughput}<div style="font-size:9px;color:var(--mute);font-weight:400">${st.unit}</div></div>
|
||
<div><div class="toc-bar-wrap"><div class="toc-bar-fill ${isBot?'bot':''}" style="width:0%" data-w="${pct}"></div></div></div>
|
||
<div class="toc-util">${pct.toFixed(0)}%<div style="font-size:9px;color:var(--mute)">cap ${st.capacity}</div></div>
|
||
</div>`;
|
||
}).join('');
|
||
setTimeout(()=>document.querySelectorAll('.toc-bar-fill').forEach(el=>el.style.width=el.dataset.w+'%'), 80);
|
||
|
||
// Funnel
|
||
const funnel = DATA.conversion_funnel || [];
|
||
document.getElementById('conv-overall').textContent = s.conversion_overall_pct.toFixed(3) + '% overall';
|
||
const maxCount = Math.max(...funnel.map(f=>f.count), 1);
|
||
document.getElementById('funnel-wrap').innerHTML = funnel.map((f,i) => {
|
||
const w = (f.count/maxCount)*100;
|
||
const cls = (f.conv_pct||100) < 15 ? 'danger' : (f.conv_pct||100) < 35 ? 'warn' : '';
|
||
return `<div class="funnel-row">
|
||
<div class="funnel-label">${f.step}</div>
|
||
<div class="funnel-bar-wrap"><div class="funnel-bar" style="width:0%;background:${f.color}" data-w="${w}">${formatK(f.count)}</div></div>
|
||
<div class="funnel-count">${fmt(f.count)}</div>
|
||
<div class="funnel-conv ${cls}">${f.conv_pct||100}%</div>
|
||
</div>`;
|
||
}).join('');
|
||
setTimeout(()=>document.querySelectorAll('.funnel-bar').forEach(el=>el.style.width=el.dataset.w+'%'), 100);
|
||
|
||
// Data pipelines
|
||
const dp = DATA.data_pipeline || [];
|
||
document.getElementById('dp-wrap').innerHTML = dp.map(d => {
|
||
const pct = d.target ? Math.min(100, (d.volume/d.target)*100) : 100;
|
||
return `<div class="dp-row">
|
||
<div class="dp-name">${d.name}</div>
|
||
<div class="dp-vol">${fmt(d.volume)} ${d.unit||''}</div>
|
||
<div class="dp-bar-wrap"><div class="dp-bar-fill" style="width:0%" data-w="${pct}"></div></div>
|
||
<div class="dp-status ${d.status}">${d.status}</div>
|
||
</div>`;
|
||
}).join('');
|
||
setTimeout(()=>document.querySelectorAll('.dp-bar-fill').forEach(el=>el.style.width=el.dataset.w+'%'), 120);
|
||
|
||
// Marketing
|
||
const m = DATA.marketing || {};
|
||
const mktCells = [
|
||
{l:'HCPs Maghreb', v:fmt(m.ethica_hcps), u:''},
|
||
{l:'Emails valides', v:fmt(m.emails_validated), u:''},
|
||
{l:'Warmup accts', v:fmt(m.warmup_accounts), u:''},
|
||
{l:'Seeds actifs', v:fmt(m.seed_accounts), u:''},
|
||
{l:'Inbox rate', v:m.inbox_rate_pct, u:'%'},
|
||
{l:'Open rate', v:m.open_rate_pct, u:'%'},
|
||
{l:'Click rate', v:m.click_rate_pct, u:'%'},
|
||
{l:'Conversions', v:m.conversions_month, u:'/mois'},
|
||
{l:'CAC', v:m.cac_eur, u:'€'},
|
||
{l:'LTV', v:m.ltv_eur, u:'€'},
|
||
{l:'Deliver. mean wk', v:m.email_deliverability_mean_week, u:'%'},
|
||
{l:'Campaigns live', v:2, u:''}
|
||
];
|
||
document.getElementById('mkt-grid').innerHTML = mktCells.map(c => `<div class="mkt-cell"><div class="l">${c.l}</div><div class="v">${c.v}<span class="u">${c.u}</span></div></div>`).join('');
|
||
|
||
// CRM pipeline by stage
|
||
const crm = DATA.crm || {};
|
||
const stages = crm.pipeline_by_stage || [];
|
||
document.getElementById('pipe-val').textContent = fmt(crm.pipeline_value_keur) + ' k€';
|
||
document.getElementById('crm-opps').textContent = crm.opportunities_active;
|
||
document.getElementById('crm-won').textContent = crm.deals_won_month;
|
||
document.getElementById('crm-lost').textContent = crm.deals_lost_month;
|
||
document.getElementById('crm-cycle').textContent = crm.avg_cycle_days;
|
||
const maxVal = Math.max(...stages.map(s=>s.value_keur), 1);
|
||
document.getElementById('crm-stages').innerHTML = stages.map(st => {
|
||
const w = (st.value_keur/maxVal)*100;
|
||
return `<div class="crm-stage-row">
|
||
<div class="stage-label">${st.stage}</div>
|
||
<div class="stage-count">${formatK(st.count)}</div>
|
||
<div><div class="stage-bar-wrap"><div class="stage-bar-fill" style="width:0%" data-w="${w}"></div></div></div>
|
||
<div class="stage-val">${fmt(st.value_keur)} k€</div>
|
||
</div>`;
|
||
}).join('');
|
||
setTimeout(()=>document.querySelectorAll('.stage-bar-fill').forEach(el=>el.style.width=el.dataset.w+'%'), 130);
|
||
|
||
// Top accounts
|
||
const accs = crm.top_accounts || [];
|
||
document.getElementById('acc-badge').textContent = accs.length + ' accounts';
|
||
document.getElementById('accounts-wrap').innerHTML = accs.map(a => `
|
||
<div class="acc-row">
|
||
<div class="acc-name">${a.name}<span class="step">→ ${a.next_step}</span></div>
|
||
<div class="acc-stage">${a.stage}</div>
|
||
<div class="acc-val">${a.value_keur ? fmt(a.value_keur)+'k€' : '—'}</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
// Risk matrix 5x5
|
||
const risks = DATA.risks || [];
|
||
document.getElementById('risk-count').textContent = s.risks_critical + ' critical · ' + s.risks_high + ' high';
|
||
const grid = {};
|
||
risks.forEach(r => { const k=r.likelihood+'_'+r.impact; grid[k]=(grid[k]||[]); grid[k].push(r); });
|
||
let rmHtml = '<div></div>'; // corner top-left
|
||
for (let imp=1; imp<=5; imp++) rmHtml += `<div class="rm-header" style="height:14px"></div>`; // column headers actually placed below
|
||
// Rows L=5 → L=1 (high likelihood at top)
|
||
rmHtml = '';
|
||
for (let l=5; l>=1; l--) {
|
||
rmHtml += `<div class="rm-header" style="font-size:10px">L=${l}</div>`;
|
||
for (let i=1; i<=5; i++) {
|
||
const cells = grid[l+'_'+i] || [];
|
||
const sev = l*i;
|
||
let cls = 'rm-sev-empty';
|
||
if (sev >= 20) cls = 'rm-sev5';
|
||
else if (sev >= 15) cls = 'rm-sev5';
|
||
else if (sev >= 10) cls = 'rm-sev4';
|
||
else if (sev >= 6) cls = 'rm-sev3';
|
||
else if (sev >= 3) cls = 'rm-sev2';
|
||
else cls = 'rm-sev1';
|
||
rmHtml += `<div class="rm-cell ${cls}" title="${cells.map(c=>c.id+': '+c.title).join(' · ')}">${cells.length || '·'}</div>`;
|
||
}
|
||
}
|
||
document.getElementById('risk-matrix').innerHTML = rmHtml;
|
||
|
||
// Risk list (top 8 by severity)
|
||
const sorted = [...risks].sort((a,b) => (b.likelihood*b.impact) - (a.likelihood*a.impact)).slice(0, 8);
|
||
document.getElementById('risks-prio').textContent = risks.length + ' risques total';
|
||
document.getElementById('risk-list').innerHTML = sorted.map(r => `
|
||
<div class="risk-row ${r.priority}">
|
||
<div class="risk-id">${r.id}</div>
|
||
<div class="risk-title">${r.title}<span class="mit">🛡 ${r.mitigation}</span></div>
|
||
<div class="risk-score">${r.likelihood}×${r.impact}=<strong>${r.likelihood*r.impact}</strong></div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
load();
|
||
setInterval(load, 20000);
|
||
</script>
|
||
<script>
|
||
/* V75 AVATAR UNIFIER — Meeting-rooms emoji style (Opus 19avr) */
|
||
(function() {
|
||
if (window.__WEVAL_AVATAR_V75) return;
|
||
window.__WEVAL_AVATAR_V75 = true;
|
||
const REG_URL = '/api/agent-avatars-v75.json';
|
||
const SVG_EP = '/api/agent-avatar-svg.php';
|
||
function emojiSVGUrl(name, emoji) {
|
||
return SVG_EP + '?n=' + encodeURIComponent(name) + '&e=' + encodeURIComponent(emoji);
|
||
}
|
||
fetch(REG_URL + '?t=' + Date.now()).then(r => r.json()).then(REG => {
|
||
function getAvatarUrl(name) {
|
||
const rec = REG[name];
|
||
if (!rec) return null;
|
||
if (typeof rec === 'object' && rec.svg) return rec.svg;
|
||
if (typeof rec === 'object' && rec.emoji) return emojiSVGUrl(name, rec.emoji);
|
||
return typeof rec === 'string' ? rec : null;
|
||
}
|
||
function findCI(key) {
|
||
const lower = key.toLowerCase();
|
||
for (const k of Object.keys(REG)) if (k.toLowerCase() === lower) return k;
|
||
return null;
|
||
}
|
||
function apply() {
|
||
document.querySelectorAll('img').forEach(img => {
|
||
const key = img.alt || img.dataset.agent || img.dataset.name || img.title || '';
|
||
if (!key) return;
|
||
let url = getAvatarUrl(key);
|
||
if (!url) { const alt = findCI(key); if (alt) url = getAvatarUrl(alt); }
|
||
if (url && img.src !== url && !img.src.endsWith(url)) {
|
||
img.src = url;
|
||
img.setAttribute('data-weval-v75', '1');
|
||
}
|
||
});
|
||
document.querySelectorAll('[data-agent]:not([data-weval-v75-applied])').forEach(el => {
|
||
const name = el.dataset.agent;
|
||
const url = getAvatarUrl(name);
|
||
if (!url) return;
|
||
const img = document.createElement('img');
|
||
img.src = url; img.alt = name; img.title = name;
|
||
img.className = 'v75-avatar';
|
||
img.style.cssText = 'width:32px;height:32px;border-radius:50%;object-fit:cover;vertical-align:middle;background:transparent';
|
||
el.setAttribute('data-weval-v75-applied', '1');
|
||
el.prepend(img);
|
||
});
|
||
}
|
||
apply();
|
||
setTimeout(apply, 400); setTimeout(apply, 1200); setTimeout(apply, 3000);
|
||
const mo = new MutationObserver(() => apply());
|
||
mo.observe(document.body, {childList: true, subtree: true});
|
||
setTimeout(() => mo.disconnect(), 20000);
|
||
console.log('[V75 AvatarUnifier] applied from', Object.keys(REG).length, 'agents');
|
||
}).catch(e => console.warn('[V75] fetch failed', e));
|
||
})();
|
||
</script>
|
||
|
||
<!-- === OPUS UNIVERSAL DRILL-DOWN v1 19avr — append-only, doctrine #14 === -->
|
||
<script>
|
||
(function(){
|
||
if (window.__opusUniversalDrill) return; window.__opusUniversalDrill = true;
|
||
var d = document;
|
||
var m = d.createElement('div');
|
||
m.id = 'opus-udrill';
|
||
m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.82);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:99995;padding:20px;cursor:pointer';
|
||
var inner = d.createElement('div');
|
||
inner.id = 'opus-udrill-in';
|
||
inner.style.cssText = 'max-width:900px;width:100%;max-height:90vh;overflow:auto;background:#0b0d15;border:1px solid rgba(99,102,241,0.35);border-radius:14px;padding:28px;cursor:default;box-shadow:0 20px 60px rgba(0,0,0,0.6);color:#e2e8f0;font:14px/1.55 Inter,system-ui,sans-serif';
|
||
inner.addEventListener('click', function(e){ e.stopPropagation(); });
|
||
m.appendChild(inner);
|
||
m.addEventListener('click', function(){ m.style.display='none'; });
|
||
d.addEventListener('keydown', function(e){ if(e.key==='Escape') m.style.display='none'; });
|
||
(d.body || d.documentElement).appendChild(m);
|
||
function openCard(card) {
|
||
var html = '<div style="display:flex;justify-content:flex-end;margin-bottom:14px"><button id="opus-udrill-close" style="padding:6px 14px;background:#171b2a;border:1px solid rgba(99,102,241,0.25);color:#e2e8f0;border-radius:8px;cursor:pointer;font-size:12px">✕ Fermer (Esc)</button></div>';
|
||
html += '<div style="transform-origin:top left;font-size:1.05em">' + card.outerHTML + '</div>';
|
||
inner.innerHTML = html;
|
||
d.getElementById('opus-udrill-close').onclick = function(){ m.style.display='none'; };
|
||
m.style.display = 'flex';
|
||
}
|
||
function wire(root) {
|
||
var sels = '.card,[class*="card"],.kpi,[class*="kpi"],.stat,[class*="stat"],.tile,[class*="tile"],.metric,[class*="metric"],.widget,[class*="widget"]';
|
||
var cards = root.querySelectorAll(sels);
|
||
for (var i = 0; i < cards.length; i++) {
|
||
var c = cards[i];
|
||
if (c.__opusWired) continue;
|
||
if (c.closest('button, a, input, select, textarea, #opus-udrill')) continue;
|
||
var r = c.getBoundingClientRect();
|
||
if (r.width < 60 || r.height < 40) continue;
|
||
c.__opusWired = true;
|
||
c.style.cursor = 'pointer';
|
||
c.setAttribute('role','button');
|
||
c.setAttribute('tabindex','0');
|
||
c.addEventListener('click', function(ev){
|
||
if (ev.target.closest('[data-pp-id]') && window.__opusDrillInit) return;
|
||
if (ev.target.closest('a,button,input,select')) return;
|
||
ev.preventDefault(); ev.stopPropagation();
|
||
openCard(this);
|
||
});
|
||
c.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();openCard(this);} });
|
||
}
|
||
}
|
||
var initRun = function(){ wire(d.body || d.documentElement); };
|
||
if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', initRun);
|
||
else initRun();
|
||
var mo = new MutationObserver(function(muts){
|
||
var newCard = false;
|
||
for (var i=0;i<muts.length;i++) if (muts[i].addedNodes.length) { newCard = true; break; }
|
||
if (newCard) initRun();
|
||
});
|
||
mo.observe(d.body || d.documentElement, {childList:true, subtree:true});
|
||
})();
|
||
</script>
|
||
<!-- === OPUS UNIVERSAL DRILL-DOWN END === -->
|
||
|
||
</body>
|
||
</html>
|