diff --git a/frontend/packages/dashboard/src/call.js b/frontend/packages/dashboard/src/call.js index 01f82015..41a34300 100644 --- a/frontend/packages/dashboard/src/call.js +++ b/frontend/packages/dashboard/src/call.js @@ -1,5 +1,5 @@ // Y3R C4LL domain — the dashboard pane for things blocked on an operator -// decision: approvals and the operator inbox (messages agents sent to +// decision: the operator inbox (messages agents sent to // `to="operator"`). Lifted out of the dashboard entry (tabs.js) into its // own module so the entry stays a thin coordinator; mirrors the // permissions.js / schedules.js extractions. @@ -10,9 +10,8 @@ // live-mutation paths call an injected `onCountsChanged` callback the entry // registers once via `initCall`. -import { $, form, appendLinkified } from "./common.js"; +import { $, appendLinkified } from "./common.js"; import { el } from "@hive/shared/dom.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. @@ -113,240 +112,3 @@ export function operatorInboxAppendFromEvent(ev) { renderOperatorInbox(); onCountsChanged(); } - -// ─── 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_resolved` dashboard events. -// `pending` is the open queue (newest-first); `history` is the last -// 30 resolved rows. -const APPROVAL_HISTORY_LIMIT = 30; -const approvalsState = { pending: [], history: [] }; -export function activeApprovalCount() { - return approvalsState.pending.length; -} -export function syncApprovalsFromSnapshot(s) { - approvalsState.pending = (s.approvals || []).slice(); - approvalsState.history = (s.approval_history || []).slice(); -} -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); - // Idempotent: a snapshot re-sync (post-disconnect SSE catchup) can - // carry this same resolved row in `approval_history` while a live - // event also delivers it — guard the unshift so history can't - // double a row. - if (!approvalsState.history.some((h) => h.id === ev.id)) { - approvalsState.history.unshift({ - id: ev.id, - agent: ev.agent, - kind: ev.approval_kind, - status: ev.status, - resolved_at: ev.resolved_at, - note: ev.note || null, - description: ev.description || null, - }); - if (approvalsState.history.length > APPROVAL_HISTORY_LIMIT) { - approvalsState.history.length = APPROVAL_HISTORY_LIMIT; - } - } - renderApprovals(); -} -export function renderApprovals() { - const root = $("approvals-section"); - // #approvals-section only lives on /dashboard.html (Y3R C4LL tab); - // no-op elsewhere — `approval_resolved` SSE - // events route through here on every page that loads the bundle. - if (!root) return; - root.replaceChildren(); - - const pending = approvalsState.pending; - const history = approvalsState.history; - const active = localStorage.getItem(APPROVAL_TAB_KEY) || "pending"; - const tabs = el("div", { class: "approval-tabs" }); - const pendingTab = el( - "button", - { - type: "button", - class: "approval-tab" + (active === "pending" ? " active" : ""), - }, - `pending · ${pending.length}`, - ); - const historyTab = el( - "button", - { - type: "button", - class: "approval-tab" + (active === "history" ? " active" : ""), - }, - `history · ${history.length}`, - ); - pendingTab.addEventListener("click", () => { - localStorage.setItem(APPROVAL_TAB_KEY, "pending"); - renderApprovals(); - }); - historyTab.addEventListener("click", () => { - localStorage.setItem(APPROVAL_TAB_KEY, "history"); - renderApprovals(); - }); - tabs.append(pendingTab, historyTab); - root.append(tabs); - - if (active === "history") { - renderApprovalHistory(root, history); - return; - } - - if (!pending.length) { - root.append(el("p", { class: "empty" }, "queue empty")); - return; - } - const ul = el("ul", { class: "approvals" }); - for (const a of pending) { - const isUpdateMeta = a.kind === "update_meta_inputs"; - const li = el("li", { class: "approval-card" }); - - // ── identity header ────────────────────────────────────────── - const head = el( - "div", - { class: "approval-head" }, - el("span", { class: "glyph" }, "↻"), - el("span", { class: "id" }, "#" + a.id), - el("span", { class: "agent" }, a.agent), - 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 - // obvious at a glance (see docs/web-ui/dashboard.md::Approval card). - if (a.requested_at != null) { - const requestedSec = epochSec(a.requested_at); - const ageSec = Math.max(0, Math.floor(Date.now() / 1000 - requestedSec)); - head.append( - el( - "span", - { - class: "approval-ts" + (ageSec >= 3600 ? " stale" : ""), - title: "requested " + new Date(a.requested_at).toLocaleString(), - "data-requested-at": String(requestedSec), - }, - "requested " + fmtAgo(a.requested_at), - ), - ); - } - li.append(head); - - // ── what-changed body ──────────────────────────────────────── - const body = el("div", { class: "approval-body" }); - if (a.description) { - body.append(el("div", { class: "approval-description" }, a.description)); - } - if (isUpdateMeta) { - let inputs; - try { - inputs = JSON.parse(a.commit_ref || "[]"); - } catch (_) { - inputs = []; - } - body.append( - el( - "span", - { class: "meta" }, - inputs.length - ? "bump flake inputs: " + inputs.join(", ") - : "bump all flake inputs", - ), - ); - } else { - body.append( - el( - "span", - { class: "meta" }, - isInit - ? "scaffold proposed config repo — submitting agent customises agent.nix before spawn" - : "new sub-agent — container will be created on approve", - ), - ); - } - li.append(body); - - // ── decision actions ───────────────────────────────────────── - // Deny prompts the operator for an optional reason; the submit - // handler stashes it into a hidden `note` input that rides along - // on the POST and is surfaced to the submitting agent via - // HelperEvent::ApprovalResolved { note }. - const denyForm = el("form", { - method: "POST", - action: "/api/deny/" + a.id, - class: "inline", - "data-async": "", - "data-no-refresh": "", - "data-prompt": "reason for denying (optional, sent to submitter):", - }); - denyForm.append( - el("button", { type: "submit", class: "btn btn-deny" }, "DENY"), - ); - li.append( - el( - "div", - { class: "approval-actions" }, - form( - "/api/approve/" + a.id, - "btn-approve", - "◆ APPR0VE", - null, - {}, - { noRefresh: true }, - ), - denyForm, - ), - ); - - ul.append(li); - } - root.append(ul); -} - -function renderApprovalHistory(root, history) { - if (!history.length) { - root.append(el("p", { class: "empty" }, "no resolved approvals yet")); - return; - } - const ul = el("ul", { class: "approvals approvals-history" }); - for (const a of history) { - const li = el("li"); - const row = el("div", { class: "row" }); - const glyph = - a.status === "approved" - ? "✓" - : a.status === "denied" - ? "✗" - : a.status === "cancelled" - ? "⊘" - : "⚠"; - row.append( - el("span", { class: "glyph glyph-" + a.status }, glyph), - " ", - el("span", { class: "id" }, "#" + a.id), - " ", - el("span", { class: "agent" }, a.agent), - " ", - el( - "span", - { class: "kind" }, - a.kind === "update_meta_inputs" ? "meta-update" : a.kind, - ), - " ", - ); - if (a.sha_short) row.append(el("code", {}, a.sha_short), " "); - row.append( - el("span", { class: "status status-" + a.status }, a.status), - " ", - el("span", { class: "msg-ts" }, fmtAgo(a.resolved_at)), - ); - li.append(row); - if (a.note) { - li.append(el("div", { class: "history-note" }, a.note)); - } - ul.append(li); - } - root.append(ul); -} diff --git a/frontend/packages/dashboard/src/common.css b/frontend/packages/dashboard/src/common.css index feb188c1..20840a9d 100644 --- a/frontend/packages/dashboard/src/common.css +++ b/frontend/packages/dashboard/src/common.css @@ -194,10 +194,6 @@ code { color: var(--green); border-color: var(--green); } -.btn-deny { - color: var(--red); - border-color: var(--red); -} .btn-destroy { color: var(--red); border-color: var(--red); diff --git a/frontend/packages/dashboard/src/common.js b/frontend/packages/dashboard/src/common.js index 956207b5..5b592d91 100644 --- a/frontend/packages/dashboard/src/common.js +++ b/frontend/packages/dashboard/src/common.js @@ -348,7 +348,7 @@ export function openBuildLogStream(id, pre, { onDone, onError } = {}) { // ─── side panel ───────────────────────────────────────────────────────── // Singleton drawer that swipes in from the right. Long content -// (file previews, approval diffs, journald logs, applied config) +// (file previews, journald logs, applied config) // opens here via `sidePanel.open(title, node)` instead of expanding // inline. Body is swapped on each open; closing just slides out so // the content stays visible through the transition. @@ -566,9 +566,7 @@ export function appendLinkified(parent, text, refs) { } // ─── browser notifications ────────────────────────────────────────────── -// Fires OS notifications on two operator-bound signals: -// - new approval landed in the queue -// - broker message sent `to: "operator"` +// Fires OS notifications on broker messages sent `to: "operator"`. // Permission grant is per-browser; a localStorage "muted" toggle lets // the operator silence without revoking. Secure-context only (HTTPS / // localhost) — on other origins the API is unavailable and we hide diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 6fd695a5..fa84305e 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -496,55 +496,6 @@ hive-agent-menu { } } -/* Pending approval: a card with three stacked sections — identity - header, what-changed body, decision actions. */ -.approvals { - list-style: none; - padding: 0; - margin: 0.4em 0 0; -} -.approval-card { - background: var(--bg-elev); - border: 1px solid var(--border); - border-left: 3px solid var(--purple); - border-radius: 4px; - padding: 0.6em 0.8em; - margin-bottom: 0.6em; -} -.approval-head { - display: flex; - align-items: baseline; - flex-wrap: wrap; - gap: 0.3em; -} -.approval-ts { - margin-left: auto; - color: var(--muted); - font-size: 0.85em; -} -.approval-ts.stale { - color: var(--amber); - text-shadow: 0 0 6px color-mix(in srgb, var(--amber) 50%, transparent); -} -.approval-body { - margin: 0.45em 0; - padding-left: 1.3em; -} -.approval-description { - font-size: 0.9em; - color: var(--fg); - white-space: pre-wrap; - margin-bottom: 0.35em; -} -.approval-actions { - display: flex; - gap: 0.5em; - padding-top: 0.45em; - border-top: 1px solid var(--border); -} -.approval-actions form.inline { - display: inline; -} /* Inline drill-in triggers (logs / config repo / view diff). */ .drill-ins { display: flex; @@ -556,91 +507,6 @@ hive-agent-menu { margin-top: 0; } -.approval-tabs { - display: flex; - gap: 0.4em; - margin: 0.6em 0 0.4em; -} -.approval-tab { - background: transparent; - border: 1px solid var(--border); - color: var(--muted); - font: inherit; - font-size: 0.85em; - letter-spacing: 0.08em; - padding: 0.25em 0.9em; - cursor: pointer; - transition: - color 0.15s ease, - border-color 0.15s ease, - background 0.15s ease; -} -.approval-tab:hover { - color: var(--fg); -} -.approval-tab.active { - color: var(--purple); - border-color: var(--purple); - background: color-mix(in srgb, var(--purple) 8%, transparent); - text-shadow: 0 0 4px currentColor; -} -/* History items: lighter card treatment so they're visually coherent with - the pending .approval-card items above them. Left accent colour reflects - the outcome via :has() on the per-status glyph span. */ -.approvals-history li { - background: color-mix(in srgb, var(--bg-elev) 40%, transparent); - border: 1px solid var(--border); - border-left: 3px solid var(--subtext0); - border-radius: 3px; - padding: 0.35em 0.8em; - margin-bottom: 0.4em; -} -.approvals-history li:has(.glyph-approved) { - border-left-color: var(--green); -} -.approvals-history li:has(.glyph-denied) { - border-left-color: var(--red); -} -.approvals-history li:has(.glyph-failed) { - border-left-color: var(--amber); -} -.approvals-history .status { - font-size: 0.85em; - padding: 0 0.5em; -} -.status-approved { - color: var(--green); -} -.status-denied { - color: var(--red); -} -.status-failed { - color: var(--amber); -} -.status-cancelled { - color: var(--muted); -} -.glyph-approved { - color: var(--green); -} -.glyph-denied { - color: var(--red); -} -.glyph-failed { - color: var(--amber); -} -.glyph-cancelled { - color: var(--muted); -} - -.history-note { - margin-left: 1.8em; - margin-top: 0.2em; - color: var(--muted); - font-size: 0.85em; - white-space: pre-wrap; - word-break: break-word; -} ul form.inline { display: inline-block; } diff --git a/frontend/packages/dashboard/src/dashboard.html b/frontend/packages/dashboard/src/dashboard.html index 062015fc..34d42ef9 100644 --- a/frontend/packages/dashboard/src/dashboard.html +++ b/frontend/packages/dashboard/src/dashboard.html @@ -134,8 +134,8 @@ -
loading…

-

◆ P3NDING APPR0VALS ◆

-
- ══════════════════════════════════════════════════════════════ -
-
-

loading…

-
- + inbox notifications above. -->

◆ PR3F3R3NC3S ◆

══════════════════════════════════════════════════════════════ @@ -182,10 +174,9 @@

◇ browser notifications

- desktop notifications for new approvals and broker messages - addressed to you. requires a secure context (https or localhost). - mute silences the notifications without revoking the OS-level - permission. + desktop notifications for broker messages addressed to you. requires + a secure context (https or localhost). mute silences the + notifications without revoking the OS-level permission.