use crate::app::ArticleView; use anyhow::Result; use feedsignal_core::{ReadingEvent, ReadingOutcome}; use feedsignal_db::Db; use feedsignal_llm::Llm; use std::sync::Arc; use tokio::sync::OnceCell; use uuid::Uuid; pub mod pipeline; pub fn init_tracing() { tracing_subscriber::fmt::init(); } static BACKGROUND_JOBS: OnceCell<()> = OnceCell::const_new(); /// Lazily starts the feed-polling/scoring scheduler on first use. Deferred /// rather than started at process boot because `dioxus_server::launch_cfg` /// owns the tokio runtime construction — this runs the first time a server /// function executes, which is guaranteed to already be inside that runtime. async fn ensure_background_jobs_started(db: Arc) { BACKGROUND_JOBS .get_or_init(|| async { // TODO: move base_url/model names to config/env once there's a // settings story; hardcoded to models already pulled locally. let llm = Arc::new( Llm::new( "http://localhost:11434", "nomic-embed-text", 768, "gemma4-e4b", ) .expect("failed to construct ollama client"), ); if let Err(err) = pipeline::start_scheduler(db, llm).await { tracing::error!(?err, "failed to start background job scheduler"); } }) .await; } pub async fn db() -> Result { // TODO: hold this in a `OnceCell`/app-wide state instead of reconnecting // per request once the server-state story is wired up. let db = Db::connect("feedsignal.db") .await .map_err(|e| dioxus::prelude::ServerFnError::new(e.to_string()))?; ensure_background_jobs_started(Arc::new(db.clone())).await; Ok(db) } pub async fn list_ranked_articles_impl(db: Db) -> Result> { Ok(db .list_ranked_articles(100) .await? .into_iter() .map( |(id, title, url, summary, topics, final_score)| ArticleView { id, title, url, summary, topics, final_score, }, ) .collect()) } pub async fn mark_dismissed_impl(db: Db, article_id: String) -> Result<()> { let event = ReadingEvent { id: Uuid::new_v4(), article_id: Uuid::parse_str(&article_id)?, occurred_at: chrono::Utc::now(), outcome: ReadingOutcome::Dismissed, }; db.record_event(&event).await?; // Affinity re-weighting from this event happens in the batched pipeline // job (see `pipeline::apply_pending_feedback`) rather than inline here, // so a burst of dismissals doesn't serialize on read-modify-write of // the single affinities row. Ok(()) }