arborist-viz/arborist_viz/static/viz/dashboard.js
russell@unturf.com 0cd96ab1d7
Initial commit: arborist VIZ — Merkle command center
Pyramid web app for inspecting arborist Merkle trees over sharded SQLite:
document roots, inclusion proofs, an interactive 3D Merkle lattice
(six.js / WebGL with graceful fallback), audit-chain linkage, and a live
SSE event stream.

Generated artifacts (env/, vendored static/six/ bundles, demo databases)
are gitignored and rebuilt via the Makefile (venv, six, init-db, serve).
2026-05-28 09:22:59 -04:00

242 lines
7.5 KiB
JavaScript

// arborist VIZ — vanilla dashboard composer.
// Fetches /api/dashboards/{id} and renders each widget into the grid.
// No build step, no React. Module-script ES2020.
import { shortHex } from "./hash.js";
const WIDGET_RENDERERS = {
"root-explorer": renderRootExplorer,
"audit-timeline": renderAuditTimeline,
"claim-warrant": renderClaimWarrant,
"run-dag": renderRunDag,
"claim-graveyard": renderClaimGraveyard,
"merkle-proof": renderMerkleProof,
"cache-key": renderGeneric,
"root-diff": renderGeneric,
"claim-graph": renderGeneric,
"mesh-peer": renderGeneric,
"circuit-trace": renderGatedCircuit,
"merkle-lattice-3d": renderGated3D,
};
export async function mountDashboard(rootEl, dashboardId) {
const grid = rootEl.querySelector(".dash-grid");
grid.innerHTML = "";
// Sidebar: list every document_root in the shard, clickable → /proof/:root.
populateRootsSidebar();
populateLiveAudit();
let cfg;
try {
const res = await fetch(`/api/dashboards/${encodeURIComponent(dashboardId)}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
cfg = await res.json();
} catch (err) {
grid.appendChild(errorWidget(`failed to load dashboard ${dashboardId}: ${err.message}`));
return;
}
document.title = `${cfg.title} — Arborist VIZ`;
for (const w of cfg.widgets || []) {
const renderer = WIDGET_RENDERERS[w.type] || renderGeneric;
const node = renderer(w);
node.classList.add(`widget-${w.type}`);
node.dataset.widgetId = w.widget_id;
grid.appendChild(node);
}
}
async function populateRootsSidebar() {
const ul = document.getElementById("dash-roots");
if (!ul) return;
try {
const res = await fetch("/api/roots?limit=100");
const j = await res.json();
if (j.error || !j.roots || j.roots.length === 0) {
ul.innerHTML = `<li class="muted">no roots in shard (configure arborist.shards)</li>`;
return;
}
ul.innerHTML = "";
for (const r of j.roots) {
const li = document.createElement("li");
li.innerHTML = `
<a href="/proof/${r.document_root}" class="root-link" title="${r.document_root}">
<span class="root-title">${escapeHTML(r.title || r.document_uri || "(untitled)")}</span>
<span class="root-meta">${r.leaf_count}L · ${escapeHTML(r.source_type)}</span>
<code class="root-hash-short">${shortHex(r.document_root)}</code>
</a>`;
ul.appendChild(li);
}
} catch (err) {
ul.innerHTML = `<li class="muted">roots lookup failed: ${err.message}</li>`;
}
}
async function populateLiveAudit() {
// Backfill the audit-timeline widget with recent events even before
// SSE delivers any new ones.
const list = document.getElementById("audit-chain-live");
if (!list) return;
try {
const res = await fetch("/api/audit/recent?limit=20");
const j = await res.json();
if (!j.events || j.events.length === 0) return;
list.innerHTML = "";
for (const ev of j.events) {
const li = document.createElement("li");
li.innerHTML = `<span class="lbl">[${ev.event_type}]</span> <a href="/audit/${ev.event_hash}" class="mono" title="${ev.event_hash}">${shortHex(ev.event_hash)}</a> ${ev.subject_root ? `subj=<a class="mono" href="/proof/${ev.subject_root}" title="${ev.subject_root}">${shortHex(ev.subject_root)}</a>` : ""}`;
list.appendChild(li);
}
} catch {}
}
function escapeHTML(s) {
return (s || "").replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
}
function el(tag, attrs = {}, ...children) {
const node = document.createElement(tag);
for (const [k, v] of Object.entries(attrs)) {
if (k === "class") node.className = v;
else if (k === "html") node.innerHTML = v;
else node.setAttribute(k, v);
}
for (const c of children) {
if (c == null) continue;
node.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
}
return node;
}
function widgetShell(w, bodyEl) {
return el(
"article",
{ class: "widget" },
el(
"header",
{ class: "widget-head" },
el("span", { class: "widget-type" }, `[${w.type}]`),
el("span", { class: "widget-title" }, w.title || w.widget_id),
),
el("div", { class: "widget-body" }, bodyEl),
el(
"footer",
{ class: "widget-foot" },
el(
"details",
{},
el("summary", {}, "query + source roots"),
el("pre", {}, JSON.stringify({ query: w.query, source_roots: w.source_roots }, null, 2)),
),
),
);
}
function renderGeneric(w) {
return widgetShell(w, el("p", { class: "muted" }, "Phase 0 stub — renderer not yet specialized."));
}
function renderRootExplorer(w) {
const body = el(
"table",
{ class: "widget-table" },
el("thead", {}, el("tr", {}, el("th", {}, "root"), el("th", {}, "leaves"), el("th", {}, "created"))),
el("tbody", {}, el("tr", {}, el("td", { colspan: "3", class: "muted" }, "waiting on arborist shard wiring"))),
);
return widgetShell(w, body);
}
function renderAuditTimeline(w) {
const body = el(
"ol",
{ class: "audit-chain", id: "audit-chain-live" },
el("li", { class: "muted" }, "waiting for SSE events on /sse/audit/live…"),
);
return widgetShell(w, body);
}
function renderClaimWarrant(w) {
const ladder = el(
"ul",
{ class: "warrant-ladder" },
el("li", {}, "UNGROUNDED"),
el("li", {}, "POINTER_LINKED"),
el("li", {}, "ANCHOR_WARRANTED"),
el("li", {}, "EVIDENCE_WARRANTED"),
el("li", {}, "FORMALLY_PROVED"),
);
return widgetShell(w, ladder);
}
function renderRunDag(w) {
const stages = [
"Question", "Preflight", "Retrieval", "EvidenceMap", "AnswerDraft",
"ClaimExtraction", "Verifier", "FalsifierSearch", "CacheWrite",
"AuditAppend", "StateAdmission",
];
const ol = el("ol", { class: "dag-stages" }, ...stages.map((s) => el("li", {}, s)));
return widgetShell(w, ol);
}
function renderClaimGraveyard(w) {
const body = el(
"table",
{ class: "widget-table" },
el(
"thead",
{},
el("tr", {}, el("th", {}, "failed_claim_hash"), el("th", {}, "failure_class"), el("th", {}, "falsifiers")),
),
el(
"tbody",
{},
el(
"tr",
{},
el("td", { colspan: "3", class: "muted" }, "recurring-error surface — bounded ingestion per #000059"),
),
),
);
return widgetShell(w, body);
}
function renderMerkleProof(w) {
const body = el(
"div",
{},
el("p", {}, "Inclusion proof verifies integrity, not semantic truth."),
el("p", { class: "muted" }, "Phase 0: server computes proof at /api/proof/:root/:leaf. Browser-side verify deferred from v1 per §3-corrected."),
);
return widgetShell(w, body);
}
function renderGatedCircuit(w) {
const body = el(
"div",
{},
el("p", {}, "Activation/circuit traces are gated on arborist #000062 (MechanisticWitnessRoot)."),
el("p", { class: "muted" }, "/api/trace/* and /api/circuit/* return 501 until the witness producer lands."),
);
return widgetShell(w, body);
}
function renderGated3D(w) {
const body = el(
"div",
{},
el("p", {}, "3D Merkle lattice ships in Phase 5 via six.js."),
el("p", { class: "muted" }, "six.js bundles are vendored under /static/six/."),
);
return widgetShell(w, body);
}
function errorWidget(msg) {
return el(
"article",
{ class: "widget" },
el("header", { class: "widget-head" }, el("span", { class: "widget-type" }, "[error]")),
el("div", { class: "widget-body" }, el("p", { class: "muted" }, msg)),
);
}