Pulls three pattern groups into shared helpers so a rename or wire-format
tweak edits one file instead of 10+:
- BulkAction enum in doctate-common replaces the "close"/"analyze"/"reset"
string literals that were duplicated between the bulk handler and 3
attack/CSRF test files. FromStr preserves the exact "Unbekannte Aktion"
error shape; the handler match is now exhaustive over the enum.
- join_url helper in doctate-common absorbs 7 identical
trim_end_matches+format! call sites across client-core, client-desktop,
server/transcribe (ollama, whisper), and server/analyze (llm).
- server/tests/common/artefacts.rs re-exports ONELINER_FILENAME
(doctate-common), DOCUMENT_FILE + ANALYSIS_INPUT_FILE
(doctate-server::analyze), and CLOSE_MARKER (doctate-server::paths) so
test files reference the canonical name instead of inlining literals.
Also lifts client-desktop local duplication:
- paths.rs: project_path helper collapses 4 identical ProjectDirs chains
- main.rs: or_die helper replaces 4 eprintln!+exit(1) blocks
- app.rs: named RecordingContext struct replaces (Uuid, String) tuple
at 3 sites around the ffmpeg-flush finalization path
Verification: 397 tests pass (baseline was 389; +8 for new unit tests on
BulkAction and join_url), 0 failed, 4 ignored (unchanged). clippy clean.
The /api/oneliners time window is now read per-user from users.toml
(window_hours, default 72h). Clients no longer carry a window:
client.toml oneliner_window_hours, SyncConfig.window_hours, the
?hours=N query param, and the render_case_list cutoff filter are
gone. ETag suffix keeps the effective hours so an admin edit to
users.toml invalidates client caches on the next request.
OnelinersResponse.window_hours stays in the wire format, but now
exists solely to anchor client reconciliation.
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.
Remove the per-user oneliner watermark, which was previously used to
generate ETags for the `/api/oneliners` endpoint. The watermark was
intended to track the last successful oneliner write for each user.
The ETag generation has been refactored to use a deterministic FNV-1a
hash. This hash is computed over a sorted list of tuples containing
`(case_id, last_recording_at_ns, oneliner_mtime_ns)` for all visible
cases. This approach ensures that the ETag changes whenever any
listen-relevant file system modification occurs, such as a new
recording, oneliner regeneration, soft-delete, undo, or
upload-auto-reopen. This new method is more robust and avoids the
complexity of managing per-user state.
The `OnelinerWatermark` type alias and its associated logic have been
removed from `AppState` and the relevant modules (`transcribe::worker`,
`transcribe::recovery`, `routes::oneliners`, `main`). New integration
tests have been added to verify that various delete operations (single
delete, undo delete, bulk delete) correctly trigger an ETag update,
ensuring cache invalidation.
Introduce `reconcile_with_server_snapshot` to the `CaseStore`. This
function
removes local markers that are synced to the server and within the
server's reported window but are no longer present in the server's
snapshot. This ensures that cases deleted server-side are also removed
locally, fixing an issue where deleted cases would still appear on the
client.
This change also modifies the `poll_once` function in `server_sync`
to call both `merge_server_snapshot` and the new
`reconcile_with_server_snapshot` after a successful poll.
Additionally, a test case `poll_once_reconciles_missing_synced_marker`
has been added to verify the correct behavior of the reconciliation
process.
The server-side upload handler is updated to automatically reopen
soft-deleted cases if a new upload is received for them. This ensures
that soft-deleted cases reappear in the API and UI without manual
intervention. Two new tests, `upload_reopens_soft_deleted_case` and
`upload_resurrects_hard_deleted_case`, are added to cover the
soft-delete
reopening and hard-delete resurrection scenarios, respectively.
The TOML dependency is added to `Cargo.lock` to support configuration
file parsing.
New modules `config` and `startup` are introduced in
`doctate-client-core`:
- `config`: Handles client configuration loading and saving, including
defaults for polling intervals and window hours.
- `startup`: Contains routines for initial cleanup tasks, such as
removing stale markers and orphan audio files based on defined
retention periods.
This commit introduces a visual indicator in the UI to show when a case
has pending uploads.
The `WorkerSnapshot` struct has been updated to include a
`pending_case_ids` field, which is an `Arc<HashSet<Uuid>>`. This set
stores the IDs of cases that currently have files in the pending
directory.
The `run_loop` function now populates this set by scanning the pending
directory and collecting the `case_id`s of any files found. This
snapshot is then sent to the UI via the `worker_rx` channel.
In the `DoctateApp::ui` method, the `pending_ids` set is cloned from the
worker's snapshot. If a case's ID is present in this set, a distinct "⬆"
prefix and a yellow label are used to highlight it, signifying an
ongoing or pending upload. Otherwise, the standard label is used.
The `WorkerSnapshot` now includes `last_failure`, which tracks the time
of the last recorded error. This allows the UI to display a more
accurate status, especially when a failure occurs after a recent
success.
The `pick_footer_status` function has been updated to prioritize
`last_failure`. If a failure occurred more recently than the last
success, the footer will show `Offline`, regardless of the success age.
This addresses scenarios where the server might have temporarily failed,
and the UI should reflect that immediately.
The `run_loop` function now updates `last_failure` upon transient or
terminal upload errors and poll failures. This ensures that the new
state information is correctly propagated.
This commit consolidates the upload and polling functionalities into a
single `ServerSync` worker. The `oneliner_poller` and `uploader` modules
have been removed, and their responsibilities are now handled by
`server_sync`.
The `ServerSync` worker manages both uploading pending recordings and
polling the `/api/oneliners` endpoint. Uploads take precedence, and
polling only occurs when the pending upload directory is empty. The
worker now uses a unified HTTP client and a single `tokio::select!` loop
for managing these tasks.
Key changes include:
- Removal of `oneliner_poller.rs` and `uploader.rs`.
- Introduction of `server_sync.rs` and `upload.rs` in
`doctate-client-core`.
- `ServerSync` now handles upload events (`UploadEvent`) and provides a
`WorkerSnapshot` for UI feedback.
- Upload logic has been refactored to handle transient and terminal
errors more robustly, including file deletion for terminal failures.
- Polling logic is integrated into the `ServerSync` loop, utilizing the
existing `SnapshotCache` and `CaseStore`.
- The `App` component in `client-desktop` has been updated to use
`ServerSync` instead of the separate poller and uploader.
- Documentation comments have been updated to reflect the new structure.
This commit introduces a new startup cleanup routine for the Doctate
client.
The cleanup process reaps stale markers and orphaned audio files to
adhere to data minimization principles and maintain client efficiency.
Specifically, it performs the following actions:
- Removes markers that are older than 72 hours and have been confirmed
by
the server. This ensures that old, irrelevant metadata is purged.
- Cleans up orphaned audio files (e.g., `.m4a` without corresponding
`.meta.json` or vice-versa) that are older than 24 hours. This
prevents the accumulation of audio data that will never be uploaded,
as such files are silently ignored by the uploader.
- Removes any temporary files (`.tmp`) left over from interrupted
writes,
as these are considered invalid at startup.
The `filetime` crate is added as a dependency to facilitate setting
modification times for testing purposes.
Introduce `last_recording_at` to `OnelinerEntry` and use it as the
primary sort key for cases. This ensures that cases with recent
dictation activity are prioritized, even if their initial creation date
is older.
The logic for determining a case's activity has been updated to consider
the most recent `.m4a` file's modification time (`last_recording_at`),
falling back to `updated_at` or `created_at` if necessary.
This change also refactors the timestamp formatting and handling within
the web interface to correctly display and sort cases based on their
actual last activity time, improving user experience and data relevance.
The `now_rfc3339` function is updated to strip sub-second precision for
consistent filename generation.
Add `SnapshotCache` struct to handle on-disk caching of API responses.
This includes `load`, `store`, and `clear` methods for managing the
cache file.
Atomic writes are implemented using a temporary file and rename
operation.
Error handling for file operations and JSON deserialization is included.
Unit tests are provided to verify cache functionality.
Introduces a new crate `doctate-client-core` to house shared client-side
logic. This includes:
- `case_store`: Manages local case marker files and merging with server
snapshots.
- `snapshot_cache`: Placeholder for caching server responses.
- `oneliner_poller`: Placeholder for the background polling task.
The workspace configuration and `Cargo.lock` have been updated to
include the new crate.