feat(status): add per-entity sync counts from migration 027

Enhances sync status reporting to include granular per-entity counts
that were added in database migration 027. This provides better
visibility into what each sync run actually processed.

New fields in SyncRunInfo and robot mode JSON:
- issues_fetched / issues_ingested: issue sync counts
- mrs_fetched / mrs_ingested: merge request sync counts
- skipped_stale: entities skipped due to staleness
- docs_regenerated / docs_embedded: document pipeline counts
- warnings_count: non-fatal issues during sync

Robot mode optimization:
- Uses skip_serializing_if = "is_zero" to omit zero-value fields
- Reduces JSON payload size for typical sync runs
- Maintains backwards compatibility (fields are additive)

SQL query now reads all 8 new columns from sync_runs table,
with defensive unwrap_or(0) for NULL handling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-25 10:02:37 -05:00
parent ed987c8f71
commit 87bdbda468

View File

@@ -12,6 +12,10 @@ use crate::core::time::{format_full_datetime, ms_to_iso};
const RECENT_RUNS_LIMIT: usize = 10; const RECENT_RUNS_LIMIT: usize = 10;
fn is_zero(value: &i64) -> bool {
*value == 0
}
#[derive(Debug)] #[derive(Debug)]
pub struct SyncRunInfo { pub struct SyncRunInfo {
pub id: i64, pub id: i64,
@@ -24,6 +28,15 @@ pub struct SyncRunInfo {
pub total_items_processed: i64, pub total_items_processed: i64,
pub total_errors: i64, pub total_errors: i64,
pub stages: Option<Vec<StageTiming>>, pub stages: Option<Vec<StageTiming>>,
// Per-entity counts (from migration 027)
pub issues_fetched: i64,
pub issues_ingested: i64,
pub mrs_fetched: i64,
pub mrs_ingested: i64,
pub skipped_stale: i64,
pub docs_regenerated: i64,
pub docs_embedded: i64,
pub warnings_count: i64,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -68,7 +81,9 @@ pub fn run_sync_status(config: &Config) -> Result<SyncStatusResult> {
fn get_recent_sync_runs(conn: &Connection, limit: usize) -> Result<Vec<SyncRunInfo>> { fn get_recent_sync_runs(conn: &Connection, limit: usize) -> Result<Vec<SyncRunInfo>> {
let mut stmt = conn.prepare( let mut stmt = conn.prepare(
"SELECT id, started_at, finished_at, status, command, error, "SELECT id, started_at, finished_at, status, command, error,
run_id, total_items_processed, total_errors, metrics_json run_id, total_items_processed, total_errors, metrics_json,
issues_fetched, issues_ingested, mrs_fetched, mrs_ingested,
skipped_stale, docs_regenerated, docs_embedded, warnings_count
FROM sync_runs FROM sync_runs
ORDER BY started_at DESC ORDER BY started_at DESC
LIMIT ?1", LIMIT ?1",
@@ -91,6 +106,14 @@ fn get_recent_sync_runs(conn: &Connection, limit: usize) -> Result<Vec<SyncRunIn
total_items_processed: row.get::<_, Option<i64>>(7)?.unwrap_or(0), total_items_processed: row.get::<_, Option<i64>>(7)?.unwrap_or(0),
total_errors: row.get::<_, Option<i64>>(8)?.unwrap_or(0), total_errors: row.get::<_, Option<i64>>(8)?.unwrap_or(0),
stages, stages,
issues_fetched: row.get::<_, Option<i64>>(10)?.unwrap_or(0),
issues_ingested: row.get::<_, Option<i64>>(11)?.unwrap_or(0),
mrs_fetched: row.get::<_, Option<i64>>(12)?.unwrap_or(0),
mrs_ingested: row.get::<_, Option<i64>>(13)?.unwrap_or(0),
skipped_stale: row.get::<_, Option<i64>>(14)?.unwrap_or(0),
docs_regenerated: row.get::<_, Option<i64>>(15)?.unwrap_or(0),
docs_embedded: row.get::<_, Option<i64>>(16)?.unwrap_or(0),
warnings_count: row.get::<_, Option<i64>>(17)?.unwrap_or(0),
}) })
})? })?
.collect(); .collect();
@@ -198,6 +221,23 @@ struct SyncRunJsonInfo {
error: Option<String>, error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
stages: Option<Vec<StageTiming>>, stages: Option<Vec<StageTiming>>,
// Per-entity counts
#[serde(skip_serializing_if = "is_zero")]
issues_fetched: i64,
#[serde(skip_serializing_if = "is_zero")]
issues_ingested: i64,
#[serde(skip_serializing_if = "is_zero")]
mrs_fetched: i64,
#[serde(skip_serializing_if = "is_zero")]
mrs_ingested: i64,
#[serde(skip_serializing_if = "is_zero")]
skipped_stale: i64,
#[serde(skip_serializing_if = "is_zero")]
docs_regenerated: i64,
#[serde(skip_serializing_if = "is_zero")]
docs_embedded: i64,
#[serde(skip_serializing_if = "is_zero")]
warnings_count: i64,
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -237,6 +277,14 @@ pub fn print_sync_status_json(result: &SyncStatusResult, elapsed_ms: u64) {
total_errors: run.total_errors, total_errors: run.total_errors,
error: run.error.clone(), error: run.error.clone(),
stages: run.stages.clone(), stages: run.stages.clone(),
issues_fetched: run.issues_fetched,
issues_ingested: run.issues_ingested,
mrs_fetched: run.mrs_fetched,
mrs_ingested: run.mrs_ingested,
skipped_stale: run.skipped_stale,
docs_regenerated: run.docs_regenerated,
docs_embedded: run.docs_embedded,
warnings_count: run.warnings_count,
} }
}) })
.collect(); .collect();