feat(release-train): Release Train Dashboard UX premium WTP-style - 388 commits 24h / 62 milestones / 38 phases / 45 doctrines / 216 intents / 98.1pct coverage UX - timeline milestones + donut features/fixes/sync + hourly barchart + tags phases/waves/doctrines + live health bar - auto-refresh 60s
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
This commit is contained in:
1244
api/release-train-data.json
Normal file
1244
api/release-train-data.json
Normal file
File diff suppressed because it is too large
Load Diff
590
release-train-dashboard.html
Normal file
590
release-train-dashboard.html
Normal file
@@ -0,0 +1,590 @@
|
||||
<!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>
|
||||
</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 => ({'<':'<','>':'>','&':'&','"':'"'}[c]));
|
||||
}
|
||||
|
||||
// Initial load + auto-refresh every 60s
|
||||
loadData();
|
||||
setInterval(loadData, 60000);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user