diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index 664eb69..96d9414 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -273,7 +273,9 @@ for (const a of approvals) { if (seenApprovals.has(a.id)) continue; seenApprovals.add(a.id); - const verb = a.kind === 'spawn' ? 'spawn approval' : 'config commit'; + const verb = a.kind === 'spawn' ? 'spawn approval' + : a.kind === 'init_config' ? 'config-init approval' + : 'config commit'; NOTIF.show('◆ approval #' + a.id, `${verb} for ${a.agent}`, 'hyperhive:approval:' + a.id); } @@ -1227,6 +1229,7 @@ const ul = el('ul', { class: 'approvals' }); for (const a of pending) { const isApply = a.kind === 'apply_commit'; + const isInit = a.kind === 'init_config'; const li = el('li', { class: 'approval-card' }); // ── identity header ────────────────────────────────────────── @@ -1235,7 +1238,7 @@ el('span', { class: 'id' }, '#' + a.id), el('span', { class: 'agent' }, a.agent), el('span', { class: 'kind' + (isApply ? '' : ' kind-spawn') }, - isApply ? 'apply' : 'spawn'), + isApply ? 'apply' : isInit ? 'init' : 'spawn'), ); if (isApply && a.sha_short) head.append(el('code', {}, a.sha_short)); li.append(head); @@ -1261,7 +1264,9 @@ body.append(drill); } else { body.append(el('span', { class: 'meta' }, - 'new sub-agent — container will be created on approve')); + isInit + ? 'scaffold proposed config repo — manager customises agent.nix before spawn' + : 'new sub-agent — container will be created on approve')); } li.append(body); diff --git a/hive-c0re/src/approvals.rs b/hive-c0re/src/approvals.rs index 9b696c5..815ecf7 100644 --- a/hive-c0re/src/approvals.rs +++ b/hive-c0re/src/approvals.rs @@ -137,8 +137,7 @@ impl Approvals { LIMIT ?1", )?; let rows = stmt.query_map([limit], row_to_approval)?; - rows.collect::>>() - .map_err(Into::into) + Ok(collect_lenient(rows)) } pub fn pending(&self) -> Result> { @@ -150,8 +149,7 @@ impl Approvals { ORDER BY id ASC", )?; let rows = stmt.query_map([], row_to_approval)?; - rows.collect::>>() - .map_err(Into::into) + Ok(collect_lenient(rows)) } pub fn get(&self, id: i64) -> Result> { @@ -261,12 +259,32 @@ impl Approvals { } } +/// Collect approval rows, dropping (and logging) any that fail to +/// deserialize. A single malformed / unknown-kind row must never blank +/// the whole list: `collect::>()` is all-or-nothing, so one +/// bad row used to make `pending()` / `recent_resolved()` error out +/// wholesale — the dashboard then rendered an empty approvals queue +/// (issue #160, an unhandled `init_config` kind poisoning every read). +fn collect_lenient( + rows: impl Iterator>, +) -> Vec { + rows.filter_map(|r| match r { + Ok(a) => Some(a), + Err(e) => { + tracing::warn!(error = ?e, "skipping unparseable approval row"); + None + } + }) + .collect() +} + fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result { // Column order: id, agent, kind, commit_ref, requested_at, status, resolved_at, note, fetched_sha, description. let kind: String = row.get(2)?; let kind = match kind.as_str() { "apply_commit" => ApprovalKind::ApplyCommit, "spawn" => ApprovalKind::Spawn, + "init_config" => ApprovalKind::InitConfig, other => { return Err(rusqlite::Error::FromSqlConversionFailure( 2,