//! Source and busser listing commands use crate::commands::error::ApiError; use crate::state::AppState; use bb_feed::FeedGenerator; use bb_interface::StructuredError; use serde::Serialize; use std::sync::Arc; use tauri::State; use tracing::instrument; /// Source/busser summary with item counts for the sidebar. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct SourceResponse { pub id: String, pub name: String, pub total_count: i64, pub unread_count: i64, pub tags: Vec, pub health: String, pub last_error: Option, pub circuit_broken: bool, pub error_category: Option, pub retry_after_secs: Option, } /// List all registered sources with their total and unread item counts. #[tauri::command] #[instrument(skip_all)] pub async fn list_sources( state: State<'_, Arc>, ) -> Result, ApiError> { let db = state.orchestrator.database().clone(); let generator = FeedGenerator::new(db); let sources = generator.get_sources().await?; Ok(sources .into_iter() .map(|s| { // Parse structured error from last_error JSON (backward-compat with plain strings) let parsed = s.last_error.as_deref().map(StructuredError::from_last_error); let error_category = parsed.as_ref().map(|e| e.category.to_string()); let retry_after_secs = parsed.as_ref().and_then(|e| e.retry_after_secs); let health = if s.circuit_broken { match error_category.as_deref() { Some("auth") => "auth_error", Some("config") => "config_error", _ => "circuit_broken", } } else { match (s.consecutive_failures, error_category.as_deref()) { (0, _) => "green", (_, Some("rate_limited")) => "rate_limited", (1..=2, _) => "yellow", _ => "red", } } .to_string(); // Use the display message from structured error, or raw string let display_error = parsed.as_ref().map(|e| e.message.clone()).or(s.last_error); SourceResponse { id: s.id, name: s.name, total_count: s.total_count, unread_count: s.unread_count, tags: s.tags, health, last_error: display_error, circuit_broken: s.circuit_broken, error_category, retry_after_secs, } }) .collect()) }