diff --git a/docs/web-ui.md b/docs/web-ui.md index b937c50..9b32ad4 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -208,8 +208,11 @@ agent is stale. Banner pulses on each broker SSE event Each pending approval renders as a card (`assets/app.js:: renderApprovals`) with three stacked sections: -- **identity header** — glyph, `#id`, agent, kind chip, and (for - `apply_commit`) the short proposal sha as ``. +- **identity header** — glyph, `#id`, agent, kind chip, (for + `apply_commit`) the short proposal sha as ``, and a + right-aligned `requested ago` relative time from + `ApprovalView.requested_at` — amber once the request has been + pending ≥ 1h so a stale approval stands out (issue #272). - **what-changed body** — the manager's description, then drill-in triggers: `↳ view diff` opens the diff in the side panel; `↳ commit on forge ↗` deep-links the proposal commit diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index 27b7834..23e8fb8 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -1193,6 +1193,12 @@ sha_short: ev.sha_short || null, diff: ev.diff || null, 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. (#272) + 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); @@ -1364,6 +1370,16 @@ isApply ? 'apply' : isInit ? 'init' : 'spawn'), ); if (isApply && a.sha_short) head.append(el('code', {}, a.sha_short)); + // 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. (issue #272) + if (a.requested_at != null) { + const ageSec = Math.max(0, Math.floor(Date.now() / 1000 - a.requested_at)); + head.append(el('span', { + class: 'approval-ts' + (ageSec >= 3600 ? ' stale' : ''), + title: 'requested ' + new Date(a.requested_at * 1000).toLocaleString(), + }, 'requested ' + fmtAgo(a.requested_at))); + } li.append(head); // ── what-changed body ──────────────────────────────────────── diff --git a/hive-c0re/assets/dashboard.css b/hive-c0re/assets/dashboard.css index 5b3e8a8..2cde96e 100644 --- a/hive-c0re/assets/dashboard.css +++ b/hive-c0re/assets/dashboard.css @@ -299,6 +299,18 @@ code { flex-wrap: wrap; gap: 0.3em; } +/* When the approval was requested — right-aligned in the head row; + goes amber once it has been pending ≥ 1h so a stale request stands + out at a glance (issue #272). */ +.approval-ts { + margin-left: auto; + color: var(--muted); + font-size: 0.85em; +} +.approval-ts.stale { + color: var(--amber); + text-shadow: 0 0 6px rgba(250, 179, 135, 0.5); +} .approval-body { margin: 0.45em 0; padding-left: 1.3em; diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index bbce25d..b0384eb 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -301,6 +301,9 @@ struct ApprovalView { /// Manager-supplied description shown on the approval card. #[serde(skip_serializing_if = "Option::is_none")] description: Option, + /// Unix seconds the approval was queued. Rendered as a relative + /// time on the card so the operator can spot a stale request. (#272) + requested_at: i64, } /// Replace silent `.unwrap_or_default()` on the data sources behind @@ -606,6 +609,7 @@ async fn build_approval_views(approvals: Vec) -> Vec { sha_short: Some(sha), diff: Some(diff), description: a.description, + requested_at: a.requested_at, } } hive_sh4re::ApprovalKind::Spawn => ApprovalView { @@ -615,6 +619,7 @@ async fn build_approval_views(approvals: Vec) -> Vec { sha_short: None, diff: None, description: a.description, + requested_at: a.requested_at, }, hive_sh4re::ApprovalKind::InitConfig => ApprovalView { id: a.id, @@ -623,6 +628,7 @@ async fn build_approval_views(approvals: Vec) -> Vec { sha_short: None, diff: None, description: a.description, + requested_at: a.requested_at, }, hive_sh4re::ApprovalKind::UpdateMetaInputs => ApprovalView { id: a.id, @@ -631,6 +637,7 @@ async fn build_approval_views(approvals: Vec) -> Vec { sha_short: None, diff: None, description: a.description, + requested_at: a.requested_at, }, }); }