arborist-viz/arborist_viz/static/viz/sse-client.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

82 lines
2.2 KiB
JavaScript

// SSE client — EventSource is browser-native, no extra deps.
// Streams audit/run/root/claim/dashboard/mesh events into the sidebar.
export function connectSSE(url, { statusEl, eventsEl }) {
setStatus(statusEl, "connecting", "dot-off");
const src = new EventSource(url);
src.addEventListener("ready", (ev) => {
setStatus(statusEl, "live", "dot-on");
appendEvent(eventsEl, "ready", ev.data);
appendAuditEvent("ready", ev.data);
});
src.addEventListener("heartbeat", (ev) => {
appendEvent(eventsEl, "heartbeat", ev.data);
});
src.addEventListener("patch", (ev) => {
appendEvent(eventsEl, "patch", ev.data);
appendAuditEvent("patch", ev.data);
// future: apply ev.data as a GraphPatch.
});
src.addEventListener("error", () => {
setStatus(statusEl, "reconnecting…", "dot-err");
});
src.addEventListener("open", () => {
setStatus(statusEl, "live", "dot-on");
});
return src;
}
function setStatus(el, label, dotClass) {
if (!el) return;
el.innerHTML = "";
const dot = document.createElement("span");
dot.className = `dot ${dotClass}`;
const lbl = document.createElement("span");
lbl.className = "lbl";
lbl.textContent = label;
el.appendChild(dot);
el.appendChild(lbl);
}
function appendEvent(el, name, data) {
if (!el) return;
const ts = new Date().toISOString().slice(11, 19);
const line = `[${ts}] ${name}\n${prettyJSON(data)}\n`;
el.textContent = (line + el.textContent).slice(0, 8192);
}
function appendAuditEvent(name, data) {
const list = document.getElementById("audit-chain-live");
if (!list) return;
if (list.firstElementChild && list.firstElementChild.classList.contains("muted")) {
list.innerHTML = "";
}
const li = document.createElement("li");
li.className = `evt evt-${name}`;
li.textContent = `${new Date().toISOString().slice(11, 19)} ${name} ${oneline(data)}`;
list.insertBefore(li, list.firstChild);
while (list.children.length > 50) list.removeChild(list.lastChild);
}
function prettyJSON(s) {
try {
return JSON.stringify(JSON.parse(s), null, 2);
} catch {
return String(s);
}
}
function oneline(s) {
try {
return JSON.stringify(JSON.parse(s));
} catch {
return String(s);
}
}