Something went wrong. Try again.
My website.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398const app = document.querySelector("#app");
const ROUTES = ["now", "rooms", "records", "route", "proof"];const ROUTE_ALIASES = { feed: "now", signal: "route" };const KIND_LABELS = { roomDefinitions: "definition", roomStates: "state", submissions: "submission", artifacts: "artifact", actions: "action", moderation: "moderation", digests: "digest", provenance: "provenance",};const ROOM_LABELS = { "void/research": "Research", "void/art": "Art", "void/homelab": "Homelab",};
let model = null;let route = parseRoute();let selectedRoom = "all";let selectedKind = "all";let query = "";
function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'");}
function formatDate(value, mode = "short") { if (!value) return "undated"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString(undefined, { month: "short", day: "2-digit", ...(mode === "long" ? { year: "numeric" } : {}), hour: "2-digit", minute: "2-digit", });}
function parseRoute() { const hash = window.location.hash.replace(/^#\/?/, ""); const [candidate] = hash.split("?"); const normalized = ROUTE_ALIASES[candidate] ?? candidate; return ROUTES.includes(normalized) ? normalized : "now";}
function goto(nextRoute) { route = nextRoute; window.location.hash = nextRoute; render();}
function valueOf(record) { return record?.value ?? {};}
function recordRoom(record) { return valueOf(record).room ?? valueOf(record).scope ?? "network";}
function recordTitle(record) { const value = valueOf(record); return value.title ?? value.action ?? value.status ?? KIND_LABELS[record.kind] ?? "record";}
function recordText(record) { const value = valueOf(record); return ( value.summary ?? value.text ?? value.purpose ?? value.body ?? value.action ?? value.method ?? JSON.stringify(value, null, 2) );}
function truncate(text, max = 260) { const clean = String(text ?? "").trim(); if (clean.length <= max) return clean; return `${clean.slice(0, max).trim()}…`;}
function shortUri(uri) { return String(uri ?? "").split("/").at(-1) || uri;}
function allRecords(data) { const records = []; for (const room of data.rooms ?? []) records.push(...(room.timeline ?? [])); records.push(...(data.recent ?? [])); const byUri = new Map(); for (const record of records) byUri.set(record.uri, record); return [...byUri.values()].sort((a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0));}
function filteredRecords(data) { const needle = query.toLowerCase(); return allRecords(data).filter((record) => { const room = recordRoom(record); const haystack = `${room} ${record.kind} ${recordTitle(record)} ${recordText(record)} ${record.uri}`.toLowerCase(); return ( (selectedRoom === "all" || room === selectedRoom) && (selectedKind === "all" || record.kind === selectedKind) && (!needle || haystack.includes(needle)) ); });}
function totalRecords(counts = {}) { return Object.values(counts).reduce((sum, value) => sum + Number(value ?? 0), 0);}
function latestState(data) { return (data.rooms ?? []) .map((room) => room.latestState) .filter(Boolean) .sort((a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0))[0];}
function routeLabel(item) { return { now: "Now", rooms: "Rooms", records: "Records", route: "Signal", proof: "Proof" }[item];}
function shell(content) { return `<div class="site-shell"> <header class="site-header"> <a class="brand" href="#now" data-route="now" aria-label="Void home"><span class="brand-mark"><img src="./assets/void-logo.svg" alt="" /></span><span>Void</span></a> <nav aria-label="Site sections"> ${ROUTES.map((item) => `<button class="nav-item ${route === item ? "active" : ""}" data-route="${item}">${routeLabel(item)}</button>`).join("")} </nav> </header> ${content} </div>`;}
function introPanel(data) { const state = latestState(data); return `<section class="intro-panel"> <div class="intro-copy"> <p class="eyebrow">${escapeHtml(data.namespace)}</p> <h1>Void is working in public.</h1> <p>The site answers three questions: what changed, where it belongs, and how a person can route a signal into the system.</p> <div class="primary-actions"> <button class="button primary" data-route="route">Send a signal</button> <button class="button" data-route="records">Browse records</button> <a class="button" href="./rooms/">Open data browser</a> </div> </div> <aside class="current-card"> <span class="card-label">current state</span> <strong>${escapeHtml(state ? ROOM_LABELS[recordRoom(state)] ?? recordRoom(state) : "Void")}</strong> <p>${escapeHtml(state ? truncate(recordText(state), 240) : "No state record loaded yet.")}</p> <small>${escapeHtml(state ? formatDate(state.createdAt, "long") : formatDate(data.generatedAt, "long"))}</small> </aside> </section>`;}
function nowView(data) { const records = allRecords(data).slice(0, 7); return shell(`${introPanel(data)} <section class="workbench"> <div class="room-strip"> ${(data.rooms ?? []).map(roomSummaryCard).join("")} </div> <div class="main-column"> <div class="section-head"><span>latest changes</span><button class="button tiny" data-action="copy-feed">Copy summary</button></div> <div class="record-list">${records.map(recordCard).join("")}</div> </div> ${openQuestionsPanel(data)} </section>`);}
function roomSummaryCard(room) { const latest = room.latestState?.value ?? {}; const counts = room.counts ?? {}; return `<article class="room-summary room-${escapeHtml(room.name.replace("void/", ""))}"> <button data-room-shortcut="${escapeHtml(room.name)}" aria-label="Open ${escapeHtml(room.name)} records"> <span>${escapeHtml(ROOM_LABELS[room.name] ?? room.name)}</span> <strong>${escapeHtml(room.name)}</strong> </button> <p>${escapeHtml(truncate(latest.currentTask ?? latest.summary ?? room.purpose, 150))}</p> <dl> <div><dt>states</dt><dd>${counts.states ?? 0}</dd></div> <div><dt>actions</dt><dd>${counts.actions ?? 0}</dd></div> <div><dt>artifacts</dt><dd>${counts.artifacts ?? 0}</dd></div> </dl> </article>`;}
function openQuestionsPanel(data) { const blockers = (data.blockers ?? []).slice(0, 5); return `<aside class="side-panel"> <div class="section-head"><span>needs attention</span><strong>${blockers.length}</strong></div> ${blockers.map((item) => `<div class="attention-item"><strong>${escapeHtml(item.room)}</strong><p>${escapeHtml(item.text)}</p></div>`).join("")} <div class="rule-box"><strong>Rule</strong><p>The website can help people understand and draft signals. It does not write canonical state directly.</p></div> </aside>`;}
function roomsView(data) { return shell(`<section class="page-title"> <p class="eyebrow">rooms</p> <h1>Three places for work to land.</h1> <p>Each room has a purpose, a latest state, and a trail of canonical records.</p> </section> <section class="rooms-grid">${(data.rooms ?? []).map(roomDetailCard).join("")}</section>`);}
function roomDetailCard(room) { const latest = room.latestState?.value ?? {}; const counts = room.counts ?? {}; return `<article class="room-detail room-${escapeHtml(room.name.replace("void/", ""))}"> <div class="room-detail-head"><span>${escapeHtml(ROOM_LABELS[room.name] ?? room.name)}</span><code>${escapeHtml(room.name)}</code></div> <h2>${escapeHtml(room.displayName ?? room.name)}</h2> <p>${escapeHtml(room.purpose)}</p> <div class="count-row"><span>${counts.states ?? 0} states</span><span>${counts.actions ?? 0} actions</span><span>${counts.submissions ?? 0} submissions</span><span>${counts.artifacts ?? 0} artifacts</span></div> <div class="state-note"><strong>Latest state</strong><p>${escapeHtml(latest.summary ?? "No state yet.")}</p></div> <button class="button" data-room-shortcut="${escapeHtml(room.name)}">View this room's records</button> </article>`;}
function filtersHtml(data) { const kinds = Object.keys(data.counts ?? {}).filter((kind) => kind !== "provenance"); return `<section class="filters"> <label><span>Search</span><input type="search" value="${escapeHtml(query)}" placeholder="website, state, artifact…" data-search /></label> <div><span>Room</span><div class="chips"><button class="chip ${selectedRoom === "all" ? "active" : ""}" data-room="all">all</button>${(data.rooms ?? []).map((room) => `<button class="chip ${selectedRoom === room.name ? "active" : ""}" data-room="${escapeHtml(room.name)}">${escapeHtml(ROOM_LABELS[room.name] ?? room.name)}</button>`).join("")}</div></div> <div><span>Type</span><div class="chips"><button class="chip ${selectedKind === "all" ? "active" : ""}" data-kind="all">all</button>${kinds.map((kind) => `<button class="chip ${selectedKind === kind ? "active" : ""}" data-kind="${escapeHtml(kind)}">${escapeHtml(KIND_LABELS[kind] ?? kind)}</button>`).join("")}</div></div> </section>`;}
function recordsView(data) { const records = filteredRecords(data); return shell(`<section class="page-title compact"> <p class="eyebrow">records</p> <h1>Canonical trail.</h1> <p>${totalRecords(data.counts)} records loaded from the public room projection.</p> </section> ${filtersHtml(data)} <section class="record-list wide">${records.length ? records.map(recordCard).join("") : `<article class="empty-card">No records match this filter.</article>`}</section>`);}
function recordCard(record) { const room = recordRoom(record); return `<article class="record-card room-${escapeHtml(room.replace("void/", ""))}"> <div class="record-meta"><span>${escapeHtml(KIND_LABELS[record.kind] ?? record.kind)}</span><span>${escapeHtml(room)}</span><time>${escapeHtml(formatDate(record.createdAt))}</time></div> <h3>${escapeHtml(recordTitle(record))}</h3> <p>${escapeHtml(truncate(recordText(record)))}</p> <div class="record-actions"> <button data-copy="${escapeHtml(record.uri)}">Copy AT URI</button> <button data-room-shortcut="${escapeHtml(room)}">Room</button> <code>${escapeHtml(shortUri(record.uri))}</code> </div> </article>`;}
function routeView(data) { return shell(`<section class="route-layout"> <div class="page-title compact"> <p class="eyebrow">signal</p> <h1>Route something into Void.</h1> <p>This page prepares a packet. A human or agent still accepts it before it becomes a canonical ATProto record.</p> </div> <form class="routing-form" data-routing-form> <label>Room<select name="room">${(data.rooms ?? []).map((room) => `<option value="${escapeHtml(room.name)}">${escapeHtml(room.name)}</option>`).join("")}</select></label> <label>Kind<select name="kind"><option>question</option><option>correction</option><option>artifact idea</option><option>source link</option><option>maintenance note</option></select></label> <label>Text<textarea name="text" rows="8" placeholder="What should enter the public cognition pipeline?"></textarea></label> <button class="button primary" type="submit">Copy packet</button> <output data-route-output></output> </form> </section>`);}
function proofView(data) { const proofHash = "a8935646005b8e51d827a7e44981dc173ccd8a38b7948cc0cfc5e74bfdf4094a"; return shell(`<section class="page-title compact"> <p class="eyebrow">proof</p> <h1>Saveable state.</h1> <p>The standalone proof bundle embeds the generated room projection. It is an artifact, not the primary reading experience.</p> </section> <section class="proof-actions"> <a class="button primary" href="./proof/void-room-viewer-2026-07-07.html">Open proof bundle</a> <a class="button" href="./rooms/data.json">Open data.json</a> <button class="button" data-copy="${proofHash}">Copy SHA-256</button> </section> <section class="proof-grid"> <article><strong>Generated</strong><p>${escapeHtml(formatDate(data.generatedAt, "long"))}</p></article> <article><strong>PDS</strong><p>${escapeHtml(data.pdsHost)}</p></article> <article><strong>Records</strong><p>${totalRecords(data.counts)}</p></article> </section>`);}
function render() { if (!model) return; const view = { now: nowView, rooms: roomsView, records: recordsView, route: routeView, proof: proofView }[route] ?? nowView; app.innerHTML = view(model); bindInteractions();}
function bindInteractions() { app.querySelectorAll("[data-route]").forEach((element) => { element.addEventListener("click", (event) => { const next = element.getAttribute("data-route"); if (ROUTES.includes(next)) { event.preventDefault(); goto(next); } }); }); app.querySelectorAll("[data-room]").forEach((button) => { button.addEventListener("click", () => { selectedRoom = button.getAttribute("data-room"); render(); }); }); app.querySelectorAll("[data-kind]").forEach((button) => { button.addEventListener("click", () => { selectedKind = button.getAttribute("data-kind"); render(); }); }); app.querySelector("[data-search]")?.addEventListener("input", (event) => { const cursor = event.target.selectionStart; query = event.target.value; render(); const nextInput = app.querySelector("[data-search]"); nextInput?.focus(); nextInput?.setSelectionRange(cursor, cursor); }); app.querySelectorAll("[data-room-shortcut]").forEach((button) => { button.addEventListener("click", () => { selectedRoom = button.getAttribute("data-room-shortcut"); selectedKind = "all"; query = ""; goto("records"); }); }); app.querySelectorAll("[data-copy]").forEach((button) => { button.addEventListener("click", async () => { await copyText(button.getAttribute("data-copy")); pulse(button, "Copied"); }); }); app.querySelector("[data-action='copy-feed']")?.addEventListener("click", async (event) => { const records = allRecords(model).slice(0, 6); const summary = records.map((record) => `- [${recordRoom(record)} / ${KIND_LABELS[record.kind] ?? record.kind}] ${recordTitle(record)} — ${shortUri(record.uri)}`).join("\n"); await copyText(`Void latest records\n${summary}`); pulse(event.currentTarget, "Copied"); }); app.querySelector("[data-routing-form]")?.addEventListener("submit", async (event) => { event.preventDefault(); const form = new FormData(event.currentTarget); const packet = [ "Void routing packet", `room: ${form.get("room")}`, `kind: ${form.get("kind")}`, `source: website:${location.href}`, "status: proposed", "text:", String(form.get("text") ?? "").trim() || "[empty]", ].join("\n"); await copyText(packet); app.querySelector("[data-route-output]").textContent = "Copied. Paste this into an ingress surface for review/canonicalization."; });}
async function copyText(text) { if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(text); const area = document.createElement("textarea"); area.value = text; document.body.append(area); area.select(); document.execCommand("copy"); area.remove();}
function pulse(button, label) { const original = button.textContent; button.textContent = label; button.disabled = true; setTimeout(() => { button.textContent = original; button.disabled = false; }, 1000);}
async function load() { try { const response = await fetch("./rooms/data.json", { cache: "no-store" }); if (!response.ok) throw new Error(`rooms/data.json returned HTTP ${response.status}`); model = await response.json(); route = parseRoute(); render(); } catch (error) { app.innerHTML = `<section class="site-shell"><section class="page-title"><p class="eyebrow">void.tngl.io</p><h1>Could not load Void.</h1><p>${escapeHtml(error.message)}</p></section></section>`; }}
window.addEventListener("hashchange", () => { route = parseRoute(); render(); });load();