From 40938d8b548e8b1fec0142214d81bfe4aa1f7fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Sat, 16 May 2026 03:49:49 +0200 Subject: [PATCH 1/3] dashboard: surface silent unwrap_or_default in api_state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit every snapshot source backing /api/state used .unwrap_or_default() — sqlite errors, broker errors, nixos-container list failures, operator_questions decode crashes all degraded to empty lists without a log line. the 'pending question doesn't render' bug we've been chasing was likely a row-decode panic in OperatorQuestions::pending() being swallowed this way. new log_default(what, result) replaces each call site: same default value on Err but emits target=api_state warn with the source name + dbg error first. five sources covered: nixos-container list, approvals.pending, approvals.recent_resolved, broker.recent_for(operator), questions.pending. next time the question goes missing the journal will say which source failed and how. todo updated — pending-question entry now points at the new log instead of three suspect paths. --- TODO.md | 22 +++++++--------- hive-c0re/src/dashboard.rs | 52 ++++++++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/TODO.md b/TODO.md index 3933ba53..1ee498f9 100644 --- a/TODO.md +++ b/TODO.md @@ -57,19 +57,15 @@ Pick anything from here when relevant. Cross-cutting design notes live in Repro: manager calls `ask_operator`, tool result is `question queued (id=N)` (so the row is in sqlite), but the M1ND H4S QU3STI0NS section keeps showing "no pending - questions". Last seen with id=5. Suspected paths: - - `OperatorQuestions::pending()` returns Err and the - `unwrap_or_default()` in `api_state` hides it. Surface the - error (warn-log) and check. - - serialization: a new field in `OpQuestion` (e.g. - `deadline_at: Option`) deserializes wrong against an - old row whose columns don't match the new SELECT order → - `row.get(N)?` panics for that row, the whole iterator - errors, `pending()` returns Err. Diagnose by curl - `/api/state | jq '.questions'` and compare with sqlite - counts. - - dashboard JS swallows a render error. Open browser console - and look for exceptions during `renderQuestions`. + questions". Last seen with id=5. Diagnostic step landed: + `api_state` now warn-logs (target=`api_state`) when any of + its source queries fail instead of silently + `unwrap_or_default`-ing — next repro should print the + underlying error in journald and tell us whether this is + sqlite (likely `OperatorQuestions::pending()` row-decode + panic on a migrated column) or dashboard-JS-side + (`renderQuestions` exception). Re-investigate with the new + log once the bug fires. ## UI / UX diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index d26e5cd9..c7bf632b 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -239,6 +239,25 @@ struct ApprovalView { diff_html: Option, } +/// Replace silent `.unwrap_or_default()` on the data sources behind +/// `/api/state` so that whichever query degrades surfaces in journald +/// instead of leaving the operator staring at an empty list. The +/// dashboard still degrades to a sensible default value; the warn +/// is just the diagnostic breadcrumb the old code swallowed. +fn log_default(what: &str, result: std::result::Result) -> T +where + T: Default, + E: std::fmt::Debug, +{ + match result { + Ok(v) => v, + Err(e) => { + tracing::warn!(target: "api_state", source = %what, error = ?e, "snapshot source failed; using default"); + T::default() + } + } +} + async fn api_state(headers: HeaderMap, State(state): State) -> axum::Json { let host = headers .get("host") @@ -246,35 +265,36 @@ async fn api_state(headers: HeaderMap, State(state): State) -> axum::J .unwrap_or("localhost"); let hostname = host.split(':').next().unwrap_or(host).to_owned(); - let raw_containers = lifecycle::list().await.unwrap_or_default(); + let raw_containers = log_default("nixos-container list", lifecycle::list().await); let current_rev = crate::auto_update::current_flake_rev(&state.coord.hyperhive_flake); let transient_snapshot = state.coord.transient_snapshot(); let pending_approvals = gc_orphans( &state.coord, - state.coord.approvals.pending().unwrap_or_default(), + log_default("approvals.pending", state.coord.approvals.pending()), ); let (containers, any_stale) = build_container_views(&raw_containers, current_rev.as_deref(), &transient_snapshot).await; let transients = build_transient_views(&raw_containers, &transient_snapshot); let approvals = build_approval_views(pending_approvals).await; - let approval_history = state - .coord - .approvals - .recent_resolved(30) - .unwrap_or_default() - .into_iter() - .map(history_view) - .collect(); + let approval_history = log_default( + "approvals.recent_resolved", + state.coord.approvals.recent_resolved(30), + ) + .into_iter() + .map(history_view) + .collect(); let tombstones = build_tombstone_views(&state.coord, &containers, &transient_snapshot); let port_conflicts = build_port_conflicts(&containers); - let operator_inbox = state - .coord - .broker - .recent_for(hive_sh4re::OPERATOR_RECIPIENT, 50) - .unwrap_or_default(); - let questions = state.coord.questions.pending().unwrap_or_default(); + let operator_inbox = log_default( + "broker.recent_for(operator)", + state + .coord + .broker + .recent_for(hive_sh4re::OPERATOR_RECIPIENT, 50), + ); + let questions = log_default("questions.pending", state.coord.questions.pending()); axum::Json(StateSnapshot { hostname, From fe8fb15f8f6fda5f1550e5c2d7aa09f3fab2d7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Sat, 16 May 2026 03:54:02 +0200 Subject: [PATCH 2/3] =?UTF-8?q?Revert=20"dashboard:=203-column=20layout=20?= =?UTF-8?q?=E2=80=94=20swarm=20/=200per4t0r=201n=20/=20m3ss4g3s"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 74ba8a63e113dec5a199f86cdeffbf26e44ba11d. --- hive-c0re/assets/app.js | 17 +++++ hive-c0re/assets/dashboard.css | 43 +------------ hive-c0re/assets/index.html | 112 ++++++++++++--------------------- 3 files changed, 60 insertions(+), 112 deletions(-) diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index 293f087d..7b075c03 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -606,6 +606,23 @@ const root = $('approvals-section'); root.innerHTML = ''; + // Spawn request form: submitting it queues a Spawn approval that + // lands in this same list, so the form belongs here rather than on + // the containers list (the agent doesn't exist yet). + const spawn = el('form', { + method: 'POST', action: '/request-spawn', + class: 'spawnform', 'data-async': '', + }); + spawn.append( + el('input', { + name: 'name', + placeholder: 'new agent name (≤9 chars)', + maxlength: '9', required: '', autocomplete: 'off', + }), + el('button', { type: 'submit', class: 'btn btn-spawn' }, '◆ R3QU3ST SP4WN'), + ); + root.append(spawn); + const history = s.approval_history || []; const active = localStorage.getItem(APPROVAL_TAB_KEY) || 'pending'; const tabs = el('div', { class: 'approval-tabs' }); diff --git a/hive-c0re/assets/dashboard.css b/hive-c0re/assets/dashboard.css index bbbe441e..27269eb1 100644 --- a/hive-c0re/assets/dashboard.css +++ b/hive-c0re/assets/dashboard.css @@ -18,50 +18,11 @@ body { background: var(--bg); color: var(--fg); font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Source Code Pro", monospace; - max-width: 110em; + max-width: 70em; margin: 1.5em auto; padding: 0 1.5em; line-height: 1.6; } -.columns { - display: grid; - gap: 1.4em; - margin-top: 1em; -} -@media (min-width: 1400px) { - .columns { - grid-template-columns: 1.1fr 1fr 1fr; - align-items: start; - } -} -.dash-col { - min-width: 0; /* lets grid children shrink instead of overflowing */ -} -.col-head { - position: sticky; - top: 0; - z-index: 5; - background: rgba(30, 30, 46, 0.92); - -webkit-backdrop-filter: blur(6px); - backdrop-filter: blur(6px); - margin: 0 0 0.6em; - padding: 0.3em 0.5em; - font-size: 1em; - border-bottom: 1px solid var(--purple-dim); -} -.sub-head { - color: var(--cyan); - font-size: 0.85em; - letter-spacing: 0.12em; - text-transform: uppercase; - margin: 1.6em 0 0.4em; - padding-bottom: 0.15em; - border-bottom: 1px dashed var(--border, var(--purple-dim)); - text-shadow: 0 0 6px rgba(137, 220, 235, 0.35); -} -.dash-col .sub-head:first-of-type { - margin-top: 0.4em; -} .banner { text-align: center; margin: 0 0 1em 0; @@ -91,7 +52,7 @@ h1, h2 { color: var(--purple); text-transform: uppercase; letter-spacing: 0.15em; - margin-top: 1em; + margin-top: 2em; text-shadow: 0 0 8px rgba(203, 166, 247, 0.4); } .divider { diff --git a/hive-c0re/assets/index.html b/hive-c0re/assets/index.html index d154610e..14399437 100644 --- a/hive-c0re/assets/index.html +++ b/hive-c0re/assets/index.html @@ -17,83 +17,53 @@ -
- -
-

◆ SW4RM ◆

+

◆ C0NTAINERS ◆

+
══════════════════════════════════════════════════════════════
+
+

loading…

+
-

containers

-
-

loading…

-
+

◆ K3PT ST4T3 ◆

+
══════════════════════════════════════════════════════════════
+
+

loading…

+
-

kept state

-
-

loading…

-
+

◆ M1ND H4S QU3STI0NS ◆

+
══════════════════════════════════════════════════════════════
+
+

loading…

+
-

spawn agent

-
- - -
+

◆ 0PER4T0R 1NB0X ◆

+
══════════════════════════════════════════════════════════════
+
+

loading…

+
-

update meta inputs

-

checkbox per input; submitting runs nix flake update in /meta/ and rebuilds affected agents (sequentially).

-
-

loading…

-
-
+

◆ P3NDING APPR0VALS ◆

+
══════════════════════════════════════════════════════════════
+
+

loading…

+
- -
-

◆ 0PER4T0R 1N ◆

+

◆ M3T4 1NPUTS ◆

+
══════════════════════════════════════════════════════════════
+

select inputs to nix flake update in /meta/. selected agents rebuild in sequence after the lock bump; manager learns each outcome via the usual rebuilt system event.

+
+

loading…

+
-

m1nd has questions

-
-

loading…

-
- -

pending approvals

-
-

loading…

-
-
- - -
-

◆ M3SS4G3S ◆

- -

operator inbox

-
-

loading…

-
- -

message flow

-

live tail — newest at the top; each row is one broker event. compose below: @name picks the recipient (sticky until you @ someone else); tab completes.

-
connecting…
-
- @—> - - -
-
+

◆ MESS4GE FL0W ◆

+
══════════════════════════════════════════════════════════════
+

live tail — newest at the top. tap on every send / recv through the broker. compose below: @name picks the recipient (sticky until you @ someone else); tab completes.

+
connecting…
+
+ @—> + +
From d1c69b134a84130374a85e11f8849f6b0da89a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Sat, 16 May 2026 03:54:53 +0200 Subject: [PATCH 3/3] dashboard: reorder sections into grouped sequence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit after reverting the 3-column attempt (74ba8a6), keep the single-column layout but put related sections adjacent: swarm: containers → kept-state → meta-inputs decisions: questions → approvals messages: operator-inbox → message-flow + compose this is a free improvement — the operator scrolls through one logical group at a time instead of bouncing between swarm / decisions / messages mid-page. follow-up improvements (collapsing rarely-active sections, multi-column at wide viewports done less aggressively) captured in TODO under 'Dashboard layout overhaul'. --- TODO.md | 23 +++++++++++++++++++++++ hive-c0re/assets/index.html | 18 +++++++++++------- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/TODO.md b/TODO.md index 1ee498f9..3cb73324 100644 --- a/TODO.md +++ b/TODO.md @@ -69,6 +69,29 @@ Pick anything from here when relevant. Cross-cutting design notes live in ## UI / UX +- **Dashboard layout overhaul.** A 3-column attempt (swarm + / 0per4t0r 1n / m3ss4g3s) landed + was reverted in 74ba8a6 + — looked worse in practice (sticky col-heads fighting the + banner, sub-heads too small, columns too narrow for the + container rows). Sections are now ordered semantically in + a single column (swarm bits first, then decisions, then + messages) which is a no-cost improvement. The bigger + restructure is still worth doing; next attempt should: + - keep current widths usable (don't crunch container + rows < ~36em — they have a lot inline) + - default the heavy-but-rare sections (kept-state, meta- + inputs, msg-flow history) into a collapsed `
` + so they don't dominate when empty + - drop the per-section banner divider lines in favour of + something quieter (a single border-top on the h2?) + - try a *masonry-ish* layout (CSS `grid-template-rows: + masonry` once browsers support it; or just two columns + where messages floats on the right at wide viewports + while the rest stacks left). avoid sticky headers — they + fought the page banner last time. + + + - **Web UI for config repos + meta deploy log.** Browse per-agent proposed / applied tags (`proposal/* / approved/* / building/* / deployed/* / diff --git a/hive-c0re/assets/index.html b/hive-c0re/assets/index.html index 14399437..6af8ef76 100644 --- a/hive-c0re/assets/index.html +++ b/hive-c0re/assets/index.html @@ -17,6 +17,8 @@
+

◆ C0NTAINERS ◆

══════════════════════════════════════════════════════════════
@@ -29,15 +31,17 @@

loading…

-

◆ M1ND H4S QU3STI0NS ◆

+

◆ M3T4 1NPUTS ◆

══════════════════════════════════════════════════════════════
-
+

select inputs to nix flake update in /meta/. selected agents rebuild in sequence after the lock bump; manager learns each outcome via the usual rebuilt system event.

+

loading…

-

◆ 0PER4T0R 1NB0X ◆

+ +

◆ M1ND H4S QU3STI0NS ◆

══════════════════════════════════════════════════════════════
-
+

loading…

@@ -47,10 +51,10 @@

loading…

-

◆ M3T4 1NPUTS ◆

+ +

◆ 0PER4T0R 1NB0X ◆

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

select inputs to nix flake update in /meta/. selected agents rebuild in sequence after the lock bump; manager learns each outcome via the usual rebuilt system event.

-
+

loading…