Needed for the new fmt-check CI step to pass; the repo had never had formatting enforced before. No logic changes.
84 lines
2.8 KiB
Rust
84 lines
2.8 KiB
Rust
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<Db>) {
|
|
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<Db, dioxus::prelude::ServerFnError> {
|
|
// 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<Vec<ArticleView>> {
|
|
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(())
|
|
}
|