// K3PT ST4T3 tab of the dashboard: kept state from agents whose container // is absent (tombstones), with a purge button per row. Owns its slice of // state like agents.js / builds.js do; tabs.js feeds it the /api/state // snapshot and the `tombstones_changed` stream event. import { $, form } from "./common.js"; import { el } from "@hive/shared/dom.js"; let tombstonesState = []; const fmtBytes = (n) => { if (n < 1024) return n + " B"; if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB"; if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + " MB"; return (n / (1024 * 1024 * 1024)).toFixed(2) + " GB"; }; const fmtAgeDays = (ts) => { if (!ts) return "?"; const d = Math.floor((Date.now() / 1000 - ts) / 86400); if (d <= 0) return "today"; if (d === 1) return "1 day ago"; return d + " days ago"; }; /** Number of kept-state entries, for the tab's count pill. */ export function keptCount() { return tombstonesState.length; } export function syncKeptFromSnapshot(s) { tombstonesState = (s.tombstones || []).slice(); } export function renderKept() { const root = $("tombstones-section"); if (!root) return; root.replaceChildren(); if (!tombstonesState.length) { root.append(el("p", { class: "empty" }, "no kept state — clean")); return; } // Only shown alongside actual rows — a caveat over an empty list is noise. // Wording is deliberately about what the list *is* rather than what it // isn't: nothing records a destroy, so "container absent" is the only thing // the backend can actually tell. const warn = el("hive-warn", { level: "warning" }); warn.append( el("strong", {}, "shows every agent whose container is absent"), " — not only destroyed ones. An agent part-way through being spawned " + "looks identical here, because nothing records a destroy. Check it is " + "really gone before you PURG3.", ); root.append(warn); const ul = el("ul", { class: "containers" }); for (const t of tombstonesState) { const li = el("li", { class: "container-row tombstone" }); const head = el("div", { class: "head" }); head.append( el("span", { class: "name" }, t.name), // `offline` is what the absence of a container proves; `destroyed` // is something the backend cannot know — see the caveat above. el("span", { class: "hive-pill-sm badge-muted" }, "offline"), ); if (t.has_creds) head.append( el("span", { class: "hive-pill-sm badge-muted" }, "creds kept"), ); head.append( el( "span", { class: "meta" }, `${fmtBytes(t.state_bytes)} · ${fmtAgeDays(t.last_seen)}`, ), ); li.append(head); const actions = el("div", { class: "actions" }); actions.append( form( "/api/purge-tombstone/" + t.name, "btn-destroy", "PURG3", "PURGE " + t.name + "? config history, claude creds, " + "and notes are all WIPED. no undo.", {}, { noRefresh: true }, ), ); li.append(actions); ul.append(li); } root.append(ul); } /** `tombstones_changed` stream event: replace the list and re-render. */ export function applyTombstonesChanged(ev) { tombstonesState = (ev.tombstones || []).slice(); renderKept(); }