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:
parent
c3a54bbdd0
commit
a4a3f81d5c
8 changed files with 68 additions and 1500 deletions
|
|
@ -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,
|
||||
),
|
||||
" ",
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue