Gap-to-Draft Pipeline (ietf pipeline): - Context builder assembles ideas, RFC foundations, similar drafts, ecosystem vision - Generator produces outlines + sections using rich context with Claude - Quality gates: novelty (embedding similarity), references, format, self-rating - Family coordinator generates 5-draft ecosystem (AEM/ATD/HITL/AEPB/APAE) - I-D formatter with proper headers, references, 72-char wrapping Living Standards Observatory (ietf observatory): - Source abstraction with IETF + W3C fetchers - 7-step update pipeline: snapshot, fetch, analyze, embed, ideas, gaps, record - Static GitHub Pages dashboard (explorer, gap tracker, timeline) - Weekly CI/CD automation via GitHub Actions Also includes: - 361 drafts (expanded from 260 with 6 new keywords), 403 authors, 1,262 ideas, 12 gaps - Blog series (8 posts planned), reports, arXiv paper figures - Agent team infrastructure (CLAUDE.md, scripts, dev journal) - 5 new DB tables, schema migration, ~15 new query methods Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
157 lines
5.4 KiB
HTML
157 lines
5.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Timeline - Living Standards Observatory</title>
|
|
<link rel="stylesheet" href="../assets/style.css">
|
|
<style>
|
|
.tl-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid #f0f0f0; }
|
|
.tl-month { min-width: 80px; font-size: 0.82rem; color: var(--text-dim); font-family: monospace; }
|
|
.tl-bars { flex: 1; display: flex; gap: 1px; align-items: center; }
|
|
.tl-count { min-width: 30px; text-align: right; font-size: 0.78rem; color: var(--text-dim); }
|
|
.legend { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
.legend-item { display: flex; align-items: center; gap: 4px; font-size: 0.8rem; }
|
|
.legend-swatch { width: 14px; height: 14px; border-radius: 3px; }
|
|
.view-toggle { display: flex; gap: 8px; margin-bottom: 16px; }
|
|
.view-btn { padding: 6px 16px; border: 1px solid var(--border); border-radius: 6px; background: var(--card-bg); cursor: pointer; font-size: 0.82rem; }
|
|
.view-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<div class="container">
|
|
<h1>Living Standards Observatory</h1>
|
|
<nav>
|
|
<a href="../index.html">Dashboard</a>
|
|
<a href="explorer.html">Explorer</a>
|
|
<a href="gaps.html">Gaps</a>
|
|
<a href="timeline.html" class="active">Timeline</a>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
<div class="container">
|
|
|
|
<h2 style="margin-bottom:8px">Submission Timeline</h2>
|
|
<p class="dim" style="margin-bottom:20px">Monthly document submissions across standards bodies and categories.</p>
|
|
|
|
<div class="view-toggle">
|
|
<button class="view-btn active" id="btnSource" onclick="setView('source')">By Source</button>
|
|
<button class="view-btn" id="btnCategory" onclick="setView('category')">By Category</button>
|
|
</div>
|
|
|
|
<div class="legend" id="legend"></div>
|
|
|
|
<div class="chart-container" id="timeline"></div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-header">Monthly Totals</div>
|
|
<table>
|
|
<thead><tr><th>Month</th><th>Total</th><th id="breakdownHeader">By Source</th></tr></thead>
|
|
<tbody id="monthTable"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
function escHtml(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
|
|
|
|
const COLORS_SOURCE = {'ietf': '#4a6cf7', 'w3c': '#ef4444', 'ieee': '#10b981', 'other': '#9ca3af'};
|
|
const COLORS_CAT = [
|
|
'#636EFA', '#EF553B', '#00CC96', '#AB63FA', '#FFA15A',
|
|
'#19D3F3', '#FF6692', '#B6E880', '#FF97FF', '#FECB52',
|
|
'#7C8CF5', '#FF8C69', '#66CDAA', '#BA55D3', '#FFD700',
|
|
];
|
|
|
|
let TL_DATA = null;
|
|
let currentView = 'source';
|
|
|
|
function setView(view) {
|
|
currentView = view;
|
|
document.getElementById('btnSource').className = 'view-btn' + (view === 'source' ? ' active' : '');
|
|
document.getElementById('btnCategory').className = 'view-btn' + (view === 'category' ? ' active' : '');
|
|
document.getElementById('breakdownHeader').textContent = view === 'source' ? 'By Source' : 'By Category';
|
|
renderTimeline();
|
|
}
|
|
|
|
function renderTimeline() {
|
|
if (!TL_DATA) return;
|
|
const months = TL_DATA.months;
|
|
const isSource = currentView === 'source';
|
|
const dataMap = isSource ? TL_DATA.by_source : TL_DATA.by_category;
|
|
const keys = isSource ? TL_DATA.sources : TL_DATA.categories;
|
|
|
|
// Assign colors
|
|
const colorMap = {};
|
|
if (isSource) {
|
|
keys.forEach(k => { colorMap[k] = COLORS_SOURCE[k] || '#9ca3af'; });
|
|
} else {
|
|
keys.forEach((k, i) => { colorMap[k] = COLORS_CAT[i % COLORS_CAT.length]; });
|
|
}
|
|
|
|
// Max for scaling
|
|
let maxTotal = 0;
|
|
months.forEach(m => {
|
|
const d = dataMap[m] || {};
|
|
let t = 0;
|
|
keys.forEach(k => { t += d[k] || 0; });
|
|
if (t > maxTotal) maxTotal = t;
|
|
});
|
|
const scale = maxTotal > 0 ? 500 / maxTotal : 1;
|
|
|
|
// Legend
|
|
const legendEl = document.getElementById('legend');
|
|
legendEl.innerHTML = '';
|
|
keys.forEach(k => {
|
|
legendEl.innerHTML += '<div class="legend-item"><div class="legend-swatch" style="background:' + colorMap[k] + '"></div>' + escHtml(k) + '</div>';
|
|
});
|
|
|
|
// Chart
|
|
const container = document.getElementById('timeline');
|
|
container.innerHTML = '';
|
|
months.forEach(m => {
|
|
const d = dataMap[m] || {};
|
|
let total = 0;
|
|
keys.forEach(k => { total += d[k] || 0; });
|
|
|
|
let barsHtml = '';
|
|
keys.forEach(k => {
|
|
const v = d[k] || 0;
|
|
if (v > 0) {
|
|
const w = Math.max(v * scale, 2);
|
|
barsHtml += '<div class="tl-bar" style="width:' + w + 'px;background:' + colorMap[k] + '" title="' + escHtml(k) + ': ' + v + '"></div>';
|
|
}
|
|
});
|
|
|
|
container.innerHTML += '<div class="tl-row"><span class="tl-month">' + m + '</span><div class="tl-bars">' + barsHtml + '</div><span class="tl-count">' + total + '</span></div>';
|
|
});
|
|
|
|
// Table
|
|
const tbody = document.getElementById('monthTable');
|
|
tbody.innerHTML = '';
|
|
[...months].reverse().forEach(m => {
|
|
const d = dataMap[m] || {};
|
|
let total = 0;
|
|
const parts = [];
|
|
keys.forEach(k => {
|
|
const v = d[k] || 0;
|
|
total += v;
|
|
if (v > 0) parts.push(k + ': ' + v);
|
|
});
|
|
if (total > 0) {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = '<td class="dim">' + m + '</td><td>' + total + '</td><td class="dim">' + parts.join(', ') + '</td>';
|
|
tbody.appendChild(tr);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function init() {
|
|
TL_DATA = await fetch('../data/timeline.json').then(r => r.json());
|
|
renderTimeline();
|
|
}
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html> |