pub mod aggregates;
pub mod store;
pub mod word_count;
pub use aggregates::{LiveTotals, ProgressSnapshot};
pub use store::ProgressStore;
pub use word_count::count_words;
use std::path::Path;
use std::sync::Arc;
use anyhow::Result;
use parking_lot::Mutex;
use uuid::Uuid;
static ACTIVE: std::sync::OnceLock<Arc<Mutex<Option<ProgressStore>>>> =
std::sync::OnceLock::new();
fn slot() -> Arc<Mutex<Option<ProgressStore>>> {
ACTIVE
.get_or_init(|| Arc::new(Mutex::new(None)))
.clone()
}
pub fn install(project_root: &Path) -> Result<()> {
let path = project_root.join("progress.db");
let store = ProgressStore::open(&path)?;
let s = slot();
*s.lock() = Some(store);
Ok(())
}
pub fn uninstall() {
let s = slot();
*s.lock() = None;
}
fn with_store<F, T>(f: F) -> Option<Result<T>>
where
F: FnOnce(&ProgressStore) -> Result<T>,
{
let s = slot();
let guard = s.lock();
let store = guard.as_ref()?;
Some(f(store))
}
pub fn record_save(node_id: Uuid, book_id: Option<Uuid>, prev_words: i64, new_words: i64) {
let delta = new_words - prev_words;
if let Some(Err(e)) = with_store(|s| s.record_event("save", node_id, book_id, delta, new_words, None)) {
tracing::warn!(target: "inkhaven::progress", "record_save: {e:#}");
}
}
pub fn record_status_change(
node_id: Uuid,
book_id: Option<Uuid>,
from: &str,
to: &str,
total_words: i64,
) {
let extra = serde_json::json!({ "from": from, "to": to }).to_string();
if let Some(Err(e)) = with_store(|s| {
s.record_event(
"status_change",
node_id,
book_id,
0,
total_words,
Some(&extra),
)
}) {
tracing::warn!(target: "inkhaven::progress", "record_status_change: {e:#}");
}
}
pub fn capture_today_baselines(per_book: &[(Uuid, i64)], project_total: i64) {
if let Some(Err(e)) = with_store(|s| {
s.capture_baselines_today(per_book, project_total)
}) {
tracing::warn!(target: "inkhaven::progress", "capture_today_baselines: {e:#}");
}
}
pub fn snapshot(
goals: &crate::config::GoalsConfig,
live: &LiveTotals,
) -> ProgressSnapshot {
let s = slot();
let guard = s.lock();
match guard.as_ref() {
Some(store) => aggregates::build_snapshot(store, goals, live)
.unwrap_or_else(|e| {
tracing::warn!(target: "inkhaven::progress", "snapshot: {e:#}");
ProgressSnapshot::empty()
}),
None => ProgressSnapshot::empty(),
}
}