The pills on 4G3NTS, R3BU1LD QU3U3 and K3PT ST4T3, the poll that fed them, the two count helpers, their styles, and the tab strip's unused badge option.
97 lines
3.1 KiB
JavaScript
97 lines
3.1 KiB
JavaScript
// 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";
|
|
};
|
|
|
|
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();
|
|
}
|