generated from coulomb/repo-seed
EP catalogue (all domains): - EP-RAIL-001 ep_id patched (schema fix: add ep_id to EPUpdate) - EP-RAIL-003 (git bare-repo mirrors) and EP-RAIL-004 (offsite secondary backup) registered from railiance-cluster/docs/backup-restore.md - EP-CUST-003..007 ep_ids assigned to existing custodian EPs - EP-CUST-008 (State Hub API auth) and EP-CUST-009 (update_workstream MCP tool) registered as new custodian extension points TD catalogue (railiance — first 5 items): - TD-RAIL-001: backup cron runs as root without audit trail (high/security) - TD-RAIL-002: k3s kubeconfig world-readable mode 644 (medium/security) - TD-RAIL-003: no Ansible role unit tests (medium/test) - TD-RAIL-004: age key extracted via awk — fragile (medium/impl) - TD-RAIL-005: etcd snapshot retention uncoordinated (low/impl) Dashboard (T08 + T10): - Extract API URL and POLL to src/components/config.js; all 15 pages now import from the shared module (contributions/goals keep custom POLL) - Shared .kpi-infobox, .filter-bar, .filter-search/.filter-owner CSS moved to observablehq.config.js head <style> block; removed from 9 pages - Build: 0 errors, 0 warnings API (T09): - progress.py: limit param now Query(100, le=1000) — prevents unbounded list requests; closes TD-CUST-004 for the only endpoint that had limit CUST-WP-0004 marked completed (all 10 tasks done). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3.2 KiB
3.2 KiB
title
| title |
|---|
| Progress |
import {API, POLL} from "./components/config.js";
const progState = (async function*() {
while (true) {
let data = [], ok = false;
try {
const r = await fetch(`${API}/progress/?limit=500`);
ok = r.ok;
data = ok ? await r.json() : [];
} catch {}
yield {data, ok, ts: new Date()};
await new Promise(res => setTimeout(res, POLL));
}
})();
const data = progState.data ?? [];
const _ok = progState.ok ?? false;
const _ts = progState.ts;
Progress Log
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp} from "./components/doc-overlay.js";
const _liveEl = html`<div class="live-indicator">
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
${_ok
? `Live · updated ${_ts?.toLocaleTimeString()} · ${data.length} events total`
: html`<span style="color:red">Offline — run: <code>make api</code></span>`}
</div>`;
withDocHelp(_liveEl, "/docs/live-data");
injectTocTop("live-indicator", _liveEl);
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/progress-log"); }
Event Volume (Last 30 Days)
import * as Plot from "npm:@observablehq/plot";
const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const byDay = Object.entries(
data
.filter(e => new Date(e.created_at) >= cutoff)
.reduce((acc, e) => {
const day = e.created_at.slice(0, 10);
acc[day] = (acc[day] ?? 0) + 1;
return acc;
}, {})
).sort().map(([day, count]) => ({day, count}));
display(byDay.length === 0
? html`<p style="color:gray">No events in the last 30 days.</p>`
: Plot.plot({
title: "Progress events per day (30-day window)",
x: {label: "Date", tickRotate: -30},
y: {label: "Events", grid: true},
marks: [
Plot.areaY(byDay, {x: "day", y: "count", fill: "steelblue", fillOpacity: 0.3}),
Plot.lineY(byDay, {x: "day", y: "count", stroke: "steelblue"}),
Plot.ruleY([0]),
],
marginBottom: 60,
width: 750,
})
);
Event Log
const authorOpts = ["(all)", ...new Set(data.map(e => e.author ?? "unknown"))].sort();
const typeOpts = ["(all)", ...new Set(data.map(e => e.event_type))].sort();
const authorFilter = view(Inputs.select(authorOpts, {label: "Author"}));
const typeFilter = view(Inputs.select(typeOpts, {label: "Event type"}));
const sinceFilter = view(Inputs.date({label: "Since"}));
const filtered = data.filter(e =>
(authorFilter === "(all)" || (e.author ?? "unknown") === authorFilter) &&
(typeFilter === "(all)" || e.event_type === typeFilter) &&
(!sinceFilter || new Date(e.created_at) >= sinceFilter)
);
display(html`<p><strong>${filtered.length}</strong> events shown (append-only, no deletions).</p>`);
display(Inputs.table(filtered.map(e => ({
Time: new Date(e.created_at).toLocaleString(),
Type: e.event_type,
Author: e.author ?? "—",
Summary: e.summary,
})), {rows: 50}));