// Shared site chrome — header, footer, status badge. Used by all pages. // SiteFooter and any page needing ledger counts pull live from // /catalog/index.json (the real, gap-free catalogue) rather than // carrying a hardcoded snapshot — see useCatalog()/catalogCounts() below. function SwMark({ className }) { return ( ); } // ── Live catalogue data ─────────────────────────────────────────────────── // Single shared fetch of the real ledger, memoized so every component that // needs counts (header, footer, about-page stats, …) hits the network once. let _catalogPromise = null; function fetchCatalog() { if (!_catalogPromise) { _catalogPromise = fetch("/catalog/index.json") .then((r) => r.json()) .catch(() => []); } return _catalogPromise; } function useCatalog() { const [entries, setEntries] = React.useState(null); React.useEffect(() => { let alive = true; fetchCatalog().then((d) => { if (alive) setEntries(d); }); return () => { alive = false; }; }, []); return entries; } function catalogCounts(entries) { if (!entries || !entries.length) return null; const nums = entries.map((e) => parseInt(e.catalogue, 10)).sort((a, b) => a - b); const first = nums[0]; const last = nums[nums.length - 1]; const pad = (n) => String(n).padStart(3, "0"); return { all: entries.length, built: entries.filter((e) => e.status === "built").length, failed: entries.filter((e) => e.status === "failed").length, withheld: entries.filter((e) => e.status === "withheld").length, first: pad(first), last: pad(last), gaps: (last - first + 1) - entries.length, }; } function SiteHeader({ active }) { const nav = [ ["Home", "index.html"], ["About", "about.html"], ["Projects", "projects.html"], ]; return (
The Somnial Workshop
); } function StatusBadge({ status }) { const label = { built: "Built", failed: "Failed", withheld: "Withheld", running: "Running" }[status] || status; return {label}; } function SiteFooter() { const entries = useCatalog(); const c = catalogCounts(entries); return ( ); } function utcStamp() { const d = new Date(); const p = (n) => String(n).padStart(2, "0"); return `${d.getUTCFullYear()}-${p(d.getUTCMonth()+1)}-${p(d.getUTCDate())} · ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())} UTC`; } // ── Per-specimen formatting/fetch helpers ───────────────────────────────── // Row-level display uses only ledger fields (no extra fetch). Detail views // lazily pull meta.json / brief.json / smoke.json for one specimen at a time. function fmtTitle(s) { if (!s) return ""; return s.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); } function fmtDate(iso) { return iso ? iso.slice(0, 10) : "—"; } function fmtTime(iso) { return iso ? iso.slice(11, 16) + " UTC" : "—"; } function fmtDur(ms) { if (!ms) return "—"; const totalSec = Math.round(ms / 1000); const m = Math.floor(totalSec / 60); const s = totalSec % 60; return m > 0 ? `${m}m ${String(s).padStart(2, "0")}s` : `${s}s`; } function fetchSpecimenFile(n, file) { return fetch(`/catalog/${n}/${file}`) .then((r) => (r.ok ? r.json() : null)) .catch(() => null); } function fetchSpecimenText(n, file) { return fetch(`/catalog/${n}/${file}`) .then((r) => (r.ok ? r.text() : null)) .catch(() => null); } // diary.txt is prose, then a separator, then the Design section // (fonts/palette/reasoning) the build model wrote — see CLAUDE.md. // The separator's exact whitespace varies between entries (a "---" rule, // or just a "Design" heading line with a stray blank line/trailing spaces), // so match loosely rather than on one exact literal string. function parseDiary(raw) { if (!raw) return { prose: null, design: null }; const txt = raw.trim(); const m = txt.match(/\n\s*-{3,}\s*\n|\n\s*Design\s*\n/i); if (!m) return { prose: txt, design: null }; return { prose: txt.slice(0, m.index).trim(), design: txt.slice(m.index + m[0].length).trim() }; } // Combined detail bundle for one specimen — meta (build stats), brief // (premise), smoke (test results), diary (the "why"), transcript (which // local model actually built it). Used when a row expands. function fetchSpecimenDetail(n) { return Promise.all([ fetchSpecimenFile(n, "meta.json"), fetchSpecimenFile(n, "brief.json"), fetchSpecimenFile(n, "smoke.json"), fetchSpecimenText(n, "diary.txt"), fetchSpecimenFile(n, "transcript.json"), fetchSpecimenFile(n, "api.json"), fetchSpecimenFile(n, "process.json"), ]).then(([meta, brief, smoke, diaryRaw, transcript, api, notes]) => { const checks = smoke?.checks || []; const passed = checks.filter((c) => c.ok).length; const { prose } = parseDiary(diaryRaw); const model = transcript?.build?.[0]?.request?.model || transcript?.diary?.request?.model || transcript?.ideas?.request?.model || null; return { meta, brief, stack: meta?.stack || "—", dur: fmtDur(meta?.build_time_ms), attempts: meta?.attempts || 1, tests: checks.length ? `${passed}/${checks.length} green` : "—", premise: brief?.premise || null, why: prose, model, api, notes, }; }); } Object.assign(window, { SiteHeader, SiteFooter, StatusBadge, SwMark, utcStamp, useCatalog, catalogCounts, fetchCatalog, fmtTitle, fmtDate, fmtTime, fmtDur, fetchSpecimenFile, fetchSpecimenText, parseDiary, fetchSpecimenDetail, });