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).
82 lines
2.2 KiB
JavaScript
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);
|
|
}
|
|
}
|