refactor: consolidate bulk actions, URL assembly, and case-artefact filenames

Pulls three pattern groups into shared helpers so a rename or wire-format
tweak edits one file instead of 10+:

- BulkAction enum in doctate-common replaces the "close"/"analyze"/"reset"
  string literals that were duplicated between the bulk handler and 3
  attack/CSRF test files. FromStr preserves the exact "Unbekannte Aktion"
  error shape; the handler match is now exhaustive over the enum.
- join_url helper in doctate-common absorbs 7 identical
  trim_end_matches+format! call sites across client-core, client-desktop,
  server/transcribe (ollama, whisper), and server/analyze (llm).
- server/tests/common/artefacts.rs re-exports ONELINER_FILENAME
  (doctate-common), DOCUMENT_FILE + ANALYSIS_INPUT_FILE
  (doctate-server::analyze), and CLOSE_MARKER (doctate-server::paths) so
  test files reference the canonical name instead of inlining literals.

Also lifts client-desktop local duplication:
- paths.rs: project_path helper collapses 4 identical ProjectDirs chains
- main.rs: or_die helper replaces 4 eprintln!+exit(1) blocks
- app.rs: named RecordingContext struct replaces (Uuid, String) tuple
  at 3 sites around the ffmpeg-flush finalization path

Verification: 397 tests pass (baseline was 389; +8 for new unit tests on
BulkAction and join_url), 0 failed, 4 ignored (unchanged). clippy clean.
This commit is contained in:
2026-04-22 12:09:46 +02:00
parent af3377a6bc
commit 813fb896ff
25 changed files with 416 additions and 166 deletions
+2 -4
View File
@@ -1,5 +1,6 @@
use std::time::Duration;
use doctate_common::join_url;
use serde::{Deserialize, Serialize};
use tracing::debug;
@@ -84,10 +85,7 @@ pub async fn chat_once(
user_content: &str,
timeout: Duration,
) -> Result<String, LlmError> {
let url = format!(
"{}/v1/chat/completions",
settings.base_url.trim_end_matches('/')
);
let url = join_url(settings.base_url, "/v1/chat/completions");
debug!(%url, model = settings.model, "calling llm chat completions");
let body = ChatRequest {
+14 -8
View File
@@ -1,7 +1,9 @@
use std::str::FromStr;
use std::sync::Arc;
use axum::extract::State;
use axum::response::Redirect;
use doctate_common::BulkAction;
use doctate_common::timestamp::now_rfc3339;
use serde::Deserialize;
use tracing::{info, warn};
@@ -55,8 +57,16 @@ pub async fn handle_bulk_action(
let user_root = config.data_path.join(&user.slug);
match form.action.as_str() {
"analyze" => {
// Parse the wire token into the shared enum so the match below is
// exhaustive at compile time. Keep the exact "Unbekannte Aktion"
// error shape the handler returned before the enum existed.
let action = BulkAction::from_str(&form.action).map_err(|err| {
warn!(slug = %user.slug, action = %err.0, "bulk: unknown action");
AppError::BadRequest("Unbekannte Aktion".into())
})?;
match action {
BulkAction::Analyze => {
bulk_analyze(
&config,
&analyze_tx,
@@ -67,12 +77,8 @@ pub async fn handle_bulk_action(
)
.await
}
"close" => bulk_close(&events_tx, &user.slug, &user_root, &form.case_ids).await,
"reset" => bulk_reset(&events_tx, &user.slug, &user_root, &form.case_ids).await,
other => {
warn!(slug = %user.slug, action = %other, "bulk: unknown action");
return Err(AppError::BadRequest("Unbekannte Aktion".into()));
}
BulkAction::Close => bulk_close(&events_tx, &user.slug, &user_root, &form.case_ids).await,
BulkAction::Reset => bulk_reset(&events_tx, &user.slug, &user_root, &form.case_ids).await,
}
Ok(Redirect::to("/web/cases"))
+2 -1
View File
@@ -1,5 +1,6 @@
use std::time::Duration;
use doctate_common::join_url;
use serde::{Deserialize, Serialize};
use tracing::debug;
@@ -100,7 +101,7 @@ pub async fn generate_oneliner(
transcript: &str,
timeout: Duration,
) -> Result<String, OllamaError> {
let url = format!("{}/api/chat", ollama_url.trim_end_matches('/'));
let url = join_url(ollama_url, "/api/chat");
debug!(%url, model, "generating oneliner");
let body = ChatRequest {
+3 -3
View File
@@ -1,6 +1,7 @@
use std::path::Path;
use std::time::Duration;
use doctate_common::join_url;
use reqwest::multipart::{Form, Part};
use tracing::debug;
@@ -100,9 +101,8 @@ pub async fn transcribe(
.filter(|s| !s.is_empty())
.unwrap_or("de");
let url = format!(
"{}/asr?output=txt&language={}",
whisper_url.trim_end_matches('/'),
language
"{}?output=txt&language={language}",
join_url(whisper_url, "/asr")
);
debug!(%url, "posting to whisper");