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
|
|
@ -685,7 +685,7 @@ async function init() {
|
|||
await refreshState();
|
||||
|
||||
// kinds= matches SSE_HANDLERS below verbatim — narrows this from all
|
||||
// 17 wire kinds down to the 3 this page acts on. This page was one
|
||||
// 13 wire kinds down to the 3 this page acts on. This page was one
|
||||
// of 4 unfiltered `/api/dashboard/stream` subscribers before this
|
||||
// (subscription discipline, part 1 of the dashboard-event-stream-
|
||||
// split issue).
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
" ",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -269,26 +269,6 @@ code {
|
|||
color: var(--amber);
|
||||
border-color: var(--amber);
|
||||
}
|
||||
.btn-fire-now {
|
||||
color: var(--purple);
|
||||
border-color: var(--purple);
|
||||
}
|
||||
/* Post-fire flash: report renders directly on the button for ~1.5s
|
||||
so the operator sees ok/failed/missing/consumed counts inline
|
||||
without a modal. Green when at least one ok; muted otherwise. */
|
||||
.btn-fire-now-flashed {
|
||||
color: var(--green);
|
||||
border-color: var(--green);
|
||||
text-shadow: 0 0 6px currentColor;
|
||||
box-shadow: 0 0 8px -2px currentColor;
|
||||
}
|
||||
/* Inline edit button on each schedule row. Yellow reads as a
|
||||
parallel destructive-adjacent action (edit changes state, but
|
||||
isn't deletion). */
|
||||
.btn-edit-schedule {
|
||||
color: var(--yellow);
|
||||
border-color: var(--yellow);
|
||||
}
|
||||
|
||||
/* ─── file-preview / diff panel (common.js Panel) ───────────────────
|
||||
Side-panel content rendered by openFilePanel / buildTabbedPreview
|
||||
|
|
|
|||
|
|
@ -532,7 +532,7 @@ async function init() {
|
|||
await refreshState();
|
||||
|
||||
// kinds= matches SSE_HANDLERS below verbatim — narrows this from all
|
||||
// 17 wire kinds down to the 4 this page acts on. This page was one
|
||||
// 13 wire kinds down to the 4 this page acts on. This page was one
|
||||
// of 4 unfiltered `/api/dashboard/stream` subscribers before this
|
||||
// (subscription discipline, part 1 of the dashboard-event-stream-
|
||||
// split issue).
|
||||
|
|
|
|||
|
|
@ -878,131 +878,7 @@ footer .banner-thin {
|
|||
}
|
||||
|
||||
/* ─── scheduled prompts tab ────────────────────────────────────────
|
||||
Creation form at the top, list of queued schedule cards below. */
|
||||
|
||||
.schedule-edit-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6em;
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 0.8em 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.schedule-edit-form-wrapper {
|
||||
margin-top: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
border-left: 2px solid var(--yellow);
|
||||
}
|
||||
.schedule-edit-targets-note {
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
font-size: 0.85em;
|
||||
margin: 0.2em 0 0 0;
|
||||
}
|
||||
.schedule-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2em;
|
||||
}
|
||||
.schedule-field-label {
|
||||
color: var(--muted);
|
||||
font-size: 0.85em;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.schedule-field input,
|
||||
.schedule-field textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
padding: 0.35em 0.5em;
|
||||
}
|
||||
.schedule-field textarea {
|
||||
resize: vertical;
|
||||
min-height: 4em;
|
||||
}
|
||||
.schedule-targets {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4em;
|
||||
}
|
||||
.schedule-target-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4em;
|
||||
padding: 0.2em 0.6em;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
font-size: 0.85em;
|
||||
cursor: pointer;
|
||||
background: color-mix(in srgb, var(--bg-elev) 40%, transparent);
|
||||
}
|
||||
.schedule-target-chip:hover {
|
||||
border-color: var(--purple-dim);
|
||||
}
|
||||
.schedule-target-chip:has(input:checked) {
|
||||
border-color: var(--purple);
|
||||
color: var(--purple);
|
||||
background: color-mix(in srgb, var(--purple) 8%, transparent);
|
||||
}
|
||||
.schedule-target-chip input {
|
||||
margin: 0;
|
||||
}
|
||||
.schedule-actions {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.schedule-interval-presets {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3em;
|
||||
margin-top: 0.1em;
|
||||
}
|
||||
.btn-interval-preset {
|
||||
color: var(--cyan);
|
||||
border-color: var(--cyan);
|
||||
font-size: 0.75em;
|
||||
padding: 0.15em 0.55em;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.btn-interval-oneshot {
|
||||
color: var(--muted);
|
||||
border-color: var(--muted);
|
||||
}
|
||||
.schedule-interval-parts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.35em;
|
||||
margin-top: 0.4em;
|
||||
}
|
||||
.schedule-interval-num {
|
||||
width: 4em;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.schedule-interval-unit {
|
||||
color: var(--muted);
|
||||
font-size: 0.85em;
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
.schedule-interval-preview {
|
||||
margin-top: 0.4em;
|
||||
color: var(--cyan);
|
||||
font-size: 0.9em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.schedule-interval-preview-oneshot {
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
}
|
||||
Read-only table of the schedules left in the hive's store. */
|
||||
|
||||
.schedules-table-wrap {
|
||||
overflow-x: auto;
|
||||
|
|
@ -1031,12 +907,10 @@ footer .banner-thin {
|
|||
width: 3em;
|
||||
text-align: center;
|
||||
}
|
||||
/* next / every columns: sized to typical content (short durations).
|
||||
Previous 8em / 7em wasted horizontal space. */
|
||||
.schedules-table-next-col {
|
||||
width: 5.5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* every column: sized to typical content (short durations). */
|
||||
.schedules-table-every-col {
|
||||
width: 5em;
|
||||
white-space: nowrap;
|
||||
|
|
@ -1044,9 +918,6 @@ footer .banner-thin {
|
|||
.schedules-table-body-th {
|
||||
min-width: 12em;
|
||||
}
|
||||
.schedules-table-actions-th {
|
||||
width: 7em;
|
||||
}
|
||||
/* Agent-name columns: 90° vertical text (writing-mode) so names are fully
|
||||
readable without truncation. Previously used a -45° CSS transform which
|
||||
clipped names mid-glyph. writing-mode + rotate(180deg) is in-flow and
|
||||
|
|
@ -1078,8 +949,7 @@ footer .banner-thin {
|
|||
padding: 0 2px;
|
||||
}
|
||||
/* Column for an agent no longer in the live roster — its past schedules
|
||||
linger but the agent is gone. Greyed + struck through so the operator
|
||||
can still see (and cancel) the stale entries rather than be surprised. */
|
||||
linger but the agent is gone. Greyed + struck through. */
|
||||
.schedules-table-agent-th--gone > div {
|
||||
color: var(--muted);
|
||||
text-decoration: line-through;
|
||||
|
|
@ -1095,12 +965,6 @@ footer .banner-thin {
|
|||
color: var(--yellow);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.btn-pause-schedule {
|
||||
color: var(--teal);
|
||||
}
|
||||
.btn-resume-schedule {
|
||||
color: var(--green);
|
||||
}
|
||||
.schedules-table-body-cell {
|
||||
max-width: 30em;
|
||||
overflow: hidden;
|
||||
|
|
@ -1112,135 +976,13 @@ footer .banner-thin {
|
|||
.schedules-table-check {
|
||||
width: 28px;
|
||||
text-align: center;
|
||||
padding: 0.3em 0 !important;
|
||||
}
|
||||
.schedules-table-check-btn {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--green);
|
||||
cursor: pointer;
|
||||
padding: 0.1em 0.3em;
|
||||
font-family: inherit;
|
||||
font-size: 1em;
|
||||
}
|
||||
.schedules-table-check-btn:hover:not(:disabled) {
|
||||
color: var(--red);
|
||||
}
|
||||
.schedules-table-check-btn:disabled {
|
||||
cursor: default;
|
||||
padding: 0.3em 0 !important;
|
||||
}
|
||||
.schedules-table-check-cancelled {
|
||||
color: var(--muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.schedules-table-actions {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.schedules-table-actions .btn-inline-small {
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
.schedules-table-edit-row > td {
|
||||
/* Off-palette dim-purple surface tint: not an exact theme var, left as a
|
||||
literal pending a dedicated named var so a theme swap can recolour it. */
|
||||
background: rgba(38, 32, 60, 0.35);
|
||||
padding: 0.5em 0.6em;
|
||||
}
|
||||
.schedules-table-create-row td {
|
||||
/* Off-palette dim-purple surface tint — see the edit-row note above. */
|
||||
background: rgba(38, 32, 60, 0.25);
|
||||
border-top: 2px solid var(--border);
|
||||
vertical-align: top;
|
||||
padding: 0.4em 0.5em;
|
||||
}
|
||||
.schedules-table-create-cell {
|
||||
padding: 0.35em 0.4em;
|
||||
}
|
||||
.schedules-table-inline-input,
|
||||
.schedules-table-inline-textarea,
|
||||
.schedules-table-inline-num {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--fg);
|
||||
font-family: ui-monospace, "JetBrains Mono", monospace;
|
||||
font-size: 0.85em;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.schedules-table-inline-input:focus,
|
||||
.schedules-table-inline-textarea:focus,
|
||||
.schedules-table-inline-num:focus {
|
||||
border-color: var(--purple);
|
||||
outline: none;
|
||||
}
|
||||
.schedules-table-inline-textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 1.6em;
|
||||
resize: vertical;
|
||||
}
|
||||
.schedules-table-create-body:focus-within .schedules-table-inline-textarea {
|
||||
min-height: 4em;
|
||||
}
|
||||
.schedules-table-inline-desc {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
.schedules-table-inline-datetime {
|
||||
width: 100%;
|
||||
font-size: 0.78em;
|
||||
}
|
||||
.schedules-table-inline-interval {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.1em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.schedules-table-inline-num {
|
||||
width: 2.4em;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
.schedules-table-inline-num::-webkit-outer-spin-button,
|
||||
.schedules-table-inline-num::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
.schedules-table-inline-unit {
|
||||
color: var(--muted);
|
||||
font-size: 0.7em;
|
||||
margin-right: 0.15em;
|
||||
}
|
||||
.schedules-table-inline-check {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
.schedules-table-inline-check-lbl {
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
padding: 0.4em 0.6em;
|
||||
}
|
||||
.schedules-table-create-submit {
|
||||
font-size: 1em;
|
||||
padding: 0.15em 0.55em;
|
||||
color: var(--green);
|
||||
border-color: var(--green);
|
||||
}
|
||||
.schedules-table-create-reset {
|
||||
font-size: 0.85em;
|
||||
padding: 0.15em 0.45em;
|
||||
color: var(--muted);
|
||||
border-color: var(--border);
|
||||
}
|
||||
.btn-inline-small {
|
||||
font-size: 0.7em;
|
||||
padding: 0.1em 0.45em;
|
||||
}
|
||||
|
||||
/* Selection bar — sticky-bottom strip for bulk actions. */
|
||||
.selection-bar {
|
||||
|
|
|
|||
|
|
@ -68,8 +68,7 @@
|
|||
<span class="tab-label">◆ P3RM1SS10NS ◆</span>
|
||||
</a>
|
||||
|
||||
<!-- SCH3DUL3S: scheduled-prompts surface. List of queued
|
||||
schedules + an operator-direct creation form. Count pill
|
||||
<!-- SCH3DUL3S: read-only scheduled-prompts list. Count pill
|
||||
mirrors the active (non-cancelled) schedule count; hidden
|
||||
when zero. -->
|
||||
<a
|
||||
|
|
@ -285,15 +284,10 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SCH3DUL3S: scheduled prompts. Creation + edit are folded
|
||||
into the same table — empty bottom row is the create form
|
||||
(fill cells, click +), inline-edit-row expands on the `✎`
|
||||
toggle for existing schedules. Schedules list driven by
|
||||
GET /api/schedules; POST /api/schedules to create, PATCH
|
||||
/api/schedules/{id} to edit, POST /api/schedules/{id}/cancel
|
||||
for per-target / whole-row cancel. Live updates via
|
||||
`schedules_changed` SSE; tab activation re-fetches as a
|
||||
safety net for disconnect windows.
|
||||
<!-- SCH3DUL3S: read-only list of the scheduled prompts left in the
|
||||
hive's store, driven by GET /api/schedules (re-fetched on tab
|
||||
activation). Nothing on the hive fires them; the banner points
|
||||
at `swarmctl schedule`, where schedules live now.
|
||||
See docs/web-ui/dashboard.md::SCH3DUL3S tab. -->
|
||||
<section
|
||||
class="tab-pane"
|
||||
|
|
@ -307,11 +301,15 @@
|
|||
<div class="divider">
|
||||
══════════════════════════════════════════════════════════════
|
||||
</div>
|
||||
<p class="server-warn server-warn-warn" role="note">
|
||||
⚠ this page is slated for removal. the hive no longer fires these
|
||||
schedules — they live at swarm level now: recreate each one with
|
||||
<code>swarmctl schedule add</code> (see
|
||||
<code>docs/swarm/schedules.md</code>).
|
||||
</p>
|
||||
<p class="meta">
|
||||
all schedules currently in the table. fill the bottom row to queue a
|
||||
new schedule (operator-direct, no approval gate; recurring when an
|
||||
interval is set). cancel a single target with the row button or the
|
||||
whole schedule with <code>✕ cancel all</code>.
|
||||
read-only: every schedule still in the hive's table, kept so it can
|
||||
be copied over.
|
||||
</p>
|
||||
<div id="schedules-section">
|
||||
<p class="meta">loading…</p>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -17,7 +17,7 @@ import { el } from "@hive/shared/dom.js";
|
|||
import { bindAsyncForms } from "@hive/shared/forms.js";
|
||||
import { createTabStrip } from "@hive/shared/tabs.js";
|
||||
import { containersState, syncContainersFromSnapshot } from "./state.js";
|
||||
import { fmtAgo, fmtDuration } from "./util.js";
|
||||
import { fmtAgo } from "./util.js";
|
||||
import {
|
||||
applyCapabilitiesChanged,
|
||||
applyToolGroupsChanged,
|
||||
|
|
@ -25,18 +25,13 @@ import {
|
|||
fetchAndRenderToolGroups,
|
||||
initPermissions,
|
||||
} from "./permissions.js";
|
||||
import {
|
||||
applySchedulesChanged,
|
||||
refreshSchedules,
|
||||
activeScheduleCount,
|
||||
} from "./schedules.js";
|
||||
import { refreshSchedules, activeScheduleCount } from "./schedules.js";
|
||||
import {
|
||||
initCall,
|
||||
refreshOperatorInbox,
|
||||
operatorInboxAppendFromEvent,
|
||||
operatorInboxCount,
|
||||
syncApprovalsFromSnapshot,
|
||||
applyApprovalAdded,
|
||||
applyApprovalResolved,
|
||||
renderApprovals,
|
||||
activeApprovalCount,
|
||||
|
|
@ -95,11 +90,9 @@ window.marked = marked;
|
|||
// opts out via `data-no-refresh` (its mutation arrives via an SSE event).
|
||||
bindAsyncForms(() => refreshState());
|
||||
|
||||
// One ticker feeds two live displays that would otherwise only
|
||||
// update on the next SSE-triggered re-render: approval request-age
|
||||
// chips (`.approval-ts`, flips `.stale` at the 1h mark) and schedule
|
||||
// next-fire countdowns (`.sched-due`, stamped with `data-due-at` by
|
||||
// the renderers so this loop can refresh them without a full re-render).
|
||||
// Refreshes approval request-age chips (`.approval-ts`, flips `.stale`
|
||||
// at the 1h mark), which would otherwise only update on the next
|
||||
// SSE-triggered re-render.
|
||||
setInterval(() => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
document
|
||||
|
|
@ -111,13 +104,6 @@ window.marked = marked;
|
|||
node.textContent = "requested " + fmtAgo(requestedAt);
|
||||
node.classList.toggle("stale", ageSec >= 3600);
|
||||
});
|
||||
document.querySelectorAll(".sched-due[data-due-at]").forEach((node) => {
|
||||
const dueAt = Number(node.getAttribute("data-due-at"));
|
||||
if (!Number.isFinite(dueAt)) return;
|
||||
const dueIn = dueAt - now;
|
||||
node.textContent =
|
||||
dueIn <= 0 ? "overdue " + fmtAgo(dueAt) : fmtDuration(dueIn);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
// ─── state polling ──────────────────────────────────────────────────────
|
||||
|
|
@ -284,7 +270,6 @@ window.marked = marked;
|
|||
// and filter client-side — the dashboard ignores broker traffic
|
||||
// and the inbox ignores mutation events.
|
||||
const MUTATION_HANDLERS = {
|
||||
approval_added: applyApprovalAdded,
|
||||
approval_resolved: applyApprovalResolved,
|
||||
transient_set: applyTransientSet,
|
||||
transient_cleared: applyTransientCleared,
|
||||
|
|
@ -295,7 +280,6 @@ window.marked = marked;
|
|||
// /builds.html gives it for its JobqGraph mount handle's .refresh()
|
||||
// (its own separate subscription).
|
||||
rebuild_queue_changed: applyRebuildQueueChanged,
|
||||
schedules_changed: applySchedulesChanged,
|
||||
capabilities_changed: applyCapabilitiesChanged,
|
||||
tool_groups_changed: applyToolGroupsChanged,
|
||||
};
|
||||
|
|
@ -309,15 +293,15 @@ window.marked = marked;
|
|||
//
|
||||
// kinds= matches MUTATION_HANDLERS below verbatim, plus `sent`
|
||||
// (checked separately, just above, for the operator inbox) —
|
||||
// narrows this from all 15 wire kinds down to the 11 this page
|
||||
// narrows this from all 13 wire kinds down to the 9 this page
|
||||
// actually acts on. This page was one of 4 unfiltered
|
||||
// `/api/dashboard/stream` subscribers before this (subscription
|
||||
// discipline, part 1 of the dashboard-event-stream-split issue).
|
||||
const es = openStream(
|
||||
"/api/dashboard/stream?kinds=sent,approval_added,approval_resolved," +
|
||||
"/api/dashboard/stream?kinds=sent,approval_resolved," +
|
||||
"transient_set,transient_cleared," +
|
||||
"container_state_changed,container_removed,rebuild_queue_changed," +
|
||||
"schedules_changed,capabilities_changed,tool_groups_changed",
|
||||
"capabilities_changed,tool_groups_changed",
|
||||
);
|
||||
es.onmessage = (e) => {
|
||||
let ev;
|
||||
|
|
@ -436,8 +420,7 @@ window.marked = marked;
|
|||
const rawTitle = document.title.replace(/^\(\d+\) /, "");
|
||||
document.title = callCount > 0 ? `(${callCount}) ${rawTitle}` : rawTitle;
|
||||
// SCH3DUL3S — count of schedules with at least one still-active
|
||||
// target (whole-schedule cancellation or all-targets-cancelled
|
||||
// means "not waiting on the worker"; those don't pull attention).
|
||||
// target: the ones left to recreate with `swarmctl schedule`.
|
||||
setTabCount("schedules", activeScheduleCount());
|
||||
}
|
||||
// Poll the state stores on a 1s tick to keep the pill counts in
|
||||
|
|
|
|||
Loading…
Reference in a new issue