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).
242 lines
7.5 KiB
JavaScript
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) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[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)),
|
|
);
|
|
}
|