Drop the H0M3 tile hub and the core page. The dashboard is now the index; kept state is its fifth tab. A pages menu in the chrome links flow, stats, API docs and the swarm UI. The swarm apex URL comes from services.hyperhive.swarm.domain via HIVE_SWARM_PUBLIC_URL and the new swarm_public_url field of the state snapshot.
102 lines
3.3 KiB
JavaScript
102 lines
3.3 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";
|
|
};
|
|
|
|
/** 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();
|
|
}
|