Watch
0
0
Fork
You've already forked hyperhive
0

dashboard: SCH3DUL3S is a read-only list with a removal banner

The tab lists the rows left in the hive's store so the operator can
recreate them with `swarmctl schedule add`: no create row, edit form,
fire/pause/cancel buttons or schedules_changed subscription. The next
column shows the stored next fire time as an absolute UTC timestamp (the
value for --at) instead of a countdown. A warning banner says the page is
slated for removal and points at swarmctl. The approvals view drops the
schedule_prompt card and the approval_added subscription.
This commit is contained in:
atlas 2026-10-04 21:23:18 +02:00
commit a4a3f81d5c
8 changed files with 68 additions and 1500 deletions

View file

@ -12,7 +12,7 @@
import { $, form, appendLinkified } from "./common.js";
import { el } from "@hive/shared/dom.js";
import { epochSec, fmtAgo, fmtDuration } from "./util.js";
import { epochSec, fmtAgo } from "./util.js";
// Registered by the dashboard entry at boot; defaults to a no-op so the
// module is safe to call before wiring.
@ -117,7 +117,7 @@ export function operatorInboxAppendFromEvent(ev) {
// ─── approvals — the operator config-change approval queue ────────
const APPROVAL_TAB_KEY = "hyperhive:approvals:tab";
// Derived approval state — cold-loaded from /api/state, then mutated
// live by `approval_added` / `approval_resolved` dashboard events.
// live by `approval_resolved` dashboard events.
// `pending` is the open queue (newest-first); `history` is the last
// 30 resolved rows.
const APPROVAL_HISTORY_LIMIT = 30;
@ -129,26 +129,6 @@ export function syncApprovalsFromSnapshot(s) {
approvalsState.pending = (s.approvals || []).slice();
approvalsState.history = (s.approval_history || []).slice();
}
export function applyApprovalAdded(ev) {
// Upsert by id so a snapshot that already included the row (cold
// load + event lands at the same tick) doesn't double it.
const existing = approvalsState.pending.findIndex((a) => a.id === ev.id);
const row = {
id: ev.id,
agent: ev.agent,
kind: ev.approval_kind,
description: ev.description || null,
// The ApprovalAdded event carries no requested_at; a live-added
// approval was queued just now, so client-now is accurate — and
// consistent with how fmtAgo compares everything to client-now.
// A later /api/state cold-load swaps in the server value.
requested_at:
ev.requested_at != null ? ev.requested_at : Math.floor(Date.now() / 1000),
};
if (existing >= 0) approvalsState.pending[existing] = row;
else approvalsState.pending.push(row);
renderApprovals();
}
export function applyApprovalResolved(ev) {
// Drop from pending; prepend to history (newest-first), cap at 30.
approvalsState.pending = approvalsState.pending.filter((a) => a.id !== ev.id);
@ -175,7 +155,7 @@ export function applyApprovalResolved(ev) {
export function renderApprovals() {
const root = $("approvals-section");
// #approvals-section only lives on /dashboard.html (Y3R C4LL tab);
// no-op elsewhere — `approval_added` / `approval_resolved` SSE
// no-op elsewhere — `approval_resolved` SSE
// events route through here on every page that loads the bundle.
if (!root) return;
root.replaceChildren();
@ -223,17 +203,16 @@ export function renderApprovals() {
const ul = el("ul", { class: "approvals" });
for (const a of pending) {
const isUpdateMeta = a.kind === "update_meta_inputs";
const isSchedule = a.kind === "schedule_prompt";
const li = el("li", { class: "approval-card" });
// ── identity header ──────────────────────────────────────────
const head = el(
"div",
{ class: "approval-head" },
el("span", { class: "glyph" }, isUpdateMeta ? "↻" : "⏱"),
el("span", { class: "glyph" }, "↻"),
el("span", { class: "id" }, "#" + a.id),
el("span", { class: "agent" }, a.agent),
el("span", { class: "kind" }, isUpdateMeta ? "meta-update" : "schedule"),
el("span", { class: "kind" }, isUpdateMeta ? "meta-update" : a.kind),
);
// When the approval was requested — relative time, right-aligned.
// Goes amber once it's been pending an hour so a stale request is
@ -276,34 +255,6 @@ export function renderApprovals() {
: "bump all flake inputs",
),
);
} else if (isSchedule) {
let payload;
try {
payload = JSON.parse(a.commit_ref || "{}");
} catch (_) {
payload = {};
}
const targets = (payload.targets || []).join(", ");
const firstFire = payload.first_fire_at_unix
? new Date(payload.first_fire_at_unix * 1000).toLocaleString()
: "?";
const cadence = payload.interval_seconds
? " · ↻ every " + fmtDuration(payload.interval_seconds)
: " · one-shot";
body.append(
el(
"div",
{ class: "meta" },
"→ " + targets + " · first: " + firstFire + cadence,
),
);
if (payload.body) {
const excerpt =
payload.body.length > 80
? payload.body.slice(0, 80) + "…"
: payload.body;
body.append(el("div", { class: "approval-description" }, excerpt));
}
} else {
body.append(
el(
@ -381,7 +332,7 @@ function renderApprovalHistory(root, history) {
el(
"span",
{ class: "kind" },
a.kind === "update_meta_inputs" ? "meta-update" : "schedule",
a.kind === "update_meta_inputs" ? "meta-update" : a.kind,
),
" ",
);