Files
Christian Nennemann d6beb9c0a0 v0.3.0: Gap-to-Draft pipeline, Living Standards Observatory, blog series
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>
2026-03-04 00:48:57 +01:00

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>