From bce93811a056b547510bc54bee0e9af37ac7d630 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 19 Apr 2026 15:19:20 +0200 Subject: [PATCH] refactor(server): split scan_user_cases into filter + view helpers The 73-line function mixed three concerns: directory traversal, UUID/deletion filtering, and view assembly. Split into: - scan_user_cases: orchestrator (loop + sort). - filter_valid_case_dir: gate that returns (case_id, case_path) for every valid case directory, skipping non-UUID names, files, and soft-deleted cases. - compute_case_view: view assembly that returns None for empty cases (no recordings yet). Each helper is now individually inspectable; nesting depth drops from 3-4 to 2 levels. Let-else guards replace nested matches. --- server/src/routes/user_web.rs | 135 ++++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 56 deletions(-) diff --git a/server/src/routes/user_web.rs b/server/src/routes/user_web.rs index e7d7336..597088a 100644 --- a/server/src/routes/user_web.rs +++ b/server/src/routes/user_web.rs @@ -347,66 +347,89 @@ async fn scan_user_cases(config: &Config, slug: &str, worker_busy: bool) -> Vec< Err(_) => return cases, }; - while let Ok(Some(case_entry)) = entries.next_entry().await { - let case_id = match case_entry.file_name().into_string() { - Ok(s) => s, - Err(_) => continue, + while let Ok(Some(entry)) = entries.next_entry().await { + let Some((case_id, case_path)) = filter_valid_case_dir(entry).await else { + continue; }; - if uuid::Uuid::parse_str(&case_id).is_err() { - warn!(case_id = %case_id, "Skipping non-UUID directory"); - continue; + if let Some(view) = + compute_case_view(case_id, &case_path, llm_configured, worker_busy).await + { + cases.push(view); } - let case_path = case_entry.path(); - if !case_path.is_dir() { - continue; - } - if crate::paths::is_deleted(&case_path).await { - continue; - } - - let recordings = scan_recordings(&case_path).await; - if recordings.is_empty() { - continue; - } - - let most_recent = recordings - .last() - .map(|r| r.filename.clone()) - .unwrap_or_default(); - let recordings_count = recordings.len(); - let non_failed_count = recordings.iter().filter(|r| !r.failed).count(); - let all_transcribed = non_failed_count > 0 - && recordings - .iter() - .filter(|r| !r.failed) - .all(|r| r.transcript.is_some()); - - let oneliner = tokio::fs::read_to_string(case_path.join("oneliner.txt")) - .await - .ok() - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()); - - let has_document = any_document_exists(&case_path).await; - let analyzing = - !has_document && worker_busy && any_analysis_input_exists(&case_path).await; - let can_analyze = !analyzing && all_transcribed && llm_configured; - - let time_hms_utc = extract_hhmm_utc(&most_recent); - let recorded_at_iso = recorded_at_iso_of(&most_recent); - cases.push(UserCaseView { - case_id, - most_recent, - time_hms_utc, - recorded_at_iso, - oneliner, - recordings_count, - analyzing, - has_document, - can_analyze, - }); } cases.sort_by(|a, b| b.most_recent.cmp(&a.most_recent)); cases } + +/// Accept an entry only if it is a non-deleted directory whose name is +/// a valid UUID. Returns the (case_id-string, absolute path) tuple so +/// the caller does not need to recompute either. Non-UUID names are +/// logged at WARN level; other rejections are silent (common case). +async fn filter_valid_case_dir( + entry: tokio::fs::DirEntry, +) -> Option<(String, std::path::PathBuf)> { + let case_id = entry.file_name().into_string().ok()?; + if uuid::Uuid::parse_str(&case_id).is_err() { + warn!(case_id = %case_id, "Skipping non-UUID directory"); + return None; + } + let case_path = entry.path(); + if !case_path.is_dir() { + return None; + } + if crate::paths::is_deleted(&case_path).await { + return None; + } + Some((case_id, case_path)) +} + +/// Build a `UserCaseView` for a single case directory. Returns `None` +/// for empty cases (no recordings yet) so the caller can skip them. +async fn compute_case_view( + case_id: String, + case_path: &Path, + llm_configured: bool, + worker_busy: bool, +) -> Option { + let recordings = scan_recordings(case_path).await; + if recordings.is_empty() { + return None; + } + + let most_recent = recordings + .last() + .map(|r| r.filename.clone()) + .unwrap_or_default(); + let recordings_count = recordings.len(); + let non_failed_count = recordings.iter().filter(|r| !r.failed).count(); + let all_transcribed = non_failed_count > 0 + && recordings + .iter() + .filter(|r| !r.failed) + .all(|r| r.transcript.is_some()); + + let oneliner = tokio::fs::read_to_string(case_path.join("oneliner.txt")) + .await + .ok() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()); + + let has_document = any_document_exists(case_path).await; + let analyzing = !has_document && worker_busy && any_analysis_input_exists(case_path).await; + let can_analyze = !analyzing && all_transcribed && llm_configured; + + let time_hms_utc = extract_hhmm_utc(&most_recent); + let recorded_at_iso = recorded_at_iso_of(&most_recent); + Some(UserCaseView { + case_id, + most_recent, + time_hms_utc, + recorded_at_iso, + oneliner, + recordings_count, + analyzing, + has_document, + can_analyze, + }) +}