feat(sync): per-project breakdown, status enrichment progress bars, and summary polish
Add per-project detail rows beneath stage completion lines during multi-project syncs, showing itemized counts (issues/MRs, discussions, events, statuses, diffs) for each project. Previously, only aggregate totals were visible, making it hard to diagnose which project contributed what during a sync. Status enrichment gets proper progress bars replacing the old spinner-only display: StatusEnrichmentStarted now carries a total count so the CLI can render a determinate bar with rate and ETA. The enrichment SQL is tightened to use IS NOT comparisons for diff-only UPDATEs (skip rows where values haven't changed), and a follow-up touch_stmt ensures status_synced_at is updated even for unchanged rows so staleness detection works correctly. Other improvements: - New ProjectSummary struct aggregates per-project metrics during ingestion - SyncResult gains statuses_enriched + per-project summary vectors - "Already up to date" message when sync finds zero changes - Remove Arc<AtomicBool> tick_started pattern from docs/embed stages (enable_steady_tick is idempotent, the guard was unnecessary) - Progress bar styling: dim spinner, dark_gray track, per_sec + eta display - Tick intervals tightened from 100ms to 60ms for smoother animation - statuses_without_widget calculation uses fetch_result.statuses.len() instead of subtracting enriched (more accurate when some statuses lack work item widgets) - Status enrichment completion log downgraded from info to debug Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,21 @@ pub struct IngestResult {
|
||||
pub mr_diffs_failed: usize,
|
||||
pub status_enrichment_errors: usize,
|
||||
pub status_enrichment_projects: Vec<ProjectStatusEnrichment>,
|
||||
pub project_summaries: Vec<ProjectSummary>,
|
||||
}
|
||||
|
||||
/// Per-project summary for display in stage completion sub-rows.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ProjectSummary {
|
||||
pub path: String,
|
||||
pub items_upserted: usize,
|
||||
pub discussions_synced: usize,
|
||||
pub events_fetched: usize,
|
||||
pub events_failed: usize,
|
||||
pub statuses_enriched: usize,
|
||||
pub statuses_seen: usize,
|
||||
pub mr_diffs_fetched: usize,
|
||||
pub mr_diffs_failed: usize,
|
||||
}
|
||||
|
||||
/// Per-project status enrichment result, collected during ingestion.
|
||||
@@ -388,11 +403,11 @@ async fn run_ingest_inner(
|
||||
let s = multi.add(ProgressBar::new_spinner());
|
||||
s.set_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.template("{spinner:.blue} {msg}")
|
||||
.template("{spinner:.cyan} {msg}")
|
||||
.unwrap(),
|
||||
);
|
||||
s.set_message(format!("Fetching {type_label} from {path}..."));
|
||||
s.enable_steady_tick(std::time::Duration::from_millis(100));
|
||||
s.enable_steady_tick(std::time::Duration::from_millis(60));
|
||||
s
|
||||
};
|
||||
|
||||
@@ -403,12 +418,13 @@ async fn run_ingest_inner(
|
||||
b.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template(
|
||||
" {spinner:.blue} {prefix:.cyan} Syncing discussions [{bar:30.cyan/dim}] {pos}/{len}",
|
||||
" {spinner:.dim} {prefix:.cyan} Syncing discussions [{bar:30.cyan/dark_gray}] {pos}/{len} {per_sec:.dim} {eta:.dim}",
|
||||
)
|
||||
.unwrap()
|
||||
.progress_chars("=> "),
|
||||
.progress_chars(crate::cli::render::Icons::progress_chars()),
|
||||
);
|
||||
b.set_prefix(path.clone());
|
||||
b.enable_steady_tick(std::time::Duration::from_millis(60));
|
||||
b
|
||||
};
|
||||
|
||||
@@ -445,7 +461,7 @@ async fn run_ingest_inner(
|
||||
spinner_clone.finish_and_clear();
|
||||
let agg_total = agg_disc_total_clone.fetch_add(total, Ordering::Relaxed) + total;
|
||||
disc_bar_clone.set_length(total as u64);
|
||||
disc_bar_clone.enable_steady_tick(std::time::Duration::from_millis(100));
|
||||
disc_bar_clone.enable_steady_tick(std::time::Duration::from_millis(60));
|
||||
stage_bar_clone.set_message(format!(
|
||||
"Syncing discussions... (0/{agg_total})"
|
||||
));
|
||||
@@ -465,7 +481,7 @@ async fn run_ingest_inner(
|
||||
spinner_clone.finish_and_clear();
|
||||
let agg_total = agg_disc_total_clone.fetch_add(total, Ordering::Relaxed) + total;
|
||||
disc_bar_clone.set_length(total as u64);
|
||||
disc_bar_clone.enable_steady_tick(std::time::Duration::from_millis(100));
|
||||
disc_bar_clone.enable_steady_tick(std::time::Duration::from_millis(60));
|
||||
stage_bar_clone.set_message(format!(
|
||||
"Syncing discussions... (0/{agg_total})"
|
||||
));
|
||||
@@ -486,11 +502,11 @@ async fn run_ingest_inner(
|
||||
disc_bar_clone.set_length(total as u64);
|
||||
disc_bar_clone.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template(" {spinner:.blue} {prefix:.cyan} Fetching resource events [{bar:30.cyan/dim}] {pos}/{len}")
|
||||
.template(" {spinner:.dim} {prefix:.cyan} Fetching resource events [{bar:30.cyan/dark_gray}] {pos}/{len} {per_sec:.dim} {eta:.dim}")
|
||||
.unwrap()
|
||||
.progress_chars("=> "),
|
||||
.progress_chars(crate::cli::render::Icons::progress_chars()),
|
||||
);
|
||||
disc_bar_clone.enable_steady_tick(std::time::Duration::from_millis(100));
|
||||
disc_bar_clone.enable_steady_tick(std::time::Duration::from_millis(60));
|
||||
agg_events_total_clone.fetch_add(total, Ordering::Relaxed);
|
||||
stage_bar_clone.set_message(
|
||||
"Fetching resource events...".to_string()
|
||||
@@ -510,7 +526,7 @@ async fn run_ingest_inner(
|
||||
ProgressEvent::ClosesIssuesFetchStarted { total } => {
|
||||
disc_bar_clone.reset();
|
||||
disc_bar_clone.set_length(total as u64);
|
||||
disc_bar_clone.enable_steady_tick(std::time::Duration::from_millis(100));
|
||||
disc_bar_clone.enable_steady_tick(std::time::Duration::from_millis(60));
|
||||
stage_bar_clone.set_message(
|
||||
"Fetching closes-issues references...".to_string()
|
||||
);
|
||||
@@ -524,7 +540,7 @@ async fn run_ingest_inner(
|
||||
ProgressEvent::MrDiffsFetchStarted { total } => {
|
||||
disc_bar_clone.reset();
|
||||
disc_bar_clone.set_length(total as u64);
|
||||
disc_bar_clone.enable_steady_tick(std::time::Duration::from_millis(100));
|
||||
disc_bar_clone.enable_steady_tick(std::time::Duration::from_millis(60));
|
||||
stage_bar_clone.set_message(
|
||||
"Fetching MR file changes...".to_string()
|
||||
);
|
||||
@@ -535,35 +551,37 @@ async fn run_ingest_inner(
|
||||
ProgressEvent::MrDiffsFetchComplete { .. } => {
|
||||
disc_bar_clone.finish_and_clear();
|
||||
}
|
||||
ProgressEvent::StatusEnrichmentStarted => {
|
||||
spinner_clone.set_message(format!(
|
||||
"{path_for_cb}: Enriching work item statuses..."
|
||||
));
|
||||
ProgressEvent::StatusEnrichmentStarted { total } => {
|
||||
spinner_clone.finish_and_clear();
|
||||
disc_bar_clone.reset();
|
||||
disc_bar_clone.set_length(total as u64);
|
||||
disc_bar_clone.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template(" {spinner:.dim} {prefix:.cyan} Statuses [{bar:30.cyan/dark_gray}] {pos}/{len} {per_sec:.dim} {eta:.dim}")
|
||||
.unwrap()
|
||||
.progress_chars(crate::cli::render::Icons::progress_chars()),
|
||||
);
|
||||
disc_bar_clone.set_prefix(path_for_cb.clone());
|
||||
disc_bar_clone.enable_steady_tick(std::time::Duration::from_millis(60));
|
||||
stage_bar_clone.set_message(
|
||||
"Enriching work item statuses...".to_string()
|
||||
);
|
||||
}
|
||||
ProgressEvent::StatusEnrichmentPageFetched { items_so_far } => {
|
||||
spinner_clone.set_message(format!(
|
||||
"{path_for_cb}: Fetching statuses... ({items_so_far} work items)"
|
||||
));
|
||||
disc_bar_clone.set_position(items_so_far as u64);
|
||||
stage_bar_clone.set_message(format!(
|
||||
"Enriching work item statuses... ({items_so_far} fetched)"
|
||||
));
|
||||
}
|
||||
ProgressEvent::StatusEnrichmentWriting { total } => {
|
||||
spinner_clone.set_message(format!(
|
||||
"{path_for_cb}: Writing {total} statuses..."
|
||||
));
|
||||
disc_bar_clone.set_message(format!("Writing {total} statuses..."));
|
||||
stage_bar_clone.set_message(format!(
|
||||
"Writing {total} work item statuses..."
|
||||
));
|
||||
}
|
||||
ProgressEvent::StatusEnrichmentComplete { enriched, cleared } => {
|
||||
disc_bar_clone.finish_and_clear();
|
||||
if enriched > 0 || cleared > 0 {
|
||||
spinner_clone.set_message(format!(
|
||||
"{path_for_cb}: {enriched} statuses enriched, {cleared} cleared"
|
||||
));
|
||||
stage_bar_clone.set_message(format!(
|
||||
"Status enrichment: {enriched} enriched, {cleared} cleared"
|
||||
));
|
||||
@@ -656,6 +674,17 @@ async fn run_ingest_inner(
|
||||
first_partial_error: result.first_partial_error.clone(),
|
||||
error: result.status_enrichment_error.clone(),
|
||||
});
|
||||
total.project_summaries.push(ProjectSummary {
|
||||
path: path.clone(),
|
||||
items_upserted: result.issues_upserted,
|
||||
discussions_synced: result.discussions_fetched,
|
||||
events_fetched: result.resource_events_fetched,
|
||||
events_failed: result.resource_events_failed,
|
||||
statuses_enriched: result.statuses_enriched,
|
||||
statuses_seen: result.statuses_seen,
|
||||
mr_diffs_fetched: 0,
|
||||
mr_diffs_failed: 0,
|
||||
});
|
||||
}
|
||||
Ok(ProjectIngestOutcome::Mrs {
|
||||
ref path,
|
||||
@@ -679,6 +708,17 @@ async fn run_ingest_inner(
|
||||
total.resource_events_failed += result.resource_events_failed;
|
||||
total.mr_diffs_fetched += result.mr_diffs_fetched;
|
||||
total.mr_diffs_failed += result.mr_diffs_failed;
|
||||
total.project_summaries.push(ProjectSummary {
|
||||
path: path.clone(),
|
||||
items_upserted: result.mrs_upserted,
|
||||
discussions_synced: result.discussions_fetched,
|
||||
events_fetched: result.resource_events_fetched,
|
||||
events_failed: result.resource_events_failed,
|
||||
statuses_enriched: 0,
|
||||
statuses_seen: 0,
|
||||
mr_diffs_fetched: result.mr_diffs_fetched,
|
||||
mr_diffs_failed: result.mr_diffs_failed,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user