Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/dashboard/src/kept.js
iris 0622736b30 dashboard: serve the dashboard at /, fold kept state in as a tab, add a pages menu
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.
2026-10-16 20:02:28 +02:00

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();
}