Skip to main content

max / balanced_breakfast

2.6 KB · 80 lines History Blame Raw
1 //! Source and busser listing commands
2 use crate::commands::error::ApiError;
3 use crate::state::AppState;
4 use bb_feed::FeedGenerator;
5 use bb_interface::StructuredError;
6 use serde::Serialize;
7 use std::sync::Arc;
8 use tauri::State;
9 use tracing::instrument;
10
11 /// Source/busser summary with item counts for the sidebar.
12 #[derive(Debug, Clone, Serialize)]
13 #[serde(rename_all = "camelCase")]
14 pub struct SourceResponse {
15 pub id: String,
16 pub name: String,
17 pub total_count: i64,
18 pub unread_count: i64,
19 pub tags: Vec<String>,
20 pub health: String,
21 pub last_error: Option<String>,
22 pub circuit_broken: bool,
23 pub error_category: Option<String>,
24 pub retry_after_secs: Option<u64>,
25 }
26
27 /// List all registered sources with their total and unread item counts.
28 #[tauri::command]
29 #[instrument(skip_all)]
30 pub async fn list_sources(
31 state: State<'_, Arc<AppState>>,
32 ) -> Result<Vec<SourceResponse>, ApiError> {
33 let db = state.orchestrator.database().clone();
34 let generator = FeedGenerator::new(db);
35
36 let sources = generator.get_sources().await?;
37
38 Ok(sources
39 .into_iter()
40 .map(|s| {
41 // Parse structured error from last_error JSON (backward-compat with plain strings)
42 let parsed = s.last_error.as_deref().map(StructuredError::from_last_error);
43 let error_category = parsed.as_ref().map(|e| e.category.to_string());
44 let retry_after_secs = parsed.as_ref().and_then(|e| e.retry_after_secs);
45
46 let health = if s.circuit_broken {
47 match error_category.as_deref() {
48 Some("auth") => "auth_error",
49 Some("config") => "config_error",
50 _ => "circuit_broken",
51 }
52 } else {
53 match (s.consecutive_failures, error_category.as_deref()) {
54 (0, _) => "green",
55 (_, Some("rate_limited")) => "rate_limited",
56 (1..=2, _) => "yellow",
57 _ => "red",
58 }
59 }
60 .to_string();
61
62 // Use the display message from structured error, or raw string
63 let display_error = parsed.as_ref().map(|e| e.message.clone()).or(s.last_error);
64
65 SourceResponse {
66 id: s.id,
67 name: s.name,
68 total_count: s.total_count,
69 unread_count: s.unread_count,
70 tags: s.tags,
71 health,
72 last_error: display_error,
73 circuit_broken: s.circuit_broken,
74 error_category,
75 retry_after_secs,
76 }
77 })
78 .collect())
79 }
80