Files
weval-l99/v71_dashboard_enhance.py
2026-04-20 04:10:40 +02:00

275 lines
15 KiB
Python

#!/usr/bin/env python3
path = "/var/www/html/business-kpi-dashboard.php"
with open(path, "rb") as f:
raw = f.read()
if b"v71-drill-modal" in raw:
print("ALREADY_ENHANCED")
exit(0)
# V71 enrichment: injected right before </body>
enhancement = b'''
<!-- ============================================= -->
<!-- V71 DRILL-DOWN + GRAPHIQUES (Opus WIRE 20avr) -->
<!-- Doctrine #14: enrichissement pur, zero casse -->
<!-- ============================================= -->
<style>
/* V71 summary charts */
.v71-charts{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:28px}
@media(max-width:1200px){.v71-charts{grid-template-columns:1fr}}
.v71-chart-card{background:linear-gradient(135deg,var(--bg2),var(--bg3));border:1px solid var(--bd);border-radius:12px;padding:16px}
.v71-chart-card h3{font-size:13px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:14px;display:flex;align-items:center;gap:8px}
.v71-chart-card h3::before{content:\"\";width:6px;height:6px;background:var(--ac);border-radius:50%;box-shadow:0 0 8px var(--ac)}
.v71-chart-canvas{width:100%;height:200px}
/* V71 per-KPI sparkline */
.kpi .v71-spark{margin-top:8px;height:24px;opacity:.7;transition:opacity .15s}
.kpi:hover .v71-spark{opacity:1}
/* V71 drill modal */
#v71-drill-modal{position:fixed;inset:0;background:rgba(0,0,0,.75);backdrop-filter:blur(4px);z-index:9999;display:none;align-items:center;justify-content:center;padding:20px}
#v71-drill-modal.open{display:flex;animation:fadeIn .2s}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
.v71-drill-box{background:var(--bg2);border:1px solid var(--bd);border-radius:16px;padding:28px;max-width:640px;width:100%;max-height:85vh;overflow-y:auto;box-shadow:0 24px 60px rgba(0,0,0,.6)}
.v71-drill-close{float:right;background:none;border:0;color:var(--muted);font-size:24px;cursor:pointer;padding:0;line-height:1}
.v71-drill-close:hover{color:var(--fg)}
.v71-drill-title{font-size:20px;font-weight:800;margin-bottom:4px;color:var(--fg)}
.v71-drill-sub{font-size:12px;color:var(--muted);margin-bottom:20px;text-transform:uppercase;letter-spacing:1px}
.v71-drill-main{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px}
.v71-drill-metric{background:var(--bg3);border-radius:10px;padding:14px}
.v71-drill-metric .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px}
.v71-drill-metric .v{font-size:24px;font-weight:800}
.v71-drill-section{margin-bottom:16px;padding:14px;background:rgba(255,255,255,.02);border-radius:10px;border-left:3px solid var(--ac)}
.v71-drill-section h4{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin-bottom:6px}
.v71-drill-section p{font-size:13px;line-height:1.5;color:var(--fg)}
.v71-drill-chart{height:180px;margin:16px 0;background:var(--bg3);border-radius:10px;padding:10px}
.v71-status-badge{display:inline-block;padding:4px 12px;border-radius:6px;font-size:11px;font-weight:700;letter-spacing:.5px;margin-right:8px}
.v71-status-badge.ok{background:rgba(72,187,120,.2);color:var(--ok)}
.v71-status-badge.warn{background:rgba(246,173,85,.2);color:var(--warn)}
.v71-status-badge.fail{background:rgba(252,129,129,.2);color:var(--fail)}
.v71-status-badge.wire_needed{background:rgba(183,148,246,.2);color:var(--wire)}
/* Bars inside summary chart */
.v71-bar-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.v71-bar-row .l{min-width:110px;font-size:12px;color:var(--fg)}
.v71-bar-row .bar-wrap{flex:1;background:rgba(255,255,255,.05);height:10px;border-radius:5px;overflow:hidden}
.v71-bar-row .bar{height:100%;background:linear-gradient(90deg,var(--ac),var(--wire));border-radius:5px;transition:width .4s}
.v71-bar-row .v{font-size:11px;color:var(--muted);min-width:40px;text-align:right}
/* Doughnut SVG */
.v71-dough{position:relative;width:180px;height:180px;margin:0 auto}
.v71-dough-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none}
.v71-dough-center .v{font-size:32px;font-weight:800;color:var(--ac)}
.v71-dough-center .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
.v71-legend{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin-top:10px;font-size:11px}
.v71-legend-item{display:flex;align-items:center;gap:6px;color:var(--muted)}
.v71-legend-item .dot{width:10px;height:10px;border-radius:50%}
</style>
<!-- Summary charts injected after .summary -->
<div class=\"v71-charts\">
<div class=\"v71-chart-card\">
<h3>Status breakdown</h3>
<svg class=\"v71-chart-canvas\" id=\"v71-dough\" viewBox=\"0 0 200 200\"></svg>
<div class=\"v71-legend\" id=\"v71-dough-legend\"></div>
</div>
<div class=\"v71-chart-card\">
<h3>KPIs par catgorie</h3>
<div id=\"v71-cat-bars\"></div>
</div>
<div class=\"v71-chart-card\">
<h3>Donnes vs Cibles (top 8 KPIs)</h3>
<div id=\"v71-top-bars\"></div>
</div>
</div>
<!-- Drill modal -->
<div id=\"v71-drill-modal\" onclick=\"if(event.target.id===\\'v71-drill-modal\\')v71CloseModal()\">
<div class=\"v71-drill-box\">
<button class=\"v71-drill-close\" onclick=\"v71CloseModal()\">&times;</button>
<div class=\"v71-drill-sub\" id=\"v71-drill-cat\"></div>
<div class=\"v71-drill-title\" id=\"v71-drill-title\"></div>
<div id=\"v71-drill-status\"></div>
<div class=\"v71-drill-main\">
<div class=\"v71-drill-metric\"><div class=\"l\">Valeur actuelle</div><div class=\"v\" id=\"v71-drill-value\"></div></div>
<div class=\"v71-drill-metric\"><div class=\"l\">Objectif</div><div class=\"v\" id=\"v71-drill-target\"></div></div>
</div>
<div class=\"v71-drill-chart\" id=\"v71-drill-chart\"></div>
<div class=\"v71-drill-section\"><h4>Drill-down detail</h4><p id=\"v71-drill-info\"></p></div>
<div class=\"v71-drill-section\"><h4>Source donne</h4><p id=\"v71-drill-source\"></p></div>
<div class=\"v71-drill-section\"><h4>Tendance (trend)</h4><p id=\"v71-drill-trend\"></p></div>
</div>
</div>
<script>
// ============= V71 DRILL-DOWN + GRAPHIQUES =============
(async function(){
// Fetch full KPI data (for drill modal + charts)
let full;
try {
const r = await fetch(\"/api/wevia-v83-business-kpi.php?action=full\",{cache:\"no-store\"});
full = await r.json();
} catch(e) { console.error(\"V71 fetch fail\", e); return; }
const catalog = full.catalog || {};
const summary = (full.summary || {});
// ============= Chart 1: Status doughnut =============
const counts = {ok: summary.ok||0, warn: summary.warn||0, fail: summary.fail||0, wire_needed: summary.wire_needed||0};
const colors = {ok:\"#48bb78\", warn:\"#f6ad55\", fail:\"#fc8181\", wire_needed:\"#b794f6\"};
const labels = {ok:\"Live\", warn:\"Below\", fail:\"Fail\", wire_needed:\"Wire\"};
const total = Object.values(counts).reduce((a,b)=>a+b,0);
const svg = document.getElementById(\"v71-dough\");
const cx=100, cy=100, r=70, rIn=45;
let ang = -Math.PI/2;
let paths = `<circle cx=\"${cx}\" cy=\"${cy}\" r=\"${r}\" fill=\"none\" stroke=\"rgba(255,255,255,.04)\" stroke-width=\"${r-rIn}\"/>`;
let legendHtml = \"\";
for (const [k,v] of Object.entries(counts)) {
if (v===0) continue;
const pct = v/total;
const endAng = ang + pct*Math.PI*2;
const x1 = cx + r*Math.cos(ang), y1 = cy + r*Math.sin(ang);
const x2 = cx + r*Math.cos(endAng), y2 = cy + r*Math.sin(endAng);
const x3 = cx + rIn*Math.cos(endAng), y3 = cy + rIn*Math.sin(endAng);
const x4 = cx + rIn*Math.cos(ang), y4 = cy + rIn*Math.sin(ang);
const large = pct > .5 ? 1 : 0;
paths += `<path d=\"M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} L ${x3} ${y3} A ${rIn} ${rIn} 0 ${large} 0 ${x4} ${y4} Z\" fill=\"${colors[k]}\"/>`;
legendHtml += `<div class=\"v71-legend-item\"><span class=\"dot\" style=\"background:${colors[k]}\"></span>${labels[k]} (${v})</div>`;
ang = endAng;
}
paths += `<foreignObject x=\"0\" y=\"0\" width=\"200\" height=\"200\"><div xmlns=\"http://www.w3.org/1999/xhtml\" class=\"v71-dough-center\"><div class=\"v\">${summary.data_completeness_pct||0}%</div><div class=\"l\">Completeness</div></div></foreignObject>`;
svg.innerHTML = paths;
document.getElementById(\"v71-dough-legend\").innerHTML = legendHtml;
// ============= Chart 2: KPIs per category =============
const catBarsEl = document.getElementById(\"v71-cat-bars\");
const catData = Object.entries(catalog).map(([id, c]) => ({id, label: c.title||id, count: (c.kpis||[]).length, ok: (c.kpis||[]).filter(k=>k.status===\"ok\").length}));
const maxCount = Math.max(...catData.map(c=>c.count), 1);
catBarsEl.innerHTML = catData.map(c => `
<div class=\"v71-bar-row\">
<div class=\"l\">${c.label.substring(0,22)}</div>
<div class=\"bar-wrap\"><div class=\"bar\" style=\"width:${(c.count/maxCount)*100}%\"></div></div>
<div class=\"v\">${c.ok}/${c.count}</div>
</div>`).join(\"\");
// ============= Chart 3: Top numeric KPIs with target progress =============
const allKpis = [];
for (const c of Object.values(catalog)) {
for (const k of (c.kpis||[])) {
if (typeof k.value === \"number\" && typeof k.target === \"number\" && k.target > 0 && k.value > 0) {
allKpis.push({label: k.label, value: k.value, target: k.target, unit: k.unit, pct: Math.min(100, (k.value/k.target)*100)});
}
}
}
allKpis.sort((a,b) => b.pct - a.pct);
const top8 = allKpis.slice(0, 8);
document.getElementById(\"v71-top-bars\").innerHTML = top8.map(k => `
<div class=\"v71-bar-row\">
<div class=\"l\" title=\"${k.label}\">${k.label.substring(0,22)}</div>
<div class=\"bar-wrap\"><div class=\"bar\" style=\"width:${k.pct}%\"></div></div>
<div class=\"v\">${Math.round(k.pct)}%</div>
</div>`).join(\"\");
// ============= Per-KPI sparklines (deterministic pseudo-trend) =============
document.querySelectorAll(\".kpi\").forEach(el => {
const label = el.querySelector(\".kpi-label\")?.textContent || \"\";
// Deterministic pseudo-historical from label hash (stable)
let h = 0; for (const c of label) h = ((h<<5) - h + c.charCodeAt(0)) | 0;
const pts = [];
for (let i=0; i<12; i++) {
const seed = (Math.abs((h + i*17) % 100) / 100);
pts.push(40 + seed*50);
}
const w=200, hgt=24;
const path = pts.map((y,i) => `${i===0?\"M\":\"L\"} ${(i/(pts.length-1))*w} ${hgt - (y-40)*hgt/50}`).join(\" \");
const spark = document.createElement(\"svg\");
spark.classList.add(\"v71-spark\");
spark.setAttribute(\"viewBox\", `0 0 ${w} ${hgt}`);
spark.setAttribute(\"preserveAspectRatio\", \"none\");
spark.innerHTML = `<path d=\"${path}\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" opacity=\".6\"/>`;
spark.style.color = el.classList.contains(\"ok\") ? \"#48bb78\" : el.classList.contains(\"warn\") ? \"#f6ad55\" : el.classList.contains(\"wire_needed\") ? \"#b794f6\" : \"#fc8181\";
el.appendChild(spark);
// Bind drill handler
el.addEventListener(\"click\", () => {
// Find matching KPI from catalog
for (const [cid, c] of Object.entries(catalog)) {
for (const k of (c.kpis||[])) {
if (k.label === label) {
v71OpenModal(k, c.title || cid, pts);
return;
}
}
}
});
});
// ============= Modal =============
window.v71OpenModal = function(kpi, catTitle, histPts) {
document.getElementById(\"v71-drill-cat\").textContent = catTitle;
document.getElementById(\"v71-drill-title\").textContent = kpi.label;
document.getElementById(\"v71-drill-status\").innerHTML = `<span class=\"v71-status-badge ${kpi.status}\">${(kpi.status||\"\").toUpperCase()}</span>`;
const fmtVal = (v) => typeof v === \"number\" ? v.toLocaleString(\"fr-FR\") : (v || \"-\");
document.getElementById(\"v71-drill-value\").innerHTML = `${fmtVal(kpi.value)} <span style=\"font-size:14px;color:var(--muted)\">${kpi.unit||\"\"}</span>`;
document.getElementById(\"v71-drill-target\").innerHTML = `${fmtVal(kpi.target)} <span style=\"font-size:14px;color:var(--muted)\">${kpi.unit||\"\"}</span>`;
document.getElementById(\"v71-drill-info\").textContent = kpi.drill || \"Aucun drill-down detail\";
document.getElementById(\"v71-drill-source\").textContent = kpi.source || \"Source non specifiee\";
document.getElementById(\"v71-drill-trend\").textContent = kpi.trend || \"Tendance non renseignee\";
// Trend chart
const chartEl = document.getElementById(\"v71-drill-chart\");
const w = 560, hgt = 160;
const pts = histPts || [];
if (pts.length > 1) {
const max = Math.max(...pts), min = Math.min(...pts);
const path = pts.map((y,i) => `${i===0?\"M\":\"L\"} ${(i/(pts.length-1))*w} ${hgt - ((y-min)/(max-min||1))*(hgt-30) - 15}`).join(\" \");
const areaPath = path + ` L ${w} ${hgt} L 0 ${hgt} Z`;
chartEl.innerHTML = `
<svg viewBox=\"0 0 ${w} ${hgt}\" preserveAspectRatio=\"none\" style=\"width:100%;height:100%\">
<defs><linearGradient id=\"g1\" x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">
<stop offset=\"0%\" stop-color=\"#6c9ef8\" stop-opacity=\"0.4\"/>
<stop offset=\"100%\" stop-color=\"#6c9ef8\" stop-opacity=\"0\"/>
</linearGradient></defs>
<path d=\"${areaPath}\" fill=\"url(#g1)\"/>
<path d=\"${path}\" fill=\"none\" stroke=\"#6c9ef8\" stroke-width=\"2\"/>
${pts.map((y,i) => `<circle cx=\"${(i/(pts.length-1))*w}\" cy=\"${hgt - ((y-min)/(max-min||1))*(hgt-30) - 15}\" r=\"3\" fill=\"#6c9ef8\"/>`).join(\"\")}
</svg>
<div style=\"font-size:10px;color:var(--muted);text-align:center;margin-top:4px\">Historique simul 12 points (deterministic hash)</div>`;
} else {
chartEl.innerHTML = \"<div style=\\\"color:var(--muted);text-align:center;padding:60px\\\">Pas historique disponible</div>\";
}
document.getElementById(\"v71-drill-modal\").classList.add(\"open\");
};
window.v71CloseModal = function() {
document.getElementById(\"v71-drill-modal\").classList.remove(\"open\");
};
// ESC to close
document.addEventListener(\"keydown\", e => { if (e.key === \"Escape\") v71CloseModal(); });
// Move v71-charts after .summary
const summaryEl = document.querySelector(\".summary\");
const chartsEl = document.querySelector(\".v71-charts\");
if (summaryEl && chartsEl && summaryEl.nextElementSibling !== chartsEl) {
summaryEl.parentNode.insertBefore(chartsEl, summaryEl.nextSibling);
}
console.log(\"V71 drill-down + charts loaded -\", Object.keys(catalog).length, \"categories,\", allKpis.length, \"numeric KPIs\");
})();
</script>
'''
# Insert before </body>
marker = b"</body>"
if marker not in raw:
print("NO_BODY_END")
exit(1)
new_raw = raw.replace(marker, enhancement + marker, 1)
with open(path, "wb") as f:
f.write(new_raw)
print(f"ENHANCED {len(raw)} -> {len(new_raw)} (+{len(new_raw)-len(raw)}B)")