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
+17 -10
View File
@@ -25,6 +25,7 @@ use crate::gazetteer::Gazetteer;
use crate::paths;
use crate::routes::case_actions::read_document;
use crate::routes::web::{RecordingView, scan_recordings};
use crate::settings::Settings;
use crate::transcribe::recovery as transcribe_recovery;
use crate::{BusyGuard, PipelineState};
@@ -429,7 +430,7 @@ impl PipelineState {
user_root: &Path,
slug: &str,
http_client: &reqwest::Client,
config: &Arc<Config>,
settings: &Arc<Settings>,
vocab: &Arc<Gazetteer>,
events_tx: &EventSender,
) {
@@ -456,7 +457,7 @@ impl PipelineState {
let user_root = user_root.to_path_buf();
let slug_owned = slug.to_owned();
let http_client = http_client.clone();
let config = Arc::clone(config);
let settings = Arc::clone(settings);
let vocab = Arc::clone(vocab);
let events_tx = events_tx.clone();
let busy = Arc::clone(&self.oneliner_heal_busy.0);
@@ -472,7 +473,7 @@ impl PipelineState {
&user_root,
&slug_owned,
&http_client,
&config,
&settings,
&vocab,
&events_tx,
&locks,
@@ -484,9 +485,11 @@ impl PipelineState {
}
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_my_cases(
user: AuthenticatedWebUser,
State(config): State<Arc<Config>>,
State(settings): State<Arc<Settings>>,
State(pipeline): State<PipelineState>,
State(events_tx): State<EventSender>,
State(http_client): State<reqwest::Client>,
@@ -499,7 +502,7 @@ pub async fn handle_my_cases(
&user_root,
&user.slug,
&http_client,
&config,
&settings,
&vocab,
&events_tx,
)
@@ -508,7 +511,7 @@ pub async fn handle_my_cases(
// render. The common case is "nothing to do" and costs a handful of
// stat-calls per case; actual enqueues happen only when pre-conditions
// flip (new transcripts in, stale document, ...).
auto_trigger::try_enqueue_all_for_user(&user_root, &pipeline.analyze_tx, &config, &events_tx)
auto_trigger::try_enqueue_all_for_user(&user_root, &pipeline.analyze_tx, &settings, &events_tx)
.await;
// Lazy retention sweep: at most one disk scan per /web/cases visit,
@@ -531,6 +534,7 @@ pub async fn handle_my_cases(
busy,
show_closed,
retention.auto_delete_days,
settings.llm_configured(),
)
.await;
let total = cases.len();
@@ -579,6 +583,7 @@ pub async fn handle_my_cases(
pub async fn handle_case_page(
user: AuthenticatedWebUser,
State(config): State<Arc<Config>>,
State(settings): State<Arc<Settings>>,
State(pipeline): State<PipelineState>,
State(events_tx): State<EventSender>,
State(http_client): State<reqwest::Client>,
@@ -592,7 +597,7 @@ pub async fn handle_case_page(
&user_root,
&user.slug,
&http_client,
&config,
&settings,
&vocab,
&events_tx,
)
@@ -619,7 +624,7 @@ pub async fn handle_case_page(
// Auto-analysis trigger for deep-link navigation: same evaluation as
// the list view. Document render below still wins the race if the
// worker happens to finish synchronously.
auto_trigger::try_enqueue(&case_dir, &pipeline.analyze_tx, &config, &events_tx).await;
auto_trigger::try_enqueue(&case_dir, &pipeline.analyze_tx, &settings, &events_tx).await;
let case_id_str = case_id.to_string();
info!(slug = %user.slug, case_id = %case_id, "case page viewed");
@@ -634,7 +639,7 @@ pub async fn handle_case_page(
let (oneliner, oneliner_is_manual) = compute_oneliner_display(&case_dir, &recordings).await;
let a_busy = pipeline.analyze_busy.0.load(Ordering::Acquire);
let flags = compute_flags(&case_dir, &recordings, config.llm_configured(), a_busy).await;
let flags = compute_flags(&case_dir, &recordings, settings.llm_configured(), a_busy).await;
let recordings_count = recordings.len();
let transcribed_count = recordings
@@ -681,9 +686,11 @@ pub async fn handle_case_page(
///
/// Read-only sub-page showing all `.m4a` + transcript pairs for the case.
/// Useful for power-users/admins; not part of the day-to-day workflow.
#[allow(clippy::too_many_arguments)]
pub async fn handle_case_recordings(
user: AuthenticatedWebUser,
State(config): State<Arc<Config>>,
State(settings): State<Arc<Settings>>,
State(pipeline): State<PipelineState>,
State(events_tx): State<EventSender>,
State(http_client): State<reqwest::Client>,
@@ -696,7 +703,7 @@ pub async fn handle_case_recordings(
&user_root,
&user.slug,
&http_client,
&config,
&settings,
&vocab,
&events_tx,
)
@@ -810,9 +817,9 @@ async fn scan_user_cases(
worker_busy: bool,
include_closed: bool,
auto_delete_days: u32,
llm_configured: bool,
) -> Vec<UserCaseView> {
let user_root = config.data_path.join(slug);
let llm_configured = config.llm_configured();
let mut cases = Vec::new();
let now = OffsetDateTime::now_utc();