GET /web/cases now honours ?show_closed=1: the toggle link flips the
listing between open-only (default) and open+closed (muted styling,
grey "geschlossen" badge, countdown "wird in N Tagen entfernt" when
auto_delete_days > 0). Each closed row shows a Lucide rotate-ccw
reopen button in the same slot where the close button sits on open
cases; selection checkboxes are suppressed so bulk-close cannot touch
closed cases.
GET /web/cases/:id honours ?show_closed=1 too: without the query a
closed case still 404s (keeps the default listing clean), with it the
detail page renders the reopen form in the header. The case_page
template swaps the icon based on is_closed.
The purge-closed bulk button from the earlier commit finally gets a
UI: it appears below the listing when show_closed=1 is active AND at
least one closed case is visible, with a JS confirm() + the
server-side confirm=yes guard. Two integration tests (listing +
detail) cover the happy path.
- POST /web/cases/:id/delete renamed to /close; emits CaseClosed.
- New POST /web/cases/:id/reopen removes the marker; emits CaseReopened.
- Bulk action "delete" renamed to "close"; the shared closed_at timestamp
still groups the selection for future UI features.
- POST /web/cases/undo-delete removed along with summarize_latest_close_group,
latest_close_timestamp and restore_close_group. Per-case reopen makes
the group-undo banner obsolete.
- CaseEventKind::CaseDeleted -> CaseClosed, CaseRestored -> CaseReopened.
- Templates updated to Lucide trash-2 icon, German label "Fall schliessen".
- Tests migrated; delete_watermark_test.rs moved to close_watermark_test.rs.
This commit introduces a new API endpoint for deleting recordings. The
endpoint
handles the removal of the audio file and its associated sidecar files
(transcript,
duration). It also invalidates derived artifacts like oneliner.json,
document.md,
and analysis_input.json. Additionally, it includes checks for path
traversal,
correct file extensions, and case ownership to ensure security and data
integrity.
A new `RecordingDeleted` event is also added to the `CaseEventKind`
enum.
Adjust the `justify-content` of the player to align items to the right,
and limit the `max-width` of the seek bar to prevent overflow and
improve layout on smaller screens.
Introduced a new macro `render` in `partials/oneliner.html` to handle
the display of the oneliner for cases. This macro encapsulates the logic
for rendering the oneliner, including states for ready, empty, error,
pending, and generating.
The `case_page.html` and `my_cases.html` templates are updated to use
this new macro. This refactoring centralizes the oneliner rendering
logic, making it more maintainable and consistent across different
views.
Additionally, the `OnelinerDisplay` enum is now used in
`CasePageTemplate` and `MyCasesTemplate` to represent the different
states of the oneliner, improving type safety and clarity.
Introduce `Generating` state and clarify `Empty` and `Missing` states.
The `Pending` state now specifically refers to transcription being in
progress.
The `Generating` state covers the period after transcription is complete
but before the LLM has produced the final oneliner state, or when a new
state needs to be generated due to new recordings.
The `Missing` state is collapsed into `Empty` as the UI treats them
identically when no title is expected.
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.
Adjusted layout for better readability and alignment. Increased heading
size and centered it. Updated input and button padding for consistency
and improved visual hierarchy. Added a subtle color and increased
font-size for labels.
Add `recorded_at_iso` and `time_hms_utc` to `CasePageTemplate` to
display the timestamp of the most recent recording.
Introduce a new partial `partials/time_format.js` for consistent date
and time formatting in the browser.
Update templates to use the new partial for rendering timestamps and
group headings.
Introduces a `resolve_return_path` function to determine the redirect
target after a case action. This function prioritizes the `Referer`
header for a more contextual redirect, falling back to the default case
list page if the header is absent or invalid.
Additionally, this commit refactors the UI elements for actions on the
case detail and case list pages, improving organization and clarity.
Introduces a new module `auto_trigger` responsible for opportunistically
initiating LLM analysis jobs. This mechanism scans case directories for
new or updated recordings and transcripts, automatically creating and
queuing analysis jobs when appropriate.
Key components:
- `evaluate_case`: Pure function to determine if a case is ready for
analysis based on recording status, transcriptions, document
modification times, and existing job/failure markers.
- `try_enqueue`: Orchestrates the analysis job creation and queuing
process if `evaluate_case` returns `AutoDecision::Enqueue`.
- `try_enqueue_all_for_user`: Iterates through all user cases to trigger
`try_enqueue` for eligible ones.
- `write_failure_marker`/`remove_failure_marker`: Handles persistent
recording of analysis failures and their cleanup.
This feature aims to reduce manual intervention by automatically
analyzing new or changed case data.
Introduces a system for broadcasting real-time events to the web UI.
This enables features like automatic page reloads when case data changes
in the background, improving the user experience by keeping the UI
synchronized with the backend.
Key components:
- `events` module: Contains the `CaseEventKind` enum, `CaseEvent`
struct, and `EventSender` type for managing the broadcast channel.
- SSE endpoint (`/web/events`): Streams events to connected browsers.
- Client-side JavaScript: Listens for events and triggers debounced page
reloads.
- Integration points: Workers and route handlers now emit events when
relevant state changes occur.
This commit enables serving audio files via HTTP Range requests. This is
crucial for allowing HTML5 audio players to seek to specific positions
within an audio file without re-downloading the entire file.
The changes include:
- Modifying `handle_audio` in `server/src/routes/web.rs` to parse
`Range` headers.
- Implementing `serve_range` to handle partial content responses.
- Adding a `parse_range` helper function.
- Updating tests to verify range request functionality.
- Adding `ACCEPT_RANGES: bytes` header to indicate support for range
requests.
- Storing recording duration in a sidecar file for faster UI rendering.
- Enhancing the HTML template to support a custom audio player with
seeking.
Introduces a toggle in the web UI that allows administrators to switch
between a standard view and an "admin view". This admin view exposes
additional administrative elements that are hidden in the standard view.
The toggle state is persisted in local storage, allowing the user's
preference to be remembered across sessions. Non-administrator users
will not see the toggle.
feat: Add admin view toggle to web UI
Introduce a client-side toggle for an "admin view" in the web UI. This
allows administrators
to selectively hide or show elements intended only for administrative
purposes. The toggle
state is persisted in local storage for a persistent user experience.
The implementation involves:
- Adding a checkbox element to the header of the case pages and my cases
list.
- Using CSS to conditionally hide elements with the `admin-only` class
when the admin view is off.
- Implementing JavaScript to manage the toggle's state, update local
storage, and apply the
`admin-view-off` class to the `<html>` element.
- Updating relevant templates (`case_page.html`, `case_recordings.html`,
`my_cases.html`)
to include the toggle and the `admin-only` class where appropriate.
- Adding unit tests to verify the visibility of the toggle for admins
and non-admins.
Passes an `is_admin` flag to the `CaseRecordingsTemplate` and
`case_page.html`.
This flag determines whether the full case ID is displayed in the meta
section
for administrative users. It also influences the header display on the
case page
to prioritize the oneliner when available.
This commit restructures the web interface for case details. The
previous `/web/cases/{case_id}` route, which previously showed both the
case summary and its recordings, has been split into two distinct pages:
- `/web/cases/{case_id}`: This page now displays the overall case
information, including the document if available.
- `/web/cases/{case_id}/recordings`: This new page is dedicated to
listing and displaying individual recordings within a case.
This change improves the organization and clarity of the web UI,
allowing for more focused views of case data. Additionally, the
`case_detail.html` and `document.html` templates have been removed as
their functionality is now handled by the new `case_page.html` and the
upcoming document rendering logic. The `cases.html` template has also
been removed, indicating a shift towards more granular page views.
This commit separates the case detail page into two distinct routes:
`/web/cases/{case_id}` for the main case information and document, and
`/web/cases/{case_id}/recordings` for a dedicated view of audio files
and their transcripts.
This change improves the organization and clarity of the case viewing
experience by segmenting the related but distinct information into their
own UI sections.
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.
This commit introduces a new module `analyze::render` to handle the
rendering of Markdown content produced by the LLM into safe HTML.
The LLM output now includes a custom `==text==` highlighting convention
to indicate sections that require doctor review due to potential
transcription errors or incomplete information. This highlighting is
converted into `<mark>` tags in the final HTML.
To ensure security, all LLM-generated Markdown is first HTML-escaped.
This prevents any malicious HTML or script injection from being executed
in the browser. Only the custom `<mark>` tags are preserved as
functional HTML elements.
The process is as follows:
1. The raw Markdown from the LLM is processed.
2. All HTML special characters (`<`, `>`, `&`, `"`, `'`) are escaped.
3. The `==text==` highlights are replaced with `<mark>text</mark>`.
4. The resulting string is parsed as Markdown by `pulldown-cmark`.
5. The parsed Markdown is converted to HTML, which is then safe to
inject into the Askama template using the `|safe` filter.
The `Cargo.toml` and `Cargo.lock` files have been updated to include the
`pulldown-cmark` dependency. The `document.html` template has been
modified to use a `div` with the class `doc-content` instead of a `pre`
tag, allowing the rendered HTML to be displayed correctly. The
`handle_document_view` function now calls the new `md_to_html` rendering
function.
Introduce a new "reset" action for bulk operations and a dedicated
endpoint for individual case resets. These features allow administrators
to revert a case to its raw audio state by deleting derived artifacts
like transcripts, analysis input, and documents. The functionality also
includes renaming `.m4a.failed` files back to `.m4a` to re-trigger
transcription.
This commit also:
- Adds a `reset_case_artefacts` helper function to `case_actions.rs`.
- Implements the `handle_reset_case` endpoint.
- Adds the "Reset" button to the UI for administrators.
- Includes unit and integration tests to verify the new functionality
and access control.
The versioning of analysis input and document files
(`analysis_input_v{N}.json`, `document_v{N}.md`) has been removed. All
analysis inputs will now use `analysis_input.json` and generated
documents will use `document.md`.
This simplifies file management, as there's no longer a need to track
and manage multiple versions of these files within a case directory. The
analysis worker will now overwrite the existing `document.md` if it
exists, ensuring that the latest analysis result is always present. The
`version` field has also been removed from `AnalyzeJob` and
`AnalysisInput`.
The case listing on the "My Cases" page is now grouped by local date,
with labels for "Heute", "Gestern", or the ISO date. This improves
readability and organization.
Additionally, the handling of silent recordings has been refined.
Previously, the absence of usable recordings would result in an error.
Now, the analysis worker gracefully handles this by writing a stub
document and skipping the LLM call. This prevents unnecessary errors and
provides a clearer status for silent cases.
The `dictate.sh` script has been updated to simplify the case directory
lookup logic. Instead of iterating through `open` and `done`
subdirectories, it now directly checks for the case ID and ensures it's
not marked as deleted. This simplifies the script and improves
efficiency.
The `server/Cargo.toml` and `server/Cargo.lock` have been updated to
include the `num_threads` dependency and enable additional features for
the `time` crate, which are necessary for proper local time zone
handling.
The `WorkerBusy` type was previously a single `Arc<AtomicBool>` shared
between the analyze and transcribe workers. This commit refactors this
to:
- Introduce `AnalyzeBusy` and `TranscribeBusy` newtype wrappers around
`WorkerBusy` to distinguish between the two flags. This allows
`axum::extract::State` to target them individually.
- Move the `BusyGuard` RAII guard into `src/lib.rs` and make it generic
to work with any `WorkerBusy` instance.
- Update the `main.rs`, `worker.rs`, and `routes/user_web.rs` files to
use the new types and guards, ensuring correct state management for
both pipelines.
- Enhance the transcription recovery scan
(`transcribe::recovery::scan_and_enqueue`) to iterate over user
directories and call `enqueue_pending_for_user` for each, making it
more robust and aligned with the per-user self-healing mechanism.
- Add a check for `transcribe_busy` in the `case_detail.html` template
to correctly display "Transkription läuft…" only when the transcribe
worker is actually active.
The analyze recovery scan has been refactored to iterate through user
directories and enqueue pending analysis jobs more efficiently. The
`WorkerBusy` status has been introduced as an `Arc<AtomicBool>` to track
whether the analyze worker is currently processing a job.
This `WorkerBusy` flag is used in the `analyze::worker::run` function
and managed by a `BusyGuard` RAII struct, ensuring the flag is correctly
set and unset even in case of panics.
The `handle_my_cases` and `handle_case_detail` handlers in `user_web.rs`
now utilize the `WorkerBusy` flag to accurately display the "analyzing"
status and to trigger a self-heal of orphaned analysis inputs when the
worker is idle.
Additionally, the `WorkerBusy` type is now exported from
`server/src/lib.rs` to be accessible by other modules. The test cases
have been updated to include the `WorkerBusy` parameter when spawning
the analyze worker.
Introduce a "Back to Cases" button in the document view and update the
case detail page to conditionally display the "Analyze" button. Also,
add a copy-to-clipboard functionality for the document content.
Introduces a new `/web/cases/bulk` endpoint to handle multiple case
actions simultaneously.
This change adds the following:
- A new `bulk` module for handling bulk operations.
- The `handle_bulk_action` function in `bulk.rs` to dispatch to specific
actions.
- `bulk_analyze` and `bulk_delete` functions to process the actions.
- Updates `Cargo.toml` and `Cargo.lock` to include necessary
dependencies: `form_urlencoded`, `serde_core`, `serde_html_form`, and
`serde_path_to_error`.
- Modified `axum-extra` features to include `form`.
- Adds checks for deleted cases in `collect_pending` for both analysis
and transcription recovery.
- Renames and modifies `handle_close_case` to `handle_analyze_case` in
`case_actions.rs` to better reflect its functionality.
- Adds a new `handle_delete_case` and `handle_undo_delete` to
`case_actions.rs`.
- Updates `my_cases.html` and `case_detail.html` to support bulk
actions, including checkboxes, a bulk action bar, and an "Undo last
delete" feature.
- Modifies `handle_audio` and `scan_cases` in `web.rs` to respect delete
markers.
- Updates `analyze_test.rs` with new request builders for analyze and
delete actions.
The distinction between `open/` and `done/` directories for cases has
been removed.
All cases for a user now reside directly under the user's directory
(e.g., `<data_path>/<slug>/<case_id>/`).
This simplifies path management and eliminates redundant directory
traversals.
Key changes include:
- Removed `open/` and `done/` subdirectories in path resolution.
- Introduced a `paths::case_dir` function as a single source of truth
for case directory layout.
- Updated various modules (`recovery`, `auth`, `routes`, `transcribe`,
`tests`) to use the new path structure.
- Adjusted templates to reflect the simplified case status
representation.
Introduce a new endpoint for reanalyzing cases. This feature allows
administrators
to rebuild analysis input files and enqueue new analysis jobs. The
system will
then generate a new document version, which will become the displayed
document.
Introduce a `DocumentTemplate` struct for rendering the document view,
replacing inline HTML.
Introduce a `CaseFlags` struct to encapsulate the logic for determining
various UI states of a case (e.g., `has_document`, `analyzing`,
`can_close`, `llm_missing`).
Update `handle_case_detail` to use `compute_flags` for populating the
`CaseDetailTemplate`.
Update `scan_user_cases` to compute `analyzing` and `has_document` flags
for the `UserCaseView`.
Add new template logic in `case_detail.html` to display actions based on
`CaseFlags`.
Add new template logic in `my_cases.html` to display `analyzing` and
`done-doc` labels for cases.
Create a new `document.html` template.
Add a helper function `any_document_exists` to check for the presence of
a document file.
This commit introduces the foundation for web-based authentication and
user interface. It includes:
- **Session Management**: Securely handling user sessions using
cryptographically generated tokens, cookies with appropriate security
flags, and server-side storage.
- **Login/Logout Functionality**: Endpoints for users to log in with
their credentials and log out, clearing their session.
- **User Interface**: Basic templates for the login page, a list of
cases, and a detailed view of a single case, allowing users to
navigate and view their data.
- **Authorization**: An `AuthenticatedWebUser` extractor to ensure only
logged-in users can access protected web routes.
- **Configuration**: Added a `cookie_secure` option to the configuration
to control the `Secure` flag on session cookies, useful for local
development.
- **Dependencies**: Added necessary dependencies for password hashing,
time handling, and TOML file manipulation.
- **Helper Binary**: Included a `hash-password` binary to simplify
generating bcrypt hashes for user passwords.
Add a new `failed` field to `RecordingView` and corresponding logic in
the worker.
When FFmpeg remuxing or Whisper transcription fails, the worker now
renames the
original `.m4a` file to `.m4a.failed`. This prevents the recovery scan
from
reprocessing these files and signals to the UI that transcription
failed.
The UI displays a message indicating failure and suggests renaming the
file
back to `.m4a` to retry. A new integration test
`worker_renames_audio_to_failed_on_whisper_error`
is included to verify this behavior.
Introduce an `oneliner` field to the `CaseView` struct and update the
`scan_cases` function to read it from `oneliner.txt`. The `cases.html`
template is updated to display the oneliner if present. A new
integration test is added to verify the functionality.
Introduces a dedicated function `validate_and_index_users` to
encapsulate the logic for validating user configurations, checking for
duplicate slugs and API keys. This function returns a `Result` to handle
errors gracefully, and its usage in `load_users` is updated to panic
with a more informative error message.
Additionally, this commit modifies the web routing to fetch and display
audio transcripts. When scanning recordings, it now attempts to read a
corresponding `.transcript.txt` file. If found, the transcript content
is included in the `RecordingView` and rendered in the `cases.html`
template. If the transcript file is not found, a "Transcription pending"
message is displayed.
The `transcribe` function in `whisper.rs` is updated to explicitly set
the language to German (`language=de`), which can improve transcription
accuracy by skipping language detection. The `encode=false` query
parameter has been removed, as the service is designed to handle encoded
audio.
Finally, tests have been added for the new `validate_and_index_users`
function to ensure duplicate slugs and API keys are correctly rejected.
Integration tests in `web_test.rs` have been updated to verify the
rendering of transcripts and the pending status.
Introduces a new web interface to list cases and play back audio
recordings. This includes:
- A new `web` module in `routes` to handle web requests.
- `handle_case_list` to scan and display available cases and their
recordings.
- `handle_audio` to serve audio files, with validation to prevent path
traversal and ensure correct file types.
- `AppError::NotFound` variant to handle missing resources gracefully.
- Updates to `projektplan.md` to document the change in STT container
and m4a preprocessing.
- New tests in `server/tests/web_test.rs` to ensure the web interface
functions correctly.