Files
doctate/server/src/paths.rs
T
Brummel d65720c671 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.
2026-04-20 12:37:48 +02:00

84 lines
3.0 KiB
Rust

use std::path::{Path, PathBuf};
use doctate_common::oneliners::{ONELINER_FILENAME, OnelinerState};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use uuid::Uuid;
/// Absolute on-disk location of a case directory.
///
/// Single source of truth for the layout `<data_path>/<slug>/<case_id>/`.
/// Use everywhere instead of inlining `.join(slug).join(case_id)` so the
/// layout can evolve in one place.
pub fn case_dir(data_path: &Path, slug: &str, case_id: &str) -> PathBuf {
data_path.join(slug).join(case_id)
}
/// Filename of the soft-delete marker inside a case directory.
pub const DELETE_MARKER: &str = ".deleted";
/// Soft-delete marker payload. One file per deleted case; all cases removed
/// in the same user click share the same `batch` UUID. `deleted_at` is an
/// RFC3339 timestamp — used to find the most recent batch for "Undo last
/// delete".
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteMarker {
pub batch: Uuid,
pub deleted_at: String,
}
/// True iff the case directory contains a `.deleted` marker file.
pub async fn is_deleted(case_dir: &Path) -> bool {
tokio::fs::try_exists(case_dir.join(DELETE_MARKER))
.await
.unwrap_or(false)
}
/// Read and parse the `.deleted` marker. Returns `None` if absent or malformed
/// (treated identically — a malformed marker still means "deleted" via
/// `is_deleted`, but cannot participate in batch-undo).
pub async fn read_delete_marker(case_dir: &Path) -> Option<DeleteMarker> {
let bytes = tokio::fs::read(case_dir.join(DELETE_MARKER)).await.ok()?;
serde_json::from_slice(&bytes).ok()
}
/// Write the `.deleted` marker as JSON. Overwrites any existing marker.
pub async fn write_delete_marker(case_dir: &Path, marker: &DeleteMarker) -> std::io::Result<()> {
let bytes = serde_json::to_vec(marker)
.map_err(|e| std::io::Error::other(format!("serialize delete marker: {e}")))?;
tokio::fs::write(case_dir.join(DELETE_MARKER), bytes).await
}
/// Read the persisted oneliner state for a case.
///
/// Returns `(Some(state), Some(mtime))` when `oneliner.json` exists and
/// parses. Missing file → `(None, None)`. A file that exists but fails
/// to parse is treated as missing and logged — the next transcription
/// job will overwrite it with a well-formed state.
pub async fn read_oneliner_state(
case_dir: &Path,
) -> (Option<OnelinerState>, Option<OffsetDateTime>) {
let path = case_dir.join(ONELINER_FILENAME);
let bytes = match tokio::fs::read(&path).await {
Ok(b) => b,
Err(_) => return (None, None),
};
let state: OnelinerState = match serde_json::from_slice(&bytes) {
Ok(s) => s,
Err(e) => {
tracing::warn!(
path = %path.display(),
error = %e,
"malformed oneliner.json; treating as missing"
);
return (None, None);
}
};
let mtime = tokio::fs::metadata(&path)
.await
.ok()
.and_then(|m| m.modified().ok())
.map(OffsetDateTime::from);
(Some(state), mtime)
}