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:
2026-04-20 12:37:48 +02:00
parent 224ee60363
commit d65720c671
15 changed files with 440 additions and 125 deletions
+39 -5
View File
@@ -9,6 +9,40 @@ use serde::{Deserialize, Serialize};
/// HTTP path of the oneliners endpoint.
pub const ONELINERS_PATH: &str = "/api/oneliners";
/// Filename used by the server to persist [`OnelinerState`] per case
/// (inside the case directory). Shared so both writer and readers
/// agree on the exact path.
pub const ONELINER_FILENAME: &str = "oneliner.json";
/// Outcome of the LLM oneliner generation for a single case.
///
/// Three disjoint outcomes used to collapse onto the same filesystem
/// representation (missing file). Making them first-class lets the UI
/// distinguish "LLM said nothing medical" (valid empty result) from
/// "LLM call errored" (transient failure worth retrying) from "file
/// simply isn't there yet" (no state persisted).
///
/// Serialized as internally-tagged JSON: `{"kind":"ready","text":...}`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum OnelinerState {
/// LLM produced a usable one-sentence summary.
Ready {
text: String,
/// RFC3339 UTC timestamp of the LLM call that produced this
/// text. Diagnostic only — the file mtime drives ETag/sort.
generated_at: String,
},
/// LLM deliberately returned an empty answer — the transcript
/// contained no medical content. This is a valid outcome, not a
/// failure, and recovery must not retry it.
Empty { generated_at: String },
/// LLM call failed (timeout, HTTP, parse, …). Details stay in the
/// server logs on purpose; the UI surfaces only "error", and
/// startup recovery retries this variant.
Error { generated_at: String },
}
/// Full response body of `GET /api/oneliners`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OnelinersResponse {
@@ -29,14 +63,14 @@ pub struct OnelinersResponse {
/// including addenda). This is the **primary sort key** for clients:
/// "when did the doctor last work on this case?". Absent only if the
/// case has no recordings at all (shouldn't happen in normal flow).
/// - `updated_at` — `oneliner.txt` mtime, i.e. when the oneliner text
/// was last regenerated. Diagnostic only — not for sort or display.
/// - `oneliner` — the text itself; `None` while transcription / LLM
/// generation is still pending.
/// - `updated_at` — `oneliner.json` mtime, i.e. when the oneliner state
/// was last written. Diagnostic only — not for sort or display.
/// - `oneliner` — the persisted state; `None` while no state file has
/// been written yet for this case (fresh case, nothing transcribed).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OnelinerEntry {
pub case_id: String,
pub oneliner: Option<String>,
pub oneliner: Option<OnelinerState>,
pub created_at: String,
pub last_recording_at: Option<String>,
pub updated_at: Option<String>,