refactor(server): plumb Arc<Settings> through workers and routes

Config sheds its 14 Bucket-B fields (retention, whisper, ollama, llm) and
gains a sibling Arc<Settings> in AppState, loaded from settings.toml at
startup via Settings::load_or_default. Workers (transcribe, analyze,
recovery, auto_trigger) and routes (health, bulk, case_actions,
user_web) take settings as a separate parameter or State<> extractor;
Config::llm_configured moves to Settings::llm_configured.

TestConfig::build_pair() returns (Arc<Config>, Arc<Settings>); the new
create_router_with_settings / create_router_and_session_store_with_settings
helpers wire both into integration tests that exercise LLM/Whisper/Ollama.
The legacy single-Arc create_router falls back to Settings::default()
so the 17 non-LLM tests stay untouched.

Verified: cargo build clean, clippy --all-targets clean, 318 tests pass.
This commit is contained in:
2026-04-27 11:48:41 +02:00
parent 6bbbf10f77
commit a510c20e75
21 changed files with 310 additions and 248 deletions
+5 -5
View File
@@ -4,11 +4,11 @@ use doctate_common::oneliners::OnelinerState;
use tracing::{info, warn};
use super::{TranscribeJob, TranscribeSender};
use crate::config::Config;
use crate::events::EventSender;
use crate::gazetteer::Gazetteer;
use crate::oneliner_locks::OnelinerLocks;
use crate::paths;
use crate::settings::Settings;
/// Walk `data_path/*/` and enqueue every recording pending transcription for
/// every user. Intended for startup; the same primitive backs the per-user
@@ -204,7 +204,7 @@ async fn has_any_transcript(case_dir: &Path) -> bool {
pub async fn regenerate_missing_oneliners(
data_path: &Path,
client: &reqwest::Client,
config: &Config,
settings: &Settings,
vocab: &Gazetteer,
events_tx: &EventSender,
locks: &OnelinerLocks,
@@ -215,7 +215,7 @@ pub async fn regenerate_missing_oneliners(
}
info!(count = cases.len(), "Regenerating missing oneliners");
for (case_dir, slug) in cases {
super::worker::update_oneliner(&case_dir, &slug, client, config, vocab, events_tx, locks)
super::worker::update_oneliner(&case_dir, &slug, client, settings, vocab, events_tx, locks)
.await;
}
}
@@ -228,7 +228,7 @@ pub async fn regenerate_missing_oneliners_for_user(
user_root: &Path,
slug: &str,
client: &reqwest::Client,
config: &Config,
settings: &Settings,
vocab: &Gazetteer,
events_tx: &EventSender,
locks: &OnelinerLocks,
@@ -238,7 +238,7 @@ pub async fn regenerate_missing_oneliners_for_user(
return;
}
for case_dir in cases {
super::worker::update_oneliner(&case_dir, slug, client, config, vocab, events_tx, locks)
super::worker::update_oneliner(&case_dir, slug, client, settings, vocab, events_tx, locks)
.await;
}
}
+12 -9
View File
@@ -14,15 +14,18 @@ use crate::events::{self, CaseEventKind, EventSender};
use crate::gazetteer::Gazetteer;
use crate::oneliner_locks::OnelinerLocks;
use crate::paths;
use crate::settings::Settings;
use crate::{BusyGuard, WorkerBusy};
const ONELINER_TIMEOUT: Duration = Duration::from_secs(60);
/// Consume transcription jobs sequentially. Whisper holds the GPU, so parallelism
/// here would only cause contention. One job in flight at a time.
#[allow(clippy::too_many_arguments)]
pub async fn run(
mut rx: TranscribeReceiver,
config: Arc<Config>,
settings: Arc<Settings>,
client: reqwest::Client,
worker_busy: WorkerBusy,
vocab: Arc<Gazetteer>,
@@ -30,7 +33,7 @@ pub async fn run(
oneliner_locks: OnelinerLocks,
) {
info!("Transcription worker started");
let timeout = Duration::from_secs(config.whisper_timeout_seconds);
let timeout = Duration::from_secs(settings.whisper.timeout_seconds);
while let Some(job) = rx.recv().await {
let _guard = BusyGuard::new(worker_busy.clone());
@@ -44,7 +47,7 @@ pub async fn run(
// Look up per-user Whisper settings live from the shared config so that
// edits to users.toml (on next restart) reach in-flight jobs naturally.
// Unknown slug → empty settings (service falls back to language=de).
let settings: WhisperUserSettings = config
let user_whisper: WhisperUserSettings = config
.users
.iter()
.find(|u| u.slug == job.user_slug)
@@ -70,10 +73,10 @@ pub async fn run(
let text = match whisper::transcribe(
&client,
&config.whisper_url,
&settings.whisper.url,
remuxed.path(),
timeout,
&settings,
&user_whisper,
)
.await
{
@@ -139,7 +142,7 @@ pub async fn run(
case_dir,
&job.user_slug,
&client,
&config,
&settings,
&vocab,
&events_tx,
&oneliner_locks,
@@ -220,7 +223,7 @@ pub async fn update_oneliner(
case_dir: &Path,
user_slug: &str,
client: &reqwest::Client,
config: &Config,
settings: &Settings,
vocab: &Gazetteer,
events_tx: &EventSender,
locks: &OnelinerLocks,
@@ -302,9 +305,9 @@ pub async fn update_oneliner(
// during the call.
let state = match ollama::generate_oneliner(
client,
&config.ollama_url,
&config.ollama_model,
config.ollama_keep_alive,
&settings.ollama.url,
&settings.ollama.model,
settings.ollama.keep_alive,
&transcript,
ONELINER_TIMEOUT,
)