Files
html/release-train-dashboard.html

654 lines
27 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Release Train · WEVAL Technology Platform</title>
<meta name="description" content="Release Train Dashboard - Commits, Phases, Waves, Doctrines, Intents, Coverage - Multi-Claude reconciliation live">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
:root{
--bg-0:#05060a;--bg-1:#0a0c14;--bg-2:#11141f;--bg-3:#181c2b;--bg-card:#0e111c;
--border:#1f2436;--border-hover:#3a425f;
--text-0:#f1f5f9;--text-1:#cbd5e1;--text-2:#94a3b8;--text-3:#64748b;
--accent:#6366f1;--accent-hover:#818cf8;
--success:#10b981;--warning:#f59e0b;--danger:#ef4444;--info:#06b6d4;
--gold:#f6d572;--mint:#5cdb95;--violet:#a78bfa;--coral:#ff6b6b;--cyan:#4ecdc4;
--shadow-lg:0 16px 48px rgba(99,102,241,.2);
--radius:14px;--radius-sm:10px;
--trans:.18s cubic-bezier(.4,0,.2,1);
--font-sans:"Inter",-apple-system,system-ui,sans-serif;
--font-mono:"JetBrains Mono",Monaco,monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
body{
background:var(--bg-0);
color:var(--text-0);
font-family:var(--font-sans);
min-height:100vh;
background-image:
radial-gradient(ellipse at 15% 20%,rgba(99,102,241,.08) 0%,transparent 60%),
radial-gradient(ellipse at 85% 80%,rgba(6,182,212,.06) 0%,transparent 60%),
radial-gradient(ellipse at 50% 50%,rgba(167,139,250,.04) 0%,transparent 70%);
overflow-x:hidden;
}
/* ================ HEADER ================ */
header.topbar{
display:flex;align-items:center;justify-content:space-between;
padding:14px 28px;
border-bottom:1px solid var(--border);
background:rgba(10,12,20,.85);
backdrop-filter:blur(24px);
position:sticky;top:0;z-index:100;
}
.brand{display:flex;align-items:center;gap:14px}
.brand-logo{
width:38px;height:38px;
border-radius:10px;
background:linear-gradient(135deg,var(--accent),var(--violet));
display:flex;align-items:center;justify-content:center;
font-weight:900;font-size:18px;color:#fff;
box-shadow:0 4px 20px rgba(99,102,241,.4);
}
.brand-text{display:flex;flex-direction:column}
.brand-title{font-size:14px;font-weight:700;letter-spacing:.08em;color:var(--text-0)}
.brand-sub{font-size:10px;letter-spacing:.2em;color:var(--text-3);text-transform:uppercase;font-weight:600}
.header-crumbs{display:flex;align-items:center;gap:8px;color:var(--text-2);font-size:12px}
.header-crumbs a{color:var(--accent-hover);text-decoration:none;font-weight:500}
.header-crumbs a:hover{color:var(--text-0)}
.health-bar{display:flex;gap:18px;align-items:center}
.h-item{
display:flex;align-items:center;gap:7px;
font-size:11px;letter-spacing:.06em;
color:var(--text-2);font-weight:500;
font-family:var(--font-mono);
}
.h-dot{width:7px;height:7px;border-radius:50%;display:inline-block}
.h-dot.ok{background:var(--mint);box-shadow:0 0 8px var(--mint)}
.h-dot.warn{background:var(--gold);box-shadow:0 0 8px var(--gold)}
.h-dot.err{background:var(--coral);box-shadow:0 0 8px var(--coral)}
.h-val{color:var(--text-0);font-weight:700}
/* ================ LAYOUT ================ */
.page{
display:grid;
grid-template-columns:1fr;
max-width:1680px;
margin:0 auto;
padding:28px;
gap:24px;
}
/* PAGE HEAD */
.page-head{
display:flex;align-items:center;justify-content:space-between;
padding-bottom:8px;
}
.page-title-wrap{display:flex;align-items:center;gap:18px}
.page-icon{
width:52px;height:52px;
border-radius:12px;
background:linear-gradient(135deg,rgba(99,102,241,.2),rgba(6,182,212,.15));
border:1px solid rgba(99,102,241,.3);
display:flex;align-items:center;justify-content:center;
font-size:24px;
}
.page-title{font-size:26px;font-weight:800;letter-spacing:-.02em;color:var(--text-0)}
.page-sub{font-size:13px;color:var(--text-2);margin-top:3px;font-weight:500}
.page-actions{display:flex;gap:10px}
.btn{
padding:10px 16px;border-radius:10px;
background:var(--bg-2);border:1px solid var(--border);color:var(--text-1);
font-size:12px;font-weight:600;letter-spacing:.04em;
cursor:pointer;transition:var(--trans);
display:flex;align-items:center;gap:7px;
font-family:var(--font-sans);
}
.btn:hover{background:var(--bg-3);border-color:var(--border-hover);color:var(--text-0)}
.btn-primary{background:linear-gradient(135deg,var(--accent),var(--violet));border:none;color:#fff}
.btn-primary:hover{opacity:.9;transform:translateY(-1px)}
/* ================ STATS GRID ================ */
.stats-grid{
display:grid;
grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
gap:18px;
}
.stat-card{
background:linear-gradient(135deg,var(--bg-card),var(--bg-1));
border:1px solid var(--border);
border-radius:var(--radius);
padding:22px 24px;
position:relative;overflow:hidden;
transition:var(--trans);
}
.stat-card:hover{border-color:var(--border-hover);transform:translateY(-2px);box-shadow:var(--shadow-lg)}
.stat-card::before{
content:"";position:absolute;top:0;left:0;right:0;height:3px;
background:linear-gradient(90deg,var(--accent),var(--violet));
opacity:.5;
}
.stat-card.mint::before{background:linear-gradient(90deg,var(--mint),var(--success))}
.stat-card.gold::before{background:linear-gradient(90deg,var(--gold),var(--warning))}
.stat-card.cyan::before{background:linear-gradient(90deg,var(--cyan),var(--info))}
.stat-card.coral::before{background:linear-gradient(90deg,var(--coral),var(--danger))}
.stat-label{font-size:10px;letter-spacing:.15em;text-transform:uppercase;color:var(--text-3);font-weight:700;margin-bottom:10px}
.stat-value{font-size:34px;font-weight:900;letter-spacing:-.03em;color:var(--text-0);line-height:1;font-family:var(--font-mono)}
.stat-sub{font-size:11px;color:var(--text-2);margin-top:7px;font-weight:500}
.stat-delta{
display:inline-flex;align-items:center;gap:4px;
padding:2px 8px;border-radius:100px;
background:rgba(16,185,129,.1);color:var(--mint);
font-size:10px;font-weight:700;
margin-left:8px;
}
/* ================ SECTION ================ */
.section{
background:var(--bg-card);border:1px solid var(--border);
border-radius:var(--radius);overflow:hidden;
}
.section-head{
padding:18px 24px;border-bottom:1px solid var(--border);
display:flex;align-items:center;justify-content:space-between;
background:linear-gradient(180deg,rgba(99,102,241,.04),transparent);
}
.section-title{font-size:14px;font-weight:700;letter-spacing:.02em;color:var(--text-0);display:flex;align-items:center;gap:10px}
.section-title::before{content:"";width:3px;height:18px;background:var(--accent);border-radius:2px}
.section-meta{font-size:11px;color:var(--text-3);font-family:var(--font-mono)}
.section-body{padding:20px 24px}
/* ================ TWO-COL ================ */
.two-col{display:grid;grid-template-columns:1.6fr 1fr;gap:24px}
@media (max-width:1200px){.two-col{grid-template-columns:1fr}}
/* ================ TIMELINE ================ */
.timeline{position:relative;padding-left:30px}
.timeline::before{
content:"";position:absolute;left:10px;top:8px;bottom:8px;width:2px;
background:linear-gradient(180deg,var(--accent),var(--violet),transparent);
}
.tl-item{
position:relative;padding:14px 0 16px 18px;
border-bottom:1px dashed var(--border);
}
.tl-item:last-child{border-bottom:none}
.tl-item::before{
content:"";position:absolute;left:-25px;top:22px;
width:11px;height:11px;border-radius:50%;
background:var(--accent);
box-shadow:0 0 0 3px var(--bg-card),0 0 0 4px var(--accent);
}
.tl-item.milestone::before{background:var(--gold);box-shadow:0 0 0 3px var(--bg-card),0 0 0 4px var(--gold),0 0 14px var(--gold)}
.tl-item.feat::before{background:var(--mint);box-shadow:0 0 0 3px var(--bg-card),0 0 0 4px var(--mint)}
.tl-item.fix::before{background:var(--coral);box-shadow:0 0 0 3px var(--bg-card),0 0 0 4px var(--coral)}
.tl-item.sync::before{background:var(--text-3);box-shadow:0 0 0 3px var(--bg-card),0 0 0 4px var(--text-3)}
.tl-head{display:flex;align-items:center;gap:10px;margin-bottom:5px;flex-wrap:wrap}
.tl-sha{font-family:var(--font-mono);font-size:10px;color:var(--text-3);font-weight:600;background:var(--bg-2);padding:2px 7px;border-radius:4px}
.tl-ts{font-size:10px;color:var(--text-3);font-family:var(--font-mono)}
.tl-tag{font-size:9px;font-weight:700;padding:2px 7px;border-radius:100px;text-transform:uppercase;letter-spacing:.08em}
.tl-tag.phase{background:rgba(99,102,241,.15);color:var(--accent-hover);border:1px solid rgba(99,102,241,.3)}
.tl-tag.wave{background:rgba(6,182,212,.15);color:var(--cyan);border:1px solid rgba(6,182,212,.3)}
.tl-tag.doctrine{background:rgba(246,213,114,.15);color:var(--gold);border:1px solid rgba(246,213,114,.3)}
.tl-subject{font-size:12.5px;color:var(--text-1);line-height:1.5;word-break:break-word}
/* ================ CHART DONUT ================ */
.chart-wrap{padding:16px 0;display:flex;flex-direction:column;align-items:center}
.donut-row{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;width:100%;margin-top:10px}
.donut-card{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:14px;text-align:center}
.donut-val{font-size:28px;font-weight:900;color:var(--text-0);font-family:var(--font-mono);line-height:1}
.donut-label{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--text-3);margin-top:6px;font-weight:700}
/* ================ BAR CHART ================ */
.barchart{display:flex;align-items:flex-end;gap:6px;height:180px;padding:16px 0 8px;border-bottom:1px dashed var(--border);margin-bottom:10px}
.bar-col{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px}
.bar{
width:100%;background:linear-gradient(180deg,var(--accent),var(--violet));
border-radius:4px 4px 0 0;min-height:4px;
transition:all .3s ease;cursor:pointer;
position:relative;
}
.bar:hover{opacity:.8;transform:scaleY(1.05)}
.bar-val{font-size:10px;color:var(--text-2);font-family:var(--font-mono);font-weight:700;position:absolute;top:-18px;left:50%;transform:translateX(-50%)}
.bar-label{font-size:10px;color:var(--text-3);font-family:var(--font-mono)}
/* ================ PROGRESS ================ */
.progress-wrap{margin:16px 0}
.progress-head{display:flex;justify-content:space-between;margin-bottom:8px}
.progress-name{font-size:12px;font-weight:600;color:var(--text-1)}
.progress-val{font-size:12px;font-family:var(--font-mono);color:var(--text-0);font-weight:700}
.progress-track{height:10px;background:var(--bg-2);border-radius:100px;overflow:hidden;border:1px solid var(--border)}
.progress-fill{
height:100%;
background:linear-gradient(90deg,var(--mint),var(--cyan),var(--accent));
border-radius:100px;
transition:width 1s cubic-bezier(.4,0,.2,1);
box-shadow:0 0 20px rgba(92,219,149,.4);
}
/* ================ TAGS GRID ================ */
.tags-grid{display:flex;flex-wrap:wrap;gap:8px;padding:4px 0}
.tag-chip{
padding:6px 12px;border-radius:100px;
font-size:11px;font-weight:600;font-family:var(--font-mono);
background:var(--bg-2);border:1px solid var(--border);color:var(--text-1);
display:flex;align-items:center;gap:6px;
transition:var(--trans);cursor:default;
}
.tag-chip:hover{border-color:var(--border-hover);transform:translateY(-1px)}
.tag-chip.phase{background:rgba(99,102,241,.08);border-color:rgba(99,102,241,.25);color:var(--accent-hover)}
.tag-chip.wave{background:rgba(6,182,212,.08);border-color:rgba(6,182,212,.25);color:var(--cyan)}
.tag-chip.doctrine{background:rgba(246,213,114,.08);border-color:rgba(246,213,114,.25);color:var(--gold)}
.tag-chip .count{background:rgba(255,255,255,.08);padding:1px 6px;border-radius:4px;font-size:9px;color:var(--text-0);font-weight:800}
/* ================ LEGEND ================ */
.legend{display:flex;gap:16px;margin:12px 0;font-size:11px;flex-wrap:wrap}
.leg-item{display:flex;align-items:center;gap:6px;color:var(--text-2)}
.leg-dot{width:10px;height:10px;border-radius:50%}
/* ================ FOOTER ================ */
footer{
text-align:center;padding:24px;
color:var(--text-3);font-size:11px;font-family:var(--font-mono);
border-top:1px solid var(--border);margin-top:24px;
}
footer a{color:var(--accent-hover);text-decoration:none}
/* ================ ANIMATIONS ================ */
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.pulse{animation:pulse 2s ease-in-out infinite}
@keyframes slideUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:none}}
.stat-card,.section{animation:slideUp .5s ease-out backwards}
.stat-card:nth-child(1){animation-delay:.05s}
.stat-card:nth-child(2){animation-delay:.1s}
.stat-card:nth-child(3){animation-delay:.15s}
.stat-card:nth-child(4){animation-delay:.2s}
.stat-card:nth-child(5){animation-delay:.25s}
.stat-card:nth-child(6){animation-delay:.3s}
@media (max-width:720px){
.page{padding:14px}
.page-head{flex-direction:column;align-items:flex-start;gap:12px}
.health-bar{display:none}
.stats-grid{grid-template-columns:repeat(2,1fr);gap:10px}
.stat-card{padding:14px}
.stat-value{font-size:24px}
}
</style>
<!-- DOCTRINE-60-UX-ENRICH direct-inject-20260424-154324 -->
<style id="doctrine60-ux-direct">
/* DOCTRINE-60-UX-ENRICH injected-direct */
body::before {
content: '';
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
background: radial-gradient(circle at 50% 50%, rgba(100,180,255,0.08), transparent 60%);
pointer-events: none;
z-index: -1;
}
.card, .kpi, .panel, .btn {
transition: all 0.3s cubic-bezier(0.2,0,0.1,1);
}
.card:hover, .kpi:hover, .panel:hover {
box-shadow: 0 4px 20px rgba(100,180,255,0.2);
border-color: rgba(100,180,255,0.5);
}
@keyframes pulseD60 {
0%,100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.05); }
}
.pulse, .live-indicator, .active, .online {
animation: pulseD60 3s ease-in-out infinite;
}
.modal, .chat, .speech, .overlay {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.enter-stagger {
animation: enterStagD60 0.5s cubic-bezier(0.2,0,0.1,1) forwards;
}
@keyframes enterStagD60 {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ===== WEVIA-OVERLAP-FIX-24AVR-PAGES - reserve top-right for nginx Logout ===== */
header.topbar{padding-right:130px !important}
#weval-global-logout{top:10px !important;right:12px !important;z-index:99995 !important;opacity:0.6 !important}
#weval-global-logout:hover{opacity:1 !important}
@media (max-width:900px){header.topbar .header-crumbs{display:none !important}}
/* ===== END WEVIA-OVERLAP-FIX-24AVR-PAGES ===== */
</style>
</head>
<body>
<header class="topbar">
<div class="brand">
<div class="brand-logo">W</div>
<div class="brand-text">
<span class="brand-title">WEVAL Technology Platform</span>
<span class="brand-sub">Release Train · Live</span>
</div>
</div>
<div class="header-crumbs">
<a href="/weval-technology-platform.html">WTP</a>
<span></span>
<a href="/wevia-cockpit.html">Cockpit</a>
<span></span>
<span style="color:var(--text-0);font-weight:600">Release Train</span>
</div>
<div class="health-bar" id="health-bar">
<div class="h-item"><span class="h-dot ok pulse"></span><span>LIVE</span></div>
<div class="h-item">S204 · <span class="h-val" id="h-load"></span></div>
<div class="h-item">Intents · <span class="h-val" id="h-intents"></span></div>
<div class="h-item">Coverage · <span class="h-val" id="h-coverage"></span></div>
<div class="h-item">Commits 24h · <span class="h-val" id="h-commits"></span></div>
</div>
</header>
<div class="page">
<!-- PAGE HEAD -->
<div class="page-head">
<div class="page-title-wrap">
<div class="page-icon">🚂</div>
<div>
<h1 class="page-title">Release Train Dashboard</h1>
<p class="page-sub">Multi-Claude reconciliation · Phases · Waves · Doctrines · Intents · Coverage UX</p>
</div>
</div>
<div class="page-actions">
<button class="btn" onclick="loadData()">
<span id="refresh-icon"></span> Refresh
</button>
<a href="/wevia-cockpit.html" class="btn btn-primary">
⚡ Cockpit
</a>
</div>
</div>
<!-- KPI STATS -->
<div class="stats-grid" id="stats-grid">
<div class="stat-card">
<div class="stat-label">📦 Commits 24h</div>
<div class="stat-value" id="kpi-commits"></div>
<div class="stat-sub">auto-sync + features + fixes</div>
</div>
<div class="stat-card mint">
<div class="stat-label">🎯 Milestones</div>
<div class="stat-value" id="kpi-milestones"></div>
<div class="stat-sub">phases + waves significatives</div>
</div>
<div class="stat-card cyan">
<div class="stat-label">🤖 Intents</div>
<div class="stat-value" id="kpi-intents"></div>
<div class="stat-sub">dernière progression</div>
</div>
<div class="stat-card gold">
<div class="stat-label">📐 Pages UX</div>
<div class="stat-value" id="kpi-coverage"></div>
<div class="stat-sub" id="kpi-coverage-sub"></div>
</div>
<div class="stat-card">
<div class="stat-label">🏗 Phases</div>
<div class="stat-value" id="kpi-phases"></div>
<div class="stat-sub">uniques 24h</div>
</div>
<div class="stat-card coral">
<div class="stat-label">📜 Doctrines</div>
<div class="stat-value" id="kpi-doctrines"></div>
<div class="stat-sub">numérotées</div>
</div>
</div>
<!-- COVERAGE PROGRESS -->
<div class="section">
<div class="section-head">
<div class="section-title">Coverage UX Doctrine 60</div>
<div class="section-meta" id="cov-meta"></div>
</div>
<div class="section-body">
<div class="progress-wrap">
<div class="progress-head">
<span class="progress-name">Pages enrichies (cascade async)</span>
<span class="progress-val" id="progress-val">0%</span>
</div>
<div class="progress-track">
<div class="progress-fill" id="progress-fill" style="width:0%"></div>
</div>
</div>
</div>
</div>
<!-- 2-COL: TIMELINE + DISTRIBUTION -->
<div class="two-col">
<!-- TIMELINE MILESTONES -->
<div class="section">
<div class="section-head">
<div class="section-title">Timeline Milestones</div>
<div class="legend">
<div class="leg-item"><span class="leg-dot" style="background:var(--gold)"></span>Milestone</div>
<div class="leg-item"><span class="leg-dot" style="background:var(--mint)"></span>Feature</div>
<div class="leg-item"><span class="leg-dot" style="background:var(--coral)"></span>Fix</div>
<div class="leg-item"><span class="leg-dot" style="background:var(--text-3)"></span>Auto-sync</div>
</div>
</div>
<div class="section-body">
<div class="timeline" id="timeline">
<div style="color:var(--text-3);text-align:center;padding:40px;font-family:var(--font-mono);font-size:12px">Chargement timeline…</div>
</div>
</div>
</div>
<!-- RIGHT COL -->
<div style="display:flex;flex-direction:column;gap:24px">
<!-- DONUT DISTRIBUTION -->
<div class="section">
<div class="section-head">
<div class="section-title">Répartition Commits</div>
</div>
<div class="section-body">
<div class="donut-row">
<div class="donut-card">
<div class="donut-val" id="d-feat"></div>
<div class="donut-label" style="color:var(--mint)">Features</div>
</div>
<div class="donut-card">
<div class="donut-val" id="d-fix"></div>
<div class="donut-label" style="color:var(--coral)">Fixes</div>
</div>
<div class="donut-card">
<div class="donut-val" id="d-sync"></div>
<div class="donut-label" style="color:var(--text-3)">Auto-sync</div>
</div>
</div>
</div>
</div>
<!-- HOURLY BARCHART -->
<div class="section">
<div class="section-head">
<div class="section-title">Distribution Horaire</div>
<div class="section-meta">24h · CET</div>
</div>
<div class="section-body">
<div class="barchart" id="barchart">
<div style="color:var(--text-3);font-size:11px;font-family:var(--font-mono);margin:auto">Chargement…</div>
</div>
</div>
</div>
<!-- PHASES TAGS -->
<div class="section">
<div class="section-head">
<div class="section-title">Phases · Waves · Doctrines</div>
</div>
<div class="section-body">
<div style="font-size:10px;letter-spacing:.15em;text-transform:uppercase;color:var(--text-3);font-weight:700;margin-bottom:10px">PHASES</div>
<div class="tags-grid" id="tags-phases"></div>
<div style="font-size:10px;letter-spacing:.15em;text-transform:uppercase;color:var(--text-3);font-weight:700;margin:16px 0 10px">WAVES</div>
<div class="tags-grid" id="tags-waves"></div>
<div style="font-size:10px;letter-spacing:.15em;text-transform:uppercase;color:var(--text-3);font-weight:700;margin:16px 0 10px">DOCTRINES</div>
<div class="tags-grid" id="tags-doctrines"></div>
</div>
</div>
</div>
</div>
<footer>
Release Train · WEVAL Technology Platform · Multi-Claude Convergence · Live Data · <span id="gen-at"></span>
· <a href="/api/release-train-data.json" target="_blank">raw JSON</a>
</footer>
</div>
<script>
// ============= Data Loading =============
async function loadData() {
const icon = document.getElementById("refresh-icon");
icon.style.animation = "pulse 1s infinite";
try {
const res = await fetch("/api/release-train-data.json?t=" + Date.now(), {cache:"no-store"});
const d = await res.json();
render(d);
// Health bar
document.getElementById("h-intents").textContent = d.stats.last_intent_count || "—";
document.getElementById("h-coverage").textContent = d.stats.last_coverage ? d.stats.last_coverage.pct + "%" : "—";
document.getElementById("h-commits").textContent = d.stats.total_commits_24h;
fetchLoad();
document.getElementById("gen-at").textContent = new Date(d.generated_at).toLocaleString("fr-FR");
} catch(e) {
console.error("Load fail", e);
} finally {
icon.style.animation = "";
}
}
async function fetchLoad() {
try {
const res = await fetch("/api/infra-dashboard-api.php?t=" + Date.now(), {cache:"no-store", signal:AbortSignal.timeout(5000)});
const d = await res.json();
const load = d.load_1m || d.load || null;
document.getElementById("h-load").textContent = load ? load.toFixed(1) : "—";
} catch(e) {
document.getElementById("h-load").textContent = "—";
}
}
function render(d) {
// KPIs
document.getElementById("kpi-commits").textContent = d.stats.total_commits_24h;
document.getElementById("kpi-milestones").textContent = d.stats.milestones_24h;
document.getElementById("kpi-intents").textContent = d.stats.last_intent_count;
if (d.stats.last_coverage) {
document.getElementById("kpi-coverage").textContent = d.stats.last_coverage.pct + "%";
document.getElementById("kpi-coverage-sub").textContent = `${d.stats.last_coverage.num}/${d.stats.last_coverage.total}`;
document.getElementById("cov-meta").textContent = `${d.stats.last_coverage.num} / ${d.stats.last_coverage.total} pages`;
document.getElementById("progress-val").textContent = d.stats.last_coverage.pct + "%";
setTimeout(() => {
document.getElementById("progress-fill").style.width = d.stats.last_coverage.pct + "%";
}, 200);
}
document.getElementById("kpi-phases").textContent = d.stats.unique_phases;
document.getElementById("kpi-doctrines").textContent = d.stats.unique_doctrines;
// Donut
document.getElementById("d-feat").textContent = d.stats.features;
document.getElementById("d-fix").textContent = d.stats.fixes;
document.getElementById("d-sync").textContent = d.stats.auto_sync;
// Timeline milestones
const tl = document.getElementById("timeline");
tl.innerHTML = "";
(d.milestone_commits || []).slice(0, 25).forEach(c => {
const cls = c.milestone ? "milestone" : (c.type === "fix" ? "fix" : (c.type === "auto-sync" ? "sync" : "feat"));
const tags = [];
if (c.phase) tags.push(`<span class="tl-tag phase">phase ${c.phase}</span>`);
if (c.wave) tags.push(`<span class="tl-tag wave">wave ${c.wave}</span>`);
if (c.doctrine) tags.push(`<span class="tl-tag doctrine">D${c.doctrine}</span>`);
const dt = new Date(c.ts);
const tsStr = dt.toLocaleTimeString("fr-FR",{hour:"2-digit",minute:"2-digit"});
const subjTrim = c.subject.length > 180 ? c.subject.slice(0,180) + "…" : c.subject;
tl.insertAdjacentHTML("beforeend",
`<div class="tl-item ${cls}">
<div class="tl-head">
<span class="tl-sha">${c.sha}</span>
<span class="tl-ts">${tsStr}</span>
${tags.join("")}
</div>
<div class="tl-subject">${escapeHtml(subjTrim)}</div>
</div>`
);
});
// Barchart hourly
const bc = document.getElementById("barchart");
bc.innerHTML = "";
const hh = d.hourly_distribution || {};
const max = Math.max(1, ...Object.values(hh));
const hours = Object.keys(hh).sort();
hours.forEach(h => {
const v = hh[h];
const pct = (v / max) * 100;
bc.insertAdjacentHTML("beforeend",
`<div class="bar-col" title="${h}h: ${v} commits">
<div class="bar" style="height:${pct}%"><span class="bar-val">${v}</span></div>
<div class="bar-label">${h}h</div>
</div>`);
});
// Tags
const renderTags = (id, data, cls) => {
const el = document.getElementById(id);
el.innerHTML = "";
Object.entries(data).forEach(([k, v]) => {
el.insertAdjacentHTML("beforeend",
`<div class="tag-chip ${cls}">${cls === "phase" ? "phase " : (cls === "wave" ? "wave " : "D")}${k}<span class="count">${v}</span></div>`);
});
};
renderTags("tags-phases", d.phases, "phase");
renderTags("tags-waves", d.waves, "wave");
renderTags("tags-doctrines", d.doctrines, "doctrine");
}
function escapeHtml(s) {
return s.replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
}
// Initial load + auto-refresh every 60s
loadData();
setInterval(loadData, 60000);
</script>
<!-- DOCTRINE-60-UX-JS --><script id="doctrine60-ux-js-direct">
// DOCTRINE-60-UX-JS staggered entrance
(function(){
if (!('IntersectionObserver' in window)) return;
const obs = new IntersectionObserver((entries) => {
entries.forEach((e, i) => {
if (e.isIntersecting) {
setTimeout(() => e.target.classList.add('enter-stagger'), i * 80);
obs.unobserve(e.target);
}
});
});
document.querySelectorAll('.card, .kpi, .panel').forEach(el => obs.observe(el));
})();
</script>
</body>
</html>