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.
This commit introduces a new magic link authentication flow.
The desktop client can now request a temporary, one-time-use token from
the server.
This token is then used to open a URL in the system browser, which
redirects to the server.
The server consumes the token, installs a regular web session, and
redirects the user
Replace the imperative loop that mixed mtime-tracking, transcript
reading, and validation with three clearly-scoped functions:
- compute_last_mtime: pure reduction over all m4as, returns max mtime.
- read_recordings: I/O loop that reads each transcript sidecar,
aborts on missing transcript (legacy "Nicht alle Aufnahmen sind
transkribiert" 400 body), silently skips blank ones.
- build_analysis_input: orchestrator that composes the above with
RFC3339 formatting.
Error bodies are preserved byte-for-byte. Replaces a one-pass loop
with two passes; n <= ~10 in practice - negligible.
The 73-line function mixed three concerns: directory traversal,
UUID/deletion filtering, and view assembly. Split into:
- scan_user_cases: orchestrator (loop + sort).
- filter_valid_case_dir: gate that returns (case_id, case_path) for
every valid case directory, skipping non-UUID names, files, and
soft-deleted cases.
- compute_case_view: view assembly that returns None for empty
cases (no recordings yet).
Each helper is now individually inspectable; nesting depth drops
from 3-4 to 2 levels. Let-else guards replace nested matches.
Group the four worker handles (analyze/transcribe x busy/tx) into a
new PipelineState struct with a FromRef<AppState> impl. Convert
heal_orphans_if_idle from a free function with 6 parameters into a
method on PipelineState. The two web handlers (handle_my_cases,
handle_case_detail) now take one State<PipelineState> instead of
four separate State extractors.
The original FromRef impls for AnalyzeBusy/Sender and TranscribeBusy/
Sender remain intact so handlers that need only one of them keep
their narrower signature.
Consolidate 5 instances of "locate_case + warn! + 404-return"
boilerplate (case_actions.rs x4, user_web.rs x1) into a single
pub(crate) helper in user_web.rs. The helper takes &CaseId (no
String allocation at call sites) plus a context tag that becomes
a structured-log field instead of being baked into the message.
bulk.rs keeps using the underlying locate_case because its miss
semantics differ (silent skip, not 404).
Replace five duplicated uuid::Uuid::parse_str calls in web handlers
(analyze, document view, delete, reset, case detail) with a
CaseIdPath extractor that validates the URL segment once and
returns the legacy "Invalid case_id" 400 body byte-for-byte.
The extractor uses axum's FromRequestParts and maps any failure
to AppError::BadRequest, so IntoResponse handling is unchanged.
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 `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.
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.
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.
This commit introduces a new API endpoint `/api/oneliners` that allows
authenticated users to retrieve a list of their recent oneliners.
The endpoint supports conditional GET requests using the `ETag` header
for efficient caching. It also allows specifying a time window for the
oneliners via the `hours` query parameter, with sensible defaults and
clamping.
The implementation includes:
- A new route handler `handle_oneliners`.
- A new type `OnelinerWatermark` for tracking user-specific timestamps.
- Logic for scanning user data directories, filtering by time, and
handling deleted cases.
- Test cases to cover various scenarios, including caching, time
windowing, and edge cases.
Moves shared types and utility functions to a new `doctate-common`
crate.
This includes:
- API key header constant
- AckResponse and AckStatus types
- Timestamp formatting and parsing utilities
This change centralizes common functionality, reducing duplication and
improving maintainability.
The `server` crate now depends on `doctate-common`.
This commit updates various dependencies to their latest versions,
ensuring better compatibility and security. It also corrects the
`.gitignore` file to properly exclude the `target/` directory instead of
`server/target/`.
Integrates the `spellbook` crate to allow for a Hunspell dictionary to
be used as a veto mechanism within the gazetteer.
This prevents common German words from being incorrectly rewritten to
vocabulary entries if they fall within the edit distance threshold. For
example, "Kaktus" will no longer be rewritten to "Lantus" if a Hunspell
dictionary is provided and contains "Kaktus".
The `hunspell_dict_path` configuration option is added, defaulting to
`/usr/share/hunspell/de_DE`. This path should point to the stem of the
Hunspell dictionary files (e.g., `/usr/share/hunspell/de_DE.aff` and
`/usr/share/hunspell/de_DE.dic`).
The gazetteer now supports an optional `DictChecker` trait, allowing for
pluggable dictionary implementations. `SpellbookDict` is the initial
implementation using the `spellbook` crate.
Includes new tests to verify the dict veto functionality, including
handling of case sensitivity and exact matches. An ignored test is added
for live verification against system-installed Hunspell dictionaries.
The system prompt for the medical assistant LLM has been updated to
improve clarity and explicitly state the desired formatting for
corrections and uncertainties. This includes:
- Consolidating similar correction examples.
- Specifying that only "==text==" annotations are allowed for
corrections and uncertainties.
- Explicitly disallowing other annotation formats like "(unsicher)" or
"[TODO]".
Avoid redundant LLM calls by waiting until all recordings in a batch
have been processed before regenerating the oneliner. This change
introduces a helper function `has_pending_recordings` to check for any
`.m4a` files without a corresponding transcript, and the
`update_oneliner` logic now only proceeds if no pending recordings are
found.
This commit integrates the Gazetteer into the transcription pipeline to
normalize terminology.
The Gazetteer, loaded at startup, is now passed to the transcription
worker. Before persisting the transcription text, it is processed
through the Gazetteer's `replace` method, ensuring standardized
terminology. This normalization is also applied to the generated
"oneliner" text.
Additionally, the `regenerate_missing_oneliners` function in the
recovery module has been updated to accept and utilize the Gazetteer,
ensuring that regenerated oneliners also benefit from terminology
normalization.
A new integration test, `transcribe_worker_normalizes_whisper_output`,
has been added to verify that the Whisper output is correctly normalized
by the Gazetteer before being saved to the transcript file.
The gazetteer has been refactored to act as a post-AI normalization
filter. Previously, it was used to annotate LLM input with potential
corrections from a vocabulary. This approach was ineffective because
LLMs often override such hints.
The new approach applies the gazetteer *after* the LLM has generated its
output. This allows for deterministic correction of known terminology,
including fixing LLM output drift (e.g., anglicized drug names).
Key changes:
- `annotate` function renamed to `replace`.
- The output format changes from `Canonical [?original]` to simply the
`Canonical` form.
- The gazetteer now operates on the final LLM output before persistence,
ensuring consistency.
- Tests have been updated to reflect this new behavior, focusing on the
final output rather than the LLM request payload.
The gazetteer now annotates text with `Canonical [?original]`. This
prioritizes the corrected term for the LLM while keeping the original
transcription as a fallback.
This change aligns the gazetteer's annotation strategy with the LLM's
prompt, which expects corrections to be marked for review. Previously,
the format was `original [?Canonical]`, which could lead to the LLM
using the potentially incorrect original term.
The build scripts for generating `anatomy.txt` and related files have
been removed as they are no longer used or maintained. The Kölner
Phonetik module was also removed as it was not being utilized.
The `filter-hunspell-de.sh` script now uses a `MIN_LEN` variable that is
synchronized with the `MIN_TOKEN_LEN` constant in
`src/gazetteer/mod.rs`.
This ensures that short tokens, which are prone to false positives due
to
collisions with common German words, are filtered out by both the
gazetteer
loading and the Hunspell dictionary processing.
Additionally, the script's dependency instructions have been updated to
be
more comprehensive, listing package manager commands for Arch/CachyOS,
Debian/Ubuntu, and Fedora. The `awk` command has been introduced to
filter
out tokens shorter than `MIN_LEN` before sorting and deduplicating.
The `Gazetteer::insert` method has been updated to skip entries shorter
than `MIN_TOKEN_LEN` to prevent noisy phonetic collisions. The
`best_candidate`
helper function has been extracted to improve readability and structure.
The test cases have been updated to reflect the change in
`MIN_TOKEN_LEN`
from 4 to 5 and to use more relevant examples for the updated
functionality,
such as "Cerebrum" and "Zerebrum". The `anatomy.txt` file has been
updated
to reflect that anatomical terms are currently disabled. The
`medications.txt` file has been significantly expanded with a curated
list
of German medication brand and active ingredient names.
Adds three new vocabulary files to the server's vocabulary directory:
`anatomy.txt`, `medications.txt`, and `substances.txt`. A README.md is
also added to explain their purpose, format, and population strategy.
The `server/vocab/raw/` directory is added to `.gitignore`.
This commit introduces functionality to annotate the user prompt with
potential proper name corrections from a gazetteer. The LLM will then
use these annotations to improve the accuracy of transcriptions,
especially for medical terms and names.
The `SYSTEM_PROMPT` has also been updated to inform the LLM about these
new annotations and how to handle them.
Pass a `Gazetteer` to the analyze worker to enable proper-name
correction. The gazetteer is loaded from disk at application startup. If
the directory is missing or unreadable, the worker will start without
proper-name correction capabilities.
This commit introduces the `VOCAB_DIR` configuration option, which
specifies the directory for gazetteer files. It also adds the `strsim`
crate, which is used for calculating string similarity and is essential
for the gazetteer's functionality.
The gazetteer module has been significantly expanded to include the
`Gazetteer` struct, its loading mechanism from `.txt` files, and an
annotation function. This functionality allows for pre-LLM correction of
proper names and technical vocabulary by identifying potential
misspellings based on phonetic similarity and edit distance.
Implements the Kölner Phonetik algorithm for German word sound-encoding.
This phonetic algorithm is used to find gazetteer entries that sound
similar to a potentially misspelled token, enabling correction
suggestions.
The implementation includes:
- `encode`: The main function that orchestrates the encoding process.
- `prepare`: Prepares the input string by expanding umlauts,
uppercasing, and filtering non-alphabetic characters.
- `raw_encode`: Performs the character-by-character phonetic encoding
with context-aware rules.
- `postprocess`: Cleans up the raw output by collapsing duplicate digits
and removing leading zeros.
Includes unit tests to verify the correctness of the encoding for
various German words and edge cases, ensuring phonetic similarity and
handling of special character combinations.
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.
The system prompt for the transcription Ollama LLM has been updated to
improve robustness and clarity, especially for handling empty or
non-medical transcripts.
The prompt has been refactored to:
- Use English for instructions to improve robustness with smaller LLMs.
- Clearly define the behavior for empty or non-medical transcripts,
requiring an empty string response.
- Reduce the maximum character limit from 120 to 60 to better fit
smartwatch displays.
- Ensure the output is German, adhering to the original language
requirement for the output.
- Add an explicit instruction to correct transcription errors in the
input dictation for the consolidation LLM.
This commit introduces a recovery mechanism to regenerate `oneliner.txt`
files for cases where transcription was successful but the oneliner
generation failed due to a crash between writing the transcript and the
oneliner.
The `transcribe::recovery::regenerate_missing_oneliners` function is
added and called at startup after `scan_and_enqueue`. This function
identifies cases with non-empty transcripts but missing oneliners and
triggers their regeneration.
The `transcribe::worker::update_oneliner` function is refactored to
always regenerate the oneliner from all available transcripts in a case,
ensuring that later recordings can correct earlier ones. This replaces
the previous `ensure_oneliner` logic which only generated the oneliner
if it didn't exist.
New helper functions `cases_needing_oneliner` and
`all_transcripts_joined` are introduced to support these changes.
Comprehensive unit tests are included for the new recovery and file
processing logic.
The LLM client now conditionally adds the `Authorization` header only
when an API key is provided. The `llm_configured` check is updated to
reflect that an API key is not strictly required for Ollama-style
endpoints. A new integration test verifies the functionality against an
Ollama-compatible endpoint without an API key.
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 system prompt has been updated to include an explicit instruction
for the LLM to return an empty string if no suitable keyword is found.
This improves robustness by handling cases where the input transcript
might not contain easily extractable medical terms.
Additionally, the `normalize` function's logic for finding the first
non-empty line has been slightly refined to use a more idiomatic Rust
approach by chaining `.lines().find(...)` and `.unwrap_or("")`. This
change improves code clarity and maintainability without altering the
functional behavior of finding the first non-empty line.
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.