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
+3 -63
View File
@@ -75,6 +75,9 @@ struct UsersFile {
user: Vec<User>,
}
/// Bootstrap config loaded once from `.env` at startup. Runtime-tunable
/// values (retention, whisper/ollama/llm endpoints, prompts) live in
/// `Settings` (loaded from `settings.toml`).
pub struct Config {
// Phase 1 — required
pub server_port: u16,
@@ -86,24 +89,6 @@ pub struct Config {
pub api_keys: HashMap<String, String>, // api_key_value → slug
// Phase 2+ — optional with defaults
pub retention_audio_days: u32,
pub retention_transcript_days: u32,
pub retention_document_days: u32,
pub whisper_url: String,
pub whisper_timeout_seconds: u64,
pub ollama_url: String,
pub ollama_model: String,
pub ollama_keep_alive: u32,
pub llm_url: String,
pub llm_api_key: String,
pub llm_model: String,
pub llm_temperature: f32,
pub llm_timeout_seconds: u64,
/// System prompt sent to the analysis LLM. Defaults to
/// `crate::analyze::prompt::SYSTEM_PROMPT` if `LLM_SYSTEM_PROMPT` is unset
/// or empty. Runtime-configurable so admins can iterate and use the
/// "Neu analysieren" button to re-run cases.
pub llm_system_prompt: String,
/// Directory containing gazetteer `*.txt` files (anatomy, substances,
/// medications). Loaded once at startup; a missing directory is a
/// non-fatal WARN — analysis runs without proper-name correction.
@@ -135,27 +120,6 @@ impl Config {
users,
api_keys,
retention_audio_days: optional_env_parsed("RETENTION_AUDIO_DAYS", 30),
retention_transcript_days: optional_env_parsed("RETENTION_TRANSCRIPT_DAYS", 30),
retention_document_days: optional_env_parsed("RETENTION_DOCUMENT_DAYS", 0),
whisper_url: optional_env("WHISPER_URL", "http://localhost:10300"),
whisper_timeout_seconds: optional_env_parsed("WHISPER_TIMEOUT_SECONDS", 120),
ollama_url: optional_env("OLLAMA_URL", "http://localhost:11434"),
ollama_model: optional_env("OLLAMA_MODEL", "gemma3:4b"),
ollama_keep_alive: optional_env_parsed("OLLAMA_KEEP_ALIVE", 0),
llm_url: optional_env("LLM_URL", ""),
llm_api_key: optional_env("LLM_API_KEY", ""),
llm_model: optional_env("LLM_MODEL", ""),
llm_temperature: optional_env_parsed("LLM_TEMPERATURE", 0.0),
llm_timeout_seconds: optional_env_parsed("LLM_TIMEOUT_SECONDS", 180),
llm_system_prompt: {
let v = optional_env("LLM_SYSTEM_PROMPT", "");
if v.is_empty() {
crate::analyze::prompt::SYSTEM_PROMPT.to_string()
} else {
v
}
},
vocab_dir: PathBuf::from(optional_env("VOCAB_DIR", "./vocab")),
hunspell_dict_path: PathBuf::from(optional_env(
"HUNSPELL_DICT",
@@ -166,16 +130,6 @@ impl Config {
}
}
/// True iff the external analysis LLM is configured. `llm_api_key` is
/// intentionally **not** required — Ollama's OpenAI-compatible endpoint
/// at `/v1/chat/completions` accepts unauthenticated requests and is a
/// supported deployment target. For hosted providers (Ionos, OpenAI) the
/// key remains necessary in practice; the provider will reject with 401
/// if it is missing, which is correct feedback.
pub fn llm_configured(&self) -> bool {
!self.llm_url.is_empty() && !self.llm_model.is_empty()
}
/// Sane defaults for integration tests. Not a `Default` impl on purpose:
/// production code must go through `from_env()`, and `Default::default()`
/// carries an implicit "safe fallback" connotation this value does not
@@ -199,20 +153,6 @@ impl Config {
log_max_days: 90,
users: Vec::new(),
api_keys: HashMap::new(),
retention_audio_days: 30,
retention_transcript_days: 30,
retention_document_days: 0,
whisper_url: "http://localhost:10300".into(),
whisper_timeout_seconds: 120,
ollama_url: "http://localhost:11434".into(),
ollama_model: "gemma3:4b".into(),
ollama_keep_alive: 0,
llm_url: String::new(),
llm_api_key: String::new(),
llm_model: String::new(),
llm_temperature: 0.0,
llm_timeout_seconds: 180,
llm_system_prompt: crate::analyze::prompt::SYSTEM_PROMPT.to_string(),
vocab_dir: PathBuf::new(),
hunspell_dict_path: PathBuf::new(),
session_timeout_hours: 8,