Refactor oneliner storage to use enum
The oneliner is now stored as `OnelinerState` enum which can represent three states: `Ready`, `Empty` or `Error`. This allows the client to differentiate between a case with no medical content and a case where the oneliner generation failed. The `OnelinerState` enum is serialized to JSON and stored in `oneliner.json` file. The `OnelinerEntry` struct has been updated to reflect this change. The client-desktop application has been updated to handle the new `OnelinerState` enum and display appropriate UI elements for each state. The server-side code has also been updated to read and write the `OnelinerState` enum, and to handle the new file format. The `reset_case_artefacts` function in `server/src/routes/case_actions.rs` has been updated to remove `oneliner.txt` and create `oneliner.json` instead. The tests have been updated to reflect these changes.
This commit is contained in:
@@ -5,6 +5,7 @@ use std::sync::atomic::Ordering;
|
||||
use askama::Template;
|
||||
use axum::extract::State;
|
||||
use axum::response::Html;
|
||||
use doctate_common::oneliners::OnelinerState;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use time::{Date, OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
@@ -18,10 +19,34 @@ use crate::case_id::{CaseId, CaseIdPath};
|
||||
use crate::config::Config;
|
||||
use crate::error::AppError;
|
||||
use crate::events::EventSender;
|
||||
use crate::paths;
|
||||
use crate::routes::case_actions::read_document;
|
||||
use crate::routes::web::{RecordingView, scan_recordings};
|
||||
use crate::transcribe::recovery as transcribe_recovery;
|
||||
|
||||
/// UI-layer oneliner status for the case list. Combines the persisted
|
||||
/// [`OnelinerState`] with derived transit states that only make sense
|
||||
/// in the list context (transcription still pending, or no state file
|
||||
/// despite completed transcription).
|
||||
///
|
||||
/// Kept local to the server — not part of the public API DTO, which
|
||||
/// exposes the raw [`OnelinerState`] and lets each client decide how
|
||||
/// to render transit states.
|
||||
enum OnelinerDisplay {
|
||||
Ready(String),
|
||||
Empty,
|
||||
Error,
|
||||
/// No state file on disk + at least one non-failed recording is
|
||||
/// still awaiting its transcript. The oneliner will appear once
|
||||
/// transcription finishes.
|
||||
Pending,
|
||||
/// No state file on disk and no pending transcription. Rare in
|
||||
/// practice — the worker writes a state at the end of every job —
|
||||
/// but possible right after a server crash between transcript and
|
||||
/// oneliner write.
|
||||
Missing,
|
||||
}
|
||||
|
||||
struct UserCaseView {
|
||||
case_id: String,
|
||||
most_recent: String,
|
||||
@@ -34,7 +59,7 @@ struct UserCaseView {
|
||||
/// render it in the doctor's actual timezone (which may differ from
|
||||
/// the server's, e.g. server on Bahamas, doctor in Germany).
|
||||
recorded_at_iso: String,
|
||||
oneliner: Option<String>,
|
||||
oneliner: OnelinerDisplay,
|
||||
recordings_count: usize,
|
||||
analyzing: bool,
|
||||
has_document: bool,
|
||||
@@ -307,11 +332,7 @@ pub async fn handle_case_page(
|
||||
.map(|md| crate::analyze::render::md_to_html(&md));
|
||||
|
||||
let recordings = scan_recordings(&case_dir).await;
|
||||
let oneliner = tokio::fs::read_to_string(case_dir.join("oneliner.txt"))
|
||||
.await
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
let oneliner = ready_text(&case_dir).await;
|
||||
|
||||
let a_busy = pipeline.analyze_busy.0.load(Ordering::Acquire);
|
||||
let flags = compute_flags(&case_dir, &recordings, config.llm_configured(), a_busy).await;
|
||||
@@ -371,11 +392,7 @@ pub async fn handle_case_recordings(
|
||||
info!(slug = %user.slug, case_id = %case_id, "case recordings viewed");
|
||||
|
||||
let recordings = scan_recordings(&case_dir).await;
|
||||
let oneliner = tokio::fs::read_to_string(case_dir.join("oneliner.txt"))
|
||||
.await
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty());
|
||||
let oneliner = ready_text(&case_dir).await;
|
||||
|
||||
let t_busy = pipeline.transcribe_busy.0.load(Ordering::Acquire);
|
||||
let case_id_short = case_id_str.chars().take(8).collect();
|
||||
@@ -509,11 +526,14 @@ async fn compute_case_view(
|
||||
.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 (state, _) = paths::read_oneliner_state(case_path).await;
|
||||
let oneliner = match state {
|
||||
Some(OnelinerState::Ready { text, .. }) => OnelinerDisplay::Ready(text),
|
||||
Some(OnelinerState::Empty { .. }) => OnelinerDisplay::Empty,
|
||||
Some(OnelinerState::Error { .. }) => OnelinerDisplay::Error,
|
||||
None if !all_transcribed => OnelinerDisplay::Pending,
|
||||
None => OnelinerDisplay::Missing,
|
||||
};
|
||||
|
||||
let has_document = any_document_exists(case_path).await;
|
||||
let analyzing = !has_document && worker_busy && any_analysis_input_exists(case_path).await;
|
||||
@@ -533,3 +553,14 @@ async fn compute_case_view(
|
||||
can_analyze,
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract only the text from a `Ready` state; other states (Empty,
|
||||
/// Error, missing file) collapse to `None`. Used by single-case pages
|
||||
/// whose headings simply fall back to a generic placeholder — they do
|
||||
/// not need the full five-state UI treatment.
|
||||
async fn ready_text(case_dir: &Path) -> Option<String> {
|
||||
match paths::read_oneliner_state(case_dir).await.0 {
|
||||
Some(OnelinerState::Ready { text, .. }) => Some(text),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user