From 910c76bd7cd9b0ece963ba5aa7fc7fcd68ce5a7b Mon Sep 17 00:00:00 2001 From: opus Date: Mon, 20 Apr 2026 03:38:46 +0200 Subject: [PATCH] auto-sync via WEVIA git_sync_all intent 2026-04-20T03:38:46+02:00 --- api/v83-business-kpi-latest.json | 2 +- business-kpi-dashboard.php | 48 ++++++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/api/v83-business-kpi-latest.json b/api/v83-business-kpi-latest.json index 94a8041d4..82b2faa3b 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-20T01:36:20+00:00", + "ts": "2026-04-20T01:38:27+00:00", "summary": { "total_categories": 7, "total_kpis": 56, diff --git a/business-kpi-dashboard.php b/business-kpi-dashboard.php index 7ddb6e15a..2fbcd88f9 100644 --- a/business-kpi-dashboard.php +++ b/business-kpi-dashboard.php @@ -208,7 +208,27 @@ body{background:var(--bg);color:var(--fg);font-family:"DM Sans","Inter",sans-ser } catch(e) { console.error("V71 fetch fail", e); return; } const catalog = full.catalog || {}; - const summary = (full.summary || {}); + // V73 FIX - action=full does NOT return summary, compute from catalog + let summary = full.summary || {}; + if (!summary.ok) { + // Fetch summary separately as fallback + try { + const rS = await fetch("/api/wevia-v83-business-kpi.php?action=summary",{cache:"no-store"}); + const sJ = await rS.json(); + summary = sJ.summary || {}; + } catch(e) { console.warn("[V71] summary fetch fail", e); } + } + // Always recompute from catalog (source of truth) if still missing + if (!summary.ok) { + summary = {ok:0, warn:0, fail:0, wire_needed:0, total_kpis:0, data_completeness_pct:0}; + for (const c of Object.values(catalog)) { + for (const k of (c.kpis||[])) { + summary.total_kpis++; + if (k.status && summary[k.status] !== undefined) summary[k.status]++; + } + } + summary.data_completeness_pct = summary.total_kpis ? Math.round((summary.ok/summary.total_kpis)*1000)/10 : 0; + } // ============= Chart 1: Status doughnut ============= const counts = {ok: summary.ok||0, warn: summary.warn||0, fail: summary.fail||0, wire_needed: summary.wire_needed||0}; @@ -252,18 +272,30 @@ body{background:var(--bg);color:var(--fg);font-family:"DM Sans","Inter",sans-ser 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)}); + if (typeof k.value === "number" && typeof k.target === "number" && k.target > 0) { + // V73: show actual pct (can be <100 for below-target, cap at 150 visually for over-performers) + const rawPct = (k.value / k.target) * 100; + allKpis.push({label: k.label, value: k.value, target: k.target, unit: k.unit, pct: Math.min(150, rawPct), rawPct: Math.round(rawPct), status: k.status}); } } } - allKpis.sort((a,b) => b.pct - a.pct); + // V73: show mix of below/on/above target for insight + allKpis.sort((a,b) => { + // Prioritize variance from 100 (show extremes) + return Math.abs(b.pct - 100) - Math.abs(a.pct - 100); + }); const top8 = allKpis.slice(0, 8); + const barColor = (pct, status) => { + if (status === "fail" || pct < 50) return "linear-gradient(90deg,#fc8181,#f6ad55)"; + if (status === "warn" || pct < 90) return "linear-gradient(90deg,#f6ad55,#ecc94b)"; + if (pct > 120) return "linear-gradient(90deg,#48bb78,#6c9ef8)"; + return "linear-gradient(90deg,#48bb78,#38b2ac)"; + }; document.getElementById("v71-top-bars").innerHTML = top8.map(k => ` -
-
${k.label.substring(0,22)}
-
-
${Math.round(k.pct)}%
+
+
${k.label.substring(0,22)}
+
+
${k.rawPct}%
`).join(""); // ============= Per-KPI sparklines (deterministic pseudo-trend) =============