From 2a5c4d441fb7d503844c8b5d87b5d25c7f73483e Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 19 Jul 2026 18:41:40 +0200 Subject: [PATCH] fix(#2594): fold unparseable-approval-row warnings into one aggregated line --- hive-c0re/src/stores/approvals.rs | 40 +++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/hive-c0re/src/stores/approvals.rs b/hive-c0re/src/stores/approvals.rs index 3c8afe33..3405d75c 100644 --- a/hive-c0re/src/stores/approvals.rs +++ b/hive-c0re/src/stores/approvals.rs @@ -335,20 +335,36 @@ impl ApprovalLookup { } } -/// 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. +/// Collect approval rows, dropping 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. +/// +/// Drops are aggregated by error message and logged **once per call** rather +/// than one line per row: a batch of legacy unknown-kind rows (e.g. the +/// retired `apply_commit` approvals from the removed non-PR config flow) sit +/// `pending` forever and would otherwise flood the journal with an identical +/// warning on every dashboard render. Aggregating keeps the signal (how many, +/// which error) without the flood. 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 + let mut out = Vec::new(); + let mut dropped: std::collections::HashMap = std::collections::HashMap::new(); + for r in rows { + match r { + Ok(a) => out.push(a), + Err(e) => *dropped.entry(e.to_string()).or_default() += 1, } - }) - .collect() + } + if !dropped.is_empty() { + let total: usize = dropped.values().sum(); + tracing::warn!( + dropped = total, + by_error = ?dropped, + "skipped unparseable approval rows" + ); + } + out } fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result {