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:
@@ -22,7 +22,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use doctate_common::oneliners::OnelinersResponse;
|
||||
use doctate_common::oneliners::{OnelinerState, OnelinersResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
@@ -48,9 +48,12 @@ pub struct CaseMarker {
|
||||
/// (`= false`) markers guard unsent uploads and are never touched by
|
||||
/// either — losing one would silently erase a patient recording.
|
||||
pub synced_to_server: bool,
|
||||
/// `None` while transcription / oneliner generation is pending; set
|
||||
/// from the server snapshot when available.
|
||||
pub oneliner: Option<String>,
|
||||
/// Persisted oneliner state mirrored from the server snapshot.
|
||||
/// `None` while no state file has been written server-side yet (fresh
|
||||
/// case, nothing transcribed). The variant distinguishes usable text
|
||||
/// from "no medical content" and "generation failed" — clients render
|
||||
/// each case as they see fit.
|
||||
pub oneliner: Option<OnelinerState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -443,10 +446,24 @@ mod tests {
|
||||
OffsetDateTime::parse(s, &Rfc3339).unwrap()
|
||||
}
|
||||
|
||||
fn ready(text: &str) -> OnelinerState {
|
||||
OnelinerState::Ready {
|
||||
text: text.to_owned(),
|
||||
generated_at: "2026-04-18T10:00:00Z".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn text_of(o: &Option<OnelinerState>) -> Option<&str> {
|
||||
match o {
|
||||
Some(OnelinerState::Ready { text, .. }) => Some(text.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn server_entry(case_id: Uuid, oneliner: Option<&str>, created: &str) -> OnelinerEntry {
|
||||
OnelinerEntry {
|
||||
case_id: case_id.to_string(),
|
||||
oneliner: oneliner.map(str::to_owned),
|
||||
oneliner: oneliner.map(ready),
|
||||
created_at: created.to_owned(),
|
||||
last_recording_at: Some(created.to_owned()),
|
||||
updated_at: Some(created.to_owned()),
|
||||
@@ -535,7 +552,7 @@ mod tests {
|
||||
let list = store.list().await;
|
||||
assert_eq!(list.len(), 1);
|
||||
assert_eq!(list[0].case_id, id);
|
||||
assert_eq!(list[0].oneliner.as_deref(), Some("Kniegelenk re."));
|
||||
assert_eq!(text_of(&list[0].oneliner), Some("Kniegelenk re."));
|
||||
assert!(list[0].synced_to_server);
|
||||
}
|
||||
|
||||
@@ -559,7 +576,7 @@ mod tests {
|
||||
let list = store.list().await;
|
||||
assert_eq!(list.len(), 1);
|
||||
assert!(list[0].synced_to_server);
|
||||
assert_eq!(list[0].oneliner.as_deref(), Some("Hypertonie"));
|
||||
assert_eq!(text_of(&list[0].oneliner), Some("Hypertonie"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -604,7 +621,7 @@ mod tests {
|
||||
let list = store.list().await;
|
||||
assert_eq!(list.len(), 1);
|
||||
assert!(list[0].synced_to_server);
|
||||
assert_eq!(list[0].oneliner.as_deref(), Some("old"));
|
||||
assert_eq!(text_of(&list[0].oneliner), Some("old"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -670,7 +687,7 @@ mod tests {
|
||||
// oneliner regenerated just now in a recovery scan.
|
||||
let entry = OnelinerEntry {
|
||||
case_id: id.to_string(),
|
||||
oneliner: Some("x".into()),
|
||||
oneliner: Some(ready("x")),
|
||||
created_at: "2026-04-17T09:00:00Z".into(),
|
||||
last_recording_at: Some("2026-04-18T09:30:00Z".into()),
|
||||
updated_at: Some("2026-04-18T10:00:00Z".into()),
|
||||
@@ -694,7 +711,7 @@ mod tests {
|
||||
let id = Uuid::new_v4();
|
||||
let entry = OnelinerEntry {
|
||||
case_id: id.to_string(),
|
||||
oneliner: Some("x".into()),
|
||||
oneliner: Some(ready("x")),
|
||||
created_at: "2026-04-17T09:00:00Z".into(),
|
||||
last_recording_at: None,
|
||||
updated_at: Some("2026-04-18T10:00:00Z".into()),
|
||||
@@ -724,7 +741,7 @@ mod tests {
|
||||
store
|
||||
.merge_server_snapshot(&server_snapshot(vec![OnelinerEntry {
|
||||
case_id: old_synced.to_string(),
|
||||
oneliner: Some("old".into()),
|
||||
oneliner: Some(ready("old")),
|
||||
created_at: "2026-04-14T10:00:00Z".into(),
|
||||
last_recording_at: Some("2026-04-14T10:00:00Z".into()),
|
||||
updated_at: Some("2026-04-14T10:00:00Z".into()),
|
||||
@@ -740,7 +757,7 @@ mod tests {
|
||||
store
|
||||
.merge_server_snapshot(&server_snapshot(vec![OnelinerEntry {
|
||||
case_id: young_synced.to_string(),
|
||||
oneliner: Some("young".into()),
|
||||
oneliner: Some(ready("young")),
|
||||
created_at: "2026-04-18T10:00:00Z".into(),
|
||||
last_recording_at: Some("2026-04-18T10:00:00Z".into()),
|
||||
updated_at: Some("2026-04-18T10:00:00Z".into()),
|
||||
|
||||
@@ -515,12 +515,17 @@ mod tests {
|
||||
}
|
||||
|
||||
fn oneliner_body(case_id: &str, oneliner: Option<&str>) -> OnelinersResponse {
|
||||
use doctate_common::oneliners::OnelinerState;
|
||||
let state = oneliner.map(|t| OnelinerState::Ready {
|
||||
text: t.to_owned(),
|
||||
generated_at: "2026-04-18T10:00:00Z".into(),
|
||||
});
|
||||
OnelinersResponse {
|
||||
as_of: "2026-04-18T10:00:00Z".into(),
|
||||
window_hours: 72,
|
||||
oneliners: vec![OnelinerEntry {
|
||||
case_id: case_id.into(),
|
||||
oneliner: oneliner.map(str::to_owned),
|
||||
oneliner: state,
|
||||
created_at: "2026-04-18T09:30:00Z".into(),
|
||||
last_recording_at: Some("2026-04-18T09:45:00Z".into()),
|
||||
updated_at: Some("2026-04-18T09:45:00Z".into()),
|
||||
|
||||
@@ -92,7 +92,7 @@ fn tmp_extension(path: &Path) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use doctate_common::oneliners::OnelinerEntry;
|
||||
use doctate_common::oneliners::{OnelinerEntry, OnelinerState};
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn sample() -> CachedSnapshot {
|
||||
@@ -104,7 +104,10 @@ mod tests {
|
||||
window_hours: 72,
|
||||
oneliners: vec![OnelinerEntry {
|
||||
case_id: "550e8400-e29b-41d4-a716-446655440000".into(),
|
||||
oneliner: Some("Kniegelenk".into()),
|
||||
oneliner: Some(OnelinerState::Ready {
|
||||
text: "Kniegelenk".into(),
|
||||
generated_at: "2026-04-18T10:30:00Z".into(),
|
||||
}),
|
||||
created_at: "2026-04-18T10:00:00Z".into(),
|
||||
last_recording_at: Some("2026-04-18T10:30:00Z".into()),
|
||||
updated_at: Some("2026-04-18T10:30:00Z".into()),
|
||||
@@ -123,10 +126,11 @@ mod tests {
|
||||
let loaded = cache.load().await.expect("cache present");
|
||||
assert_eq!(loaded.etag, s.etag);
|
||||
assert_eq!(loaded.response.oneliners.len(), 1);
|
||||
assert_eq!(
|
||||
loaded.response.oneliners[0].oneliner.as_deref(),
|
||||
Some("Kniegelenk")
|
||||
);
|
||||
let text = match &loaded.response.oneliners[0].oneliner {
|
||||
Some(OnelinerState::Ready { text, .. }) => Some(text.as_str()),
|
||||
_ => None,
|
||||
};
|
||||
assert_eq!(text, Some("Kniegelenk"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user