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
+15 -15
View File
@@ -13,6 +13,7 @@ use doctate_server::analyze;
use doctate_server::config::Config;
use doctate_server::events;
use doctate_server::gazetteer::{Gazetteer, SpellbookDict};
use doctate_server::settings::Settings;
use doctate_server::transcribe;
#[tokio::main]
@@ -20,6 +21,9 @@ async fn main() {
dotenvy::dotenv().ok();
let config = Arc::new(Config::from_env());
let settings_path = std::env::var("SETTINGS_FILE").unwrap_or_else(|_| "settings.toml".into());
let settings = Arc::new(Settings::load_or_default(&settings_path));
// Logging: stdout + daily rotating log files.
let env_filter =
EnvFilter::try_new(&config.log_level).unwrap_or_else(|_| EnvFilter::new("info"));
@@ -43,23 +47,17 @@ async fn main() {
.init();
// Surface configuration gaps that silently disable features.
if !config.llm_configured() {
if !settings.llm_configured() {
warn!(
"LLM not configured — 'Fall abschließen' is disabled. \
Set LLM_URL, LLM_API_KEY and LLM_MODEL in .env to enable."
Set [llm].url and [llm].model in settings.toml to enable."
);
}
{
let custom = std::env::var("LLM_SYSTEM_PROMPT")
.map(|v| !v.is_empty())
.unwrap_or(false);
info!(
prompt_source = if custom { "env" } else { "default" },
prompt_chars = config.llm_system_prompt.chars().count(),
"system prompt loaded"
);
}
info!(
prompt_chars = settings.llm.system_prompt.chars().count(),
"system prompt loaded"
);
// Shared HTTP client for both downstream pipelines.
let http_client = reqwest::Client::builder()
@@ -135,6 +133,7 @@ async fn main() {
tokio::spawn(transcribe::worker::run(
transcribe_rx,
config.clone(),
settings.clone(),
http_client.clone(),
transcribe_busy.clone(),
vocab.clone(),
@@ -145,7 +144,7 @@ async fn main() {
let tx = transcribe_tx.clone();
let data_path = config.data_path.clone();
let client = http_client.clone();
let cfg = config.clone();
let settings = settings.clone();
let vocab = vocab.clone();
let events = events_tx.clone();
let heal_busy = oneliner_heal_busy.clone();
@@ -158,7 +157,7 @@ async fn main() {
// page-load does not spawn a second parallel regen loop.
let _guard = doctate_server::BusyGuard::new(heal_busy);
transcribe::recovery::regenerate_missing_oneliners(
&data_path, &client, &cfg, &vocab, &events, &locks,
&data_path, &client, &settings, &vocab, &events, &locks,
)
.await;
});
@@ -170,7 +169,7 @@ async fn main() {
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
tokio::spawn(analyze::worker::run(
analyze_rx,
config.clone(),
settings.clone(),
http_client.clone(),
analyze_busy.clone(),
vocab.clone(),
@@ -186,6 +185,7 @@ async fn main() {
let state = AppState {
config: config.clone(),
settings: settings.clone(),
transcribe_tx,
analyze_tx,
session_store: doctate_server::web_session::new_store(),