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.