From 7bcd94f0d0e9d78445e106d312ba69599336b40f Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 15 Apr 2026 21:16:32 +0200 Subject: [PATCH] Refactor analysis input building Extract the logic for building `AnalysisInput` into a new function `build_analysis_input`. This function encapsulates the process of collecting recordings, reading transcripts, and determining the last recording modification time. This change improves code organization and reusability by centralizing the input building logic. --- server/src/auth.rs | 12 ++++ server/src/routes/case_actions.rs | 111 ++++++++++++++++-------------- 2 files changed, 71 insertions(+), 52 deletions(-) diff --git a/server/src/auth.rs b/server/src/auth.rs index 26fbb82..4838d8b 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -25,6 +25,12 @@ pub struct AuthenticatedUser { pub data_dir: PathBuf, } +impl AuthenticatedUser { + pub fn is_admin(&self) -> bool { + self.role == "admin" + } +} + impl FromRequestParts for AuthenticatedUser where S: Send + Sync, @@ -72,6 +78,12 @@ pub struct AuthenticatedWebUser { pub data_dir: PathBuf, } +impl AuthenticatedWebUser { + pub fn is_admin(&self) -> bool { + self.role == "admin" + } +} + impl FromRequestParts for AuthenticatedWebUser where S: Send + Sync, diff --git a/server/src/routes/case_actions.rs b/server/src/routes/case_actions.rs index 42d0558..a14ad24 100644 --- a/server/src/routes/case_actions.rs +++ b/server/src/routes/case_actions.rs @@ -60,56 +60,7 @@ pub async fn handle_close_case( )); } - // Collect all active `.m4a` recordings and the transcripts next to them. - // `.m4a.failed` entries are deliberately skipped: they block nothing and - // carry no transcript. The doctor handles them manually. - let m4as = collect_m4as(&case_dir).await?; - if m4as.is_empty() { - return Err(AppError::BadRequest("Keine Aufnahmen im Fall".into())); - } - - let mut recordings: Vec = Vec::new(); - let mut last_mtime: SystemTime = SystemTime::UNIX_EPOCH; - for (m4a_path, mtime) in &m4as { - if *mtime > last_mtime { - last_mtime = *mtime; - } - - let transcript_path = m4a_path.with_extension("transcript.txt"); - let text = tokio::fs::read_to_string(&transcript_path) - .await - .map_err(|_| { - AppError::BadRequest("Nicht alle Aufnahmen sind transkribiert".into()) - })?; - - if text.trim().is_empty() { - // Skip blank transcripts from the LLM input — they add noise - // without signal. The `.m4a` still contributes to `last_mtime`. - continue; - } - - let stem = m4a_path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); - recordings.push(RecordingInput { - recorded_at: filename_stem_to_recorded_at(stem), - text, - }); - } - - if recordings.is_empty() { - return Err(AppError::BadRequest( - "Keine nicht-leeren Transkripte im Fall".into(), - )); - } - - let last_recording_mtime = OffsetDateTime::from(last_mtime) - .format(&Rfc3339) - .map_err(|e| AppError::Internal(format!("format mtime: {e}")))?; - - let input = AnalysisInput { - version: 1, - last_recording_mtime, - recordings, - }; + let input = build_analysis_input(&case_dir, 1).await?; write_input_create_new(&input_path, &input).await?; @@ -171,6 +122,62 @@ pub async fn handle_document_view( .map_err(|e| AppError::Internal(format!("Template render failed: {e}"))) } +/// Build an `AnalysisInput` for the given case directory at the given version. +/// Collects all `.m4a` (excluding `.m4a.failed`), reads the matching +/// `.transcript.txt` for each, skips blank transcripts, and computes +/// `last_recording_mtime` as the max mtime over ALL `.m4a` (including blank +/// ones — a late blank addendum still counts as activity). +pub(crate) async fn build_analysis_input( + case_dir: &Path, + version: u32, +) -> Result { + let m4as = collect_m4as(case_dir).await?; + if m4as.is_empty() { + return Err(AppError::BadRequest("Keine Aufnahmen im Fall".into())); + } + + let mut recordings: Vec = Vec::new(); + let mut last_mtime: SystemTime = SystemTime::UNIX_EPOCH; + for (m4a_path, mtime) in &m4as { + if *mtime > last_mtime { + last_mtime = *mtime; + } + + let transcript_path = m4a_path.with_extension("transcript.txt"); + let text = tokio::fs::read_to_string(&transcript_path) + .await + .map_err(|_| { + AppError::BadRequest("Nicht alle Aufnahmen sind transkribiert".into()) + })?; + + if text.trim().is_empty() { + continue; + } + + let stem = m4a_path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); + recordings.push(RecordingInput { + recorded_at: filename_stem_to_recorded_at(stem), + text, + }); + } + + if recordings.is_empty() { + return Err(AppError::BadRequest( + "Keine nicht-leeren Transkripte im Fall".into(), + )); + } + + let last_recording_mtime = OffsetDateTime::from(last_mtime) + .format(&Rfc3339) + .map_err(|e| AppError::Internal(format!("format mtime: {e}")))?; + + Ok(AnalysisInput { + version, + last_recording_mtime, + recordings, + }) +} + /// Return `(path, mtime)` for every `.m4a` file directly under `case_dir`, /// sorted by filename (chronological, since filenames are UTC timestamps). /// `.m4a.failed` entries are skipped. @@ -201,7 +208,7 @@ async fn collect_m4as(case_dir: &Path) -> Result, App /// a zero-byte or partially-written JSON remains. The worker will fail to /// deserialize it and log an error — manual cleanup required. For MVP the /// simpler check-and-create is acceptable. -async fn write_input_create_new( +pub(crate) async fn write_input_create_new( path: &Path, input: &AnalysisInput, ) -> Result<(), AppError> { @@ -239,7 +246,7 @@ fn filename_stem_to_recorded_at(stem: &str) -> String { /// Scan `case_dir` for `document_v{N}.md`, pick the highest N, return its /// content together with N. Returns `None` if no document exists. -async fn find_latest_document(case_dir: &Path) -> Option<(u32, String)> { +pub(crate) async fn find_latest_document(case_dir: &Path) -> Option<(u32, String)> { let mut highest: Option = None; let mut entries = tokio::fs::read_dir(case_dir).await.ok()?; while let Ok(Some(entry)) = entries.next_entry().await {