Watch
0
0
Fork
You've already forked hyperhive
0

dashboard: drop the approvals queue

Y3R C4LL is the operator inbox alone: no P3NDING APPR0VALS section, no
approval cards or history, no approval_resolved subscription, no
approval browser notifications, and the tab count and title prefix count
unread operator messages only. The H0M3 tile text drops "approvals".

The deny form was the only data-prompt form, so bindAsyncForms' prompt
branch, themedPrompt and its dialog styles go too, along with the
approval-only CSS (.approval*, .approvals-history, .btn-deny, the
status/glyph colours and .history-note).
This commit is contained in:
atlas 2026-10-04 23:55:14 +02:00
commit ac1caef972
13 changed files with 32 additions and 584 deletions

View file

@ -1,9 +1,8 @@
// /dashboard.html entry point: tab renderers + tab routing + refreshState
// + notification deltas. Reads /api/state on cold load and after every
// async-form submit; live updates run through `applyXxx` mutation
// handlers triggered by the dashboard event stream (subscribed via
// `openStream` from common.js). See docs/web-ui/shape.md::Shape (shared by
// both) for the broader contract.
// /dashboard.html entry point: tab renderers + tab routing + refreshState.
// Reads /api/state on cold load and after every async-form submit; live
// updates run through `applyXxx` mutation handlers triggered by the
// dashboard event stream (subscribed via `openStream` from common.js). See
// docs/web-ui/shape.md::Shape (shared by both) for the broader contract.
//
// Pure helpers (DOM, side panel, OS notifications, path linkification)
// live in `./common.js`; the flow-page surface (operator inbox, broker
@ -17,7 +16,6 @@ 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 } from "./util.js";
import {
applyCapabilitiesChanged,
applyToolGroupsChanged,
@ -31,10 +29,6 @@ import {
refreshOperatorInbox,
operatorInboxAppendFromEvent,
operatorInboxCount,
syncApprovalsFromSnapshot,
applyApprovalResolved,
renderApprovals,
activeApprovalCount,
} from "./call.js";
import {
initJobqRollup,
@ -54,58 +48,12 @@ import {
window.marked = marked;
(() => {
// Track which items we've already notified about so a re-render
// doesn't re-fire for the same row. Keyed by stable ids; reset only
// when the page reloads.
const seenApprovals = new Set();
let seededNotify = false;
function notifyDeltas(s) {
const approvals = s.approvals || [];
if (!seededNotify) {
// First render after page load — fill the "seen" set without
// firing notifications. We only want to notify on NEW items
// that arrived while the page is open. The inbox no longer
// needs seeding here: it's derived from the broker stream which
// does its own per-event notification on live arrival, and
// history-replayed events are silent by virtue of `fromHistory`.
for (const a of approvals) seenApprovals.add(a.id);
seededNotify = true;
return;
}
for (const a of approvals) {
if (seenApprovals.has(a.id)) continue;
seenApprovals.add(a.id);
NOTIF.show(
"◆ approval #" + a.id,
`config commit for ${a.agent}`,
"hyperhive:approval:" + a.id,
);
}
}
// ─── async forms ────────────────────────────────────────────────────────
// Shared `data-async` submit interceptor (now in common.js so /core.html's
// bundle gets it too). On success it re-runs refreshState unless the form
// opts out via `data-no-refresh` (its mutation arrives via an SSE event).
bindAsyncForms(() => refreshState());
// 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
.querySelectorAll(".approval-ts[data-requested-at]")
.forEach((node) => {
const requestedAt = Number(node.getAttribute("data-requested-at"));
if (!Number.isFinite(requestedAt)) return;
const ageSec = Math.max(0, now - requestedAt);
node.textContent = "requested " + fmtAgo(requestedAt);
node.classList.toggle("stale", ageSec >= 3600);
});
}, 1000);
// ─── state polling ──────────────────────────────────────────────────────
let pollTimer = null;
// Sections whose innerHTML gets blown away on each refresh. If the
@ -114,7 +62,6 @@ window.marked = marked;
const MANAGED_SECTION_IDS = [
"containers-section",
"inbox-section",
"approvals-section",
"schedules-section",
"capabilities-section",
"tool-groups-section",
@ -211,18 +158,12 @@ window.marked = marked;
// mount's own effect handles the actual fetch.
initJobqRollup();
renderContainers(s);
// Sync the derived approvals store from the snapshot, then
// render. Live `*_added` / `*_resolved` events mutate the store
// directly and re-render without a snapshot refetch.
syncApprovalsFromSnapshot(s);
renderApprovals();
refreshSchedules();
restoreOpenDetails(openDetails);
notifyDeltas(s);
// No periodic refresh timer. Phase 6 covers every container
// mutation with `ContainerStateChanged` / `ContainerRemoved`
// (lifecycle ops, destroy, rebuild, crash_watch's 10s poll);
// approvals + transients have their own events;
// transients have their own events;
// broker traffic flows through the SSE channel. The only
// /api/state fetches are the initial cold load and the
// post-submit refetch on forms without `data-no-refresh`
@ -246,7 +187,7 @@ window.marked = marked;
refreshOperatorInbox();
// NOTIF.bind() wires the enable/mute/unmute toggle buttons that now
// live in the Y3R C4LL tab's ◆ PR3F3R3NC3S ◆ section. NOTIF.show()
// (approval/inbox notifications) reads the same browser permission +
// (inbox notifications) reads the same browser permission +
// localStorage mute flag regardless of which page fires it — bind()
// only needs to run once, here, where the buttons live. The side
// panel (<hive-side-panel>) wires its own internals in
@ -270,7 +211,6 @@ window.marked = marked;
// and filter client-side — the dashboard ignores broker traffic
// and the inbox ignores mutation events.
const MUTATION_HANDLERS = {
approval_resolved: applyApprovalResolved,
transient_set: applyTransientSet,
transient_cleared: applyTransientCleared,
container_state_changed: applyContainerStateChanged,
@ -293,12 +233,12 @@ window.marked = marked;
//
// kinds= matches MUTATION_HANDLERS below verbatim, plus `sent`
// (checked separately, just above, for the operator inbox) —
// narrows this from all 13 wire kinds down to the 9 this page
// narrows this from all 12 wire kinds down to the 8 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_resolved," +
"/api/dashboard/stream?kinds=sent," +
"transient_set,transient_cleared," +
"container_state_changed,container_removed,rebuild_queue_changed," +
"capabilities_changed,tool_groups_changed",
@ -358,8 +298,7 @@ window.marked = marked;
document.body.dataset.activeTab = target;
renderSelectionBar(Array.from(containersState.values()));
// Re-fetch on activation as a safety net: SSE covers live mutations,
// re-sync covers disconnect windows / approval-path inserts that
// don't yet emit.
// re-sync covers disconnect windows.
if (target === "schedules") {
refreshSchedules();
}
@ -410,8 +349,8 @@ window.marked = marked;
if (c.needs_update) swarm++;
}
setTabCount("swarm", swarm);
// Y3R C4LL — pending approvals + unread agent->operator messages.
const callCount = activeApprovalCount() + operatorInboxCount();
// Y3R C4LL — unread agent->operator messages.
const callCount = operatorInboxCount();
setTabCount("call", callCount);
// Browser tab title prefix — lets the operator see the pending
// call count without switching to the window. Strips any existing