Commit Graph

282 Commits

Author SHA1 Message Date
Brummel 4dc15c5450 fix(analyze): drop transcript timestamp headings from LLM prompt
render_prompt prefixed each recording with a '## <recorded_at>' heading.
No system prompt consumes that timestamp, and the LLM occasionally echoed
it into the final document as a heading. Join recordings with the bare
'\n\n---\n\n' separator instead; chronology is carried by recording order.

Adds a regression test pinning that no RFC3339 timestamp or '##' reaches
the prompt.

closes #8
2026-05-30 14:42:48 +02:00
Brummel d34b2645ee docs(wear): point profile template at https://app.doctate.de
App clients reach minerva only over HTTPS now: the Android cleartext
policy blocks plain HTTP to minerva.lan, and the public Let's Encrypt
cert on app.doctate.de chains to the system trust store, so no
network-security-config change is needed. Update the profile template's
SERVER_URL and comment to reflect the HTTPS convention.

closes #7
2026-05-30 14:11:14 +02:00
Brummel 11de27d435 feat(wear): key watch allow-list on ro.serialno, not adb transport serial
The ALLOWED_SERIALS guard matched the adb transport serial, which over
WiFi is a rotating ip:port (changes on every wireless-debugging restart)
and over USB is the bare hardware serial. Pinning the transport serial
meant every port rotation broke the deploy until the profile was hand-edited.

Resolve the device's stable ro.serialno (getprop) after target resolution
and match that against ALLOWED_SERIALS instead. The transport serial still
drives adb/gradle/ANDROID_SERIAL; only the wrong-watch guard switches to the
stable identity, so it survives port rotation and is identical over WiFi/USB.

Also: 'devices' now prints 'transport -> ro.serialno' per device to make the
value for ALLOWED_SERIALS easy to read off. Help text and the example profile
are updated to the ro.serialno form.

closes #6
2026-05-30 12:57:42 +02:00
Brummel 8a0134c7e8 feat(wear): add DevBadge visible only in dev-profile builds
Small red 'DEV' tag in the top-right of CaseListScreen and RecordingScreen
when BuildConfig.IS_DEV_PROFILE is true. Production profiles render
nothing — Krey's UI stays clean.

refs #3
2026-05-20 18:59:15 +02:00
Brummel be056331f7 feat(wear): use appLabel placeholder for launcher label
Dev profiles (name ends in -dev) get 'Doctate (Dev)' in the launcher;
production profiles keep the plain 'Doctate'. The string resource
@string/app_name is no longer the application-level label, only used
for the complication entry where the launcher distinction is irrelevant.

refs #3
2026-05-20 18:57:49 +02:00
Brummel cd5c1a9587 feat(wear): expose PROFILE_NAME and IS_DEV_PROFILE to the app
BuildConfig now carries the active profile name plus an isDev boolean
(profile name ends in '-dev'). The manifestPlaceholder 'appLabel'
switches between 'Doctate' and 'Doctate (Dev)' for the launcher.

refs #3
2026-05-20 18:56:46 +02:00
Brummel 78868f86af refactor(wear): drop local.properties fallback for doctate.* keys
Profile file (via -Pdoctate.* injected by run.sh) is now the only source.
local.properties stays gitignored but is no longer consulted by the build.

refs #3
2026-05-20 18:55:40 +02:00
Brummel 4d9b6126fb docs(wear): rewrite run.sh help-text for profile-based builds
Drop DOCTATE_DEV_SERVER, DOCTATE_API_KEY, and DEFAULT_DEV_SERVER — the
profile file is the only source for URL + key now.

refs #3
2026-05-20 18:54:33 +02:00
Brummel 165c83fd1f feat(wear): wire watch profile into apply_target + argument parser
./run.sh watch <profile> <subcmd> [args]

The second positional after \`watch\` is now the profile name. apply_target
calls load_profile + verify_serial_allowed before Gradle, so a wrong-watch
install refuses fast without burning a build. DOCTATE_API_KEY pass-through
is gone — keys come from the profile file only.

refs #3
2026-05-20 18:53:38 +02:00
Brummel ce26b9c100 feat(wear): add load_profile + verify_serial_allowed helpers
Helpers source profiles.d/<name>.sh and assert SERVER_URL, API_KEY, and
ALLOWED_SERIALS. verify_serial_allowed dies with an explicit cross-watch
warning when the attached serial is not in the profile's allow-list. Not
yet wired into apply_target — that follows in the next commit.

refs #3
2026-05-20 18:52:05 +02:00
Brummel 740314a7ad feat(wear): add profiles.d/ scaffolding for multi-profile builds
Adds the directory, gitignore rule, and checked-in template. Real profile
files (e.g. brummel-dev.sh) live alongside but are gitignored as they
contain plaintext api_keys.

refs #3
2026-05-20 18:50:51 +02:00
Brummel 0de96081c8 Add implementation plan for watch multi-profile build mechanism
Eleven tasks: profiles.d/ scaffolding + run.sh helpers + dispatch rewire,
BuildConfig PROFILE_NAME / IS_DEV_PROFILE + manifest appLabel placeholder,
optional DevBadge composable, and a full smoke-test matrix. External
prereqs V1-V3 (minerva users.toml plaintext keys, Krey user, ADB serials)
called out before the code path.

refs #3
2026-05-20 18:39:39 +02:00
Brummel 430bc2f371 Add spec for watch multi-profile build mechanism
Three named profiles (brummel-dev, brummel-minerva, krey-minerva) with
hard-locked watch-serial whitelist in run.sh, to enable safe parallel
testing on Brummel's and Krey's watches against dev and minerva servers.

refs #3
2026-05-20 18:32:01 +02:00
Brummel 8a1f032d8b Drop redundant German-chat directive from CLAUDE.md 2026-05-20 17:03:34 +02:00
Brummel 76bf65be1a Refactor: Track analysis jobs via InFlight struct
This commit replaces the disk-based `analysis_input.json` marker for
in-progress analysis jobs with an in-memory
`Arc<RwLock<HashSet<PathBuf>>>`.

Previously, the existence of `analysis_input.json` was used as a signal
that a case was queued or being analyzed. This led to stale "Queued"
states after server restarts, as the disk file would persist while the
server process tracking it had vanished.

The new `InFlight` struct, held in `AppState`, provides a process-local
marker. This ensures that:

- Server restarts correctly reset the state, as the `HashSet` is
  cleared.
- Race conditions for triggering analysis are handled by the `try_claim`
  method, replacing the previous reliance on file system `O_EXCL` flags.
- The `AnalyzeJob` payload now travels with the job over the channel,
  eliminating the need for workers to re-read `analysis_input.json` from
  disk.

This change simplifies state management and improves the reliability of
analysis job tracking.
2026-05-05 20:01:07 +02:00
Brummel e0cfd5d512 Refactor: Simplify case status display
Introduce a `Badge` enum and a `badge_for` function to consolidate
case status logic. This replaces multiple boolean flags (`analyzing`,
`has_failed_recording`, `analysis_required`) with a single, more
expressive `badge` field.

The HTML template has been updated to use this new badge system,
removing old status classes and logic. The `scan_user_cases` function
no longer needs the `worker_busy` argument as the analysis state is
now derived directly from the `auto_trigger` state.

This change improves code clarity and maintainability by centralizing
status determination and reducing redundant boolean flags.
2026-05-05 16:41:30 +02:00
Brummel 16ef0bbe78 Introduce boot_time to evaluate_state
The `boot_time` parameter is introduced to `evaluate_state` and its
callers. This parameter acts as a lower bound for the "seen at"
timestamp used in determining the `Idle` state for cases.

Previously, the `elapsed` duration was calculated as
`now.duration_since(latest_mtime)`. This meant that a server restart
could immediately cause old recordings to be classified as `Idle` if
their modification times were significantly in the past, potentially
triggering a large number of LLM calls upon the first render of the
`/cases` page.

By using `seen_at = std::cmp::max(latest_mtime, boot_time)`, the idle
calculation now ensures that a fresh server start respects a grace
period. Old recordings will not satisfy the idle threshold until the
elapsed time since the server's boot (or the recording's modification
time, whichever is later) exceeds the `idle_threshold`.

`SystemTime::UNIX_EPOCH` is used as the default `boot_time` in tests and
in `create_router_and_session_store_with_settings`. This preserves
existing test behavior where the idle grace period is effectively
disabled. For production, `main.rs` now captures the server's actual
boot time and passes it down.

This change also refactors `evaluate_case` and `try_enqueue` to accept
the new `boot_time` parameter. `try_enqueue_all_for_user` is updated to
pass `boot_time` along. `boot_scan_state` and `compute_state_for_user`
in `recovery.rs` also receive `boot_time`.

Additionally, a new test case
`analyze_required_endpoint_enqueues_only_required_cases` is added to
verify the behavior of the `/cases/analyze-required` endpoint. This
endpoint now correctly enqueues only `Required`
2026-05-05 15:12:08 +02:00
Brummel bd133e9944 Make write_input_overwrite public
This commit makes the `write_input_overwrite` function public. This is
necessary because the new `handle_analyze_required` function in
`bulk.rs` needs to call it. Previously, it was only used internally by
`auto_trigger.rs`.
2026-05-05 14:38:30 +02:00
Brummel 58b36fbcd9 Add stale banner and required analysis UI
Introduces a visual indicator for stale analysis on the case page and a
new section on the "My Cases" page to highlight cases requiring
analysis. This improves user awareness of data currency and pending
actions.
2026-05-05 14:34:37 +02:00
Brummel c5798eb877 Refactor auto-analysis to use CaseAnalysisState
The `evaluate_case` function has been replaced with `evaluate_state` in
the `auto_trigger` module. This change introduces a new enum,
`CaseAnalysisState`, which provides a more granular representation of a
case's analysis status.

The `try_enqueue` and `try_enqueue_all_for_user` functions now utilize
`evaluate_state` to determine if analysis should proceed. This ensures
that analysis is only triggered for cases in the `Idle` or `Required`
states.

Additionally, the `CaseFlags` struct has been updated to include
`analysis_stale_with_doc`, and corresponding logic has been added in
`compute_flags` and `compute_case_view` to accurately reflect the
analysis status for UI display. The `UserCaseView` and `MyCasesTemplate`
structs have also been updated to incorporate these new fields.
2026-05-05 14:31:29 +02:00
Brummel 0660727faf Refactor auto-trigger and recovery logic
This commit introduces a significant refactor to the auto-trigger and
recovery mechanisms for the analysis pipeline. The core change is the
introduction of a more granular `CaseAnalysisState` enum, replacing the
simpler `AutoDecision`.

The `evaluate_state` function now computes the full lifecycle state of a
case, considering factors like the presence of recordings, pending
transcripts, queued jobs, failure markers, document staleness, and an
idle threshold. This provides a richer understanding of the case's
status.

The `evaluate_case` function is retained as a backwards-compatible
adapter for existing callsites and tests. It maps the new
`CaseAnalysisState` to the old `AutoDecision` enum, simplifying the
decision to "enqueue" or "skip."

The `recovery` module is also refactored. The `scan_and_enqueue`
function is replaced by `boot_scan_state`, which is now purely
observational. It aggregates statistics about cases in different states
at boot time and logs them, but it no longer attempts to re-enqueue
jobs. The responsibility for triggering analysis now lies with the
per-render `try_enqueue_all_for_user` logic, which utilizes the new
typed state machine.

This refactoring aims to provide more clarity and control over the
analysis pipeline's state management, particularly in handling cases
that have previously failed or are waiting for further input.
2026-05-05 14:26:05 +02:00
Brummel 9e8db7518d Refactor document handling to JSON envelope
The `document.md` file has been replaced by `document.json`. This new
format acts as a JSON envelope, containing the original markdown content
along with metadata such as `covered_mtime`, `written_at`, and
`backend_id`.

This change addresses a bug where new recordings arriving during an LLM
analysis call could be ignored. The `covered_mtime` field in the new
JSON envelope ensures that the document's metadata accurately reflects
the state of recordings at the time of analysis, preventing incorrect
"analysis current" states.

Additionally, the `analysis_input.json` file is now removed upon
successful analysis, and a failure marker is used to indicate analysis
failures. This ensures that a case is re-analyzed if necessary.

The `analysis_idle_threshold_secs` configuration option has been
introduced to control the delay before a case in the `Required` state is
automatically promoted to `Idle` and enqueued for LLM analysis.
2026-05-05 14:01:43 +02:00
Brummel c09a74cef3 Refactor system prompt for clarity 2026-05-05 11:12:19 +02:00
Brummel 44e93f08ee feat(analyze): structured LLM failure diagnostics + SSE reload on failure
Failure markers now carry finish_reason and usage tokens (prompt /
completion / reasoning) pulled from the chat-completions response, so
the case-page "Technische Details" block can show why an analysis
truncated — e.g. gpt-oss-120b hitting max_completion_tokens with the
chain-of-thought before producing any visible answer.

The worker also emits a new CaseEventKind::AnalysisFailed after writing
the marker, so subscribed browsers reload immediately instead of leaving
the page on the "Wird analysiert …" placeholder.
2026-05-05 11:01:29 +02:00
Brummel a142725a8a fix(desktop): keep recordings on hard upload errors, reap ghost markers
Mirror of the 2026-05-05 Wear-OS fix: port the canonical client sync
spec (project_client_sync_semantics.md) into the Rust desktop client.

Before this change, server_sync::run_loop had a `UploadOutcome::Terminal`
branch that deleted the m4a + sidecar pair on any non-2xx + non-5xx
response (401, 413, 415, parse drift, audio read failures) and never
called mark_synced — so the marker stayed `synced_to_server=false` as a
non-removable ghost in the desktop UI. Same shape as the Wear-OS bug.

Changes:
- `UploadOutcome` reduced to two variants (`Succeeded` | `Transient`).
  The Terminal arm in `run_loop` is gone; `delete_pair` only fires from
  the success path.
- `post_upload` now classifies every failure mode as `Transient`:
  4xx incl. 401, parse-ack drift on 200, file-read errors, multipart
  build errors, and network errors. Backoff retries forever.
- New `CaseStore::cleanup_orphaned_unsynced(pending_case_ids, cutoff)`:
  removes markers that are unsynced AND have no waiting upload pair AND
  are older than a small grace window. Self-heals pre-spec ghosts.
- `run_startup_cleanup` gains a third sweep that calls
  `cleanup_orphaned_unsynced` with `DEFAULT_ORPHAN_MARKER_GRACE = 5 min`,
  matching the Wear-OS StartupCleanup contract.
- Tests inverted/added: `post_upload_transient_on_401`,
  `post_upload_transient_on_2xx_unparseable_body`,
  `former_terminal_401_keeps_pair_and_retries`, four new
  `cleanup_orphaned_unsynced_*` cases, plus `sweeps_orphan_unsynced_marker`
  and `keeps_unsynced_marker_with_pending_pair` integration tests.

Followup (out of scope): `UploadEvent::Failed.will_retry` can now only
be `true` and is vestigial. Removing it touches every Failed-event
consumer in app.rs/UI; left for a separate cleanup commit.
2026-05-05 10:21:33 +02:00
Brummel a8994fd84d fix(wear): keep recordings on hard upload errors, reap ghost markers
Treat every non-Success upload response as Transient so audio survives
expired sessions, 4xx replies, and unparseable 2xx bodies — the watch
keeps retrying instead of silently dropping the recording. Drop the
UploadResult.Terminal variant entirely and the SyncWorkerLoop branch
that consumed it.

Also reap orphaned unsynced markers on app start: a syncedToServer=false
marker without a matching pending pair (audio gone) and older than a
5-minute grace window is inconsistent — the user couldn't discard it,
and no other path could clean it up. Eliminates the 2026-05-03 21:08
ghost-marker class.

Test fixture (InMemoryCaseStore), SyncWorkerLoopTest, UploadClientTest,
and StartupCleanupTest updated to reflect the new contract.
2026-05-05 09:52:29 +02:00
Brummel 2c6062a53e refactor: drop /web/ URL prefix from browser routes
The /web/ prefix predated the /api/ split; today it just clutters every URL
without disambiguating anything. All 16 browser routes move to the apex
(/cases, /login, /magic, /events, /audio/...). The 6 /api/* routes are
unchanged. A new /->>/cases redirect closes the apex 404.

The open-redirect guard in magic.rs and case_actions.rs flips from a
positive whitelist (starts_with("/web/")) to a deny-list: same-origin path,
not protocol-relative, not under /api/, no \. The /api/ exclusion is now
load-bearing and covered by tests.

Pre-production: no transition redirects.
2026-05-04 18:36:10 +02:00
Brummel 3d67cbc1c8 Add --force flag to deploy script
Introduces a `--force` or `-f` flag to the `deploy-server.sh` script,
allowing deployments even when the working tree is dirty. The script now
also checks for a dirty working tree and exits with an error unless
`--force` is provided.
2026-05-04 11:50:40 +02:00
Brummel 2fc7f8ddd2 feat: Add display name to user profiles
Introduce `display_name` field to `User` struct for human-readable names
in the UI.
This falls back to the user's `slug` if not provided.

Update `AuthenticatedWebUser` and `MyCasesTemplate` to use this new
field for rendering.
Add tests for parsing `display_name` and ensure fallback logic works
correctly.
Update `users.toml.example` to document the new field.
2026-05-04 11:46:19 +02:00
Brummel 03129ad592 chore: add restart-server.sh helper for fast minerva restarts
One-line wrapper around docker compose ... restart plus a /api/health
poll, useful after editing users.toml/settings.toml/.env on minerva
(boot-time read). docs/deployment.md updated (user-mgmt, troubleshooting,
TLS-proxy sections) to reference the wrapper instead of the inline
ssh+docker command.
2026-05-04 11:35:08 +02:00
Brummel cf80bc7e97 feat: containerize doctate-server for minerva deployment
Multi-stage Dockerfile, host-network compose, idempotent deploy script.
Configs and persistent data bind-mounted from /opt/stacks/doctate-server.
deploy-server.sh handles bootstrap + re-deploy in one command with
auto-prune (keep 5 last SHA-tagged images).

See docs/deployment.md for bootstrap, rollback, and TLS-proxy notes.
2026-05-04 11:14:31 +02:00
Brummel 88e469d0a1 feat: Add project integrity check script
This script automates checks for formatting, linting, building, and
testing across
all Cargo workspaces and the Wear OS Gradle build. It provides a summary
of all
stages, indicating PASS, WARN, or FAIL. The script's exit code reflects
the number
of failed stages.
2026-05-04 09:38:04 +02:00
Brummel ac98dc4ef9 Refactor config path module imports
Move `ProjectDirs` import to be consistent with other imports in the
module and avoid potential ordering issues.
2026-05-04 09:37:59 +02:00
Brummel 012e6732b3 Refactor LLM interaction to use backend catalog
Introduces a new `LlmBackend` struct to centralize LLM configuration.
This change refactors the `call_llm` function in both `run_full_case.rs`
and `run_llm_only.rs` to accept an `LlmBackend` instance instead of
individual settings.

The `doctate_server::analyze::backend` module is now used to obtain the
default backend, simplifying configuration and improving consistency.
The `clone_backend_with_system_prompt` helper function is introduced to
allow temporary modification of LLM backends for experimentation without
altering the production catalog.

This change improves code organization and makes it easier to manage LLM
configurations across different parts of the application.
2026-05-04 09:17:08 +02:00
Brummel d86e2d7843 Add .kotlin/sessions to wearos gitignore
Add Fachrichtung to prompt and refine medical terms.
Added Fachrichtung to the system prompt, instructing the LLM to consider
the medical specialty for precise terminology.
Also, added "Abdomensonographie" to the `medical_terms.txt` file.
2026-05-03 21:42:30 +02:00
Brummel 8ce2e54847 Refactor tile layout to show recent cases
The tile layout has been refactored to display a list of recent cases
instead of the currently active case. This change includes:

- Removing the "current case" view and its associated logic.
- Implementing a new `caseListContent` function to render a list of up
  to 8 recent cases.
- Modifying the main `buildTileLayout` to integrate the new case list
  and a prominent "Neu" (New) button.
- Updating the styling and click handling to reflect the new layout and
  functionality.
2026-05-03 21:41:13 +02:00
Brummel d5f234e159 Update edit launcher title to use case oneliner
Use the current case's oneliner text as the title for the remote input
intent. This ensures consistency with the CaseListScreen's display when
no oneliner is present, showing a "…" placeholder. This improves the
user experience by making the system input picker's title match what the
user just selected.
2026-05-03 21:14:59 +02:00
Brummel 99a481bda5 Fix: Adjust LLM token budget and navigation flow
Increase `max_completion_tokens` for `gpt_oss_120b` from 8192 to 16384
to prevent incomplete responses.

Navigate to the detail screen before the recording screen when starting
a new case, ensuring correct back navigation.
2026-05-03 20:42:42 +02:00
Brummel 1b6f4bde67 feat: Handle analyze retry with failure marker
When a previous analysis attempt fails and leaves a failure marker, the
retry mechanism now correctly identifies this state. It removes the
stale
input file, allowing the retry to proceed without a conflict error. This
ensures users can recover from analysis failures more gracefully.
2026-05-03 19:33:09 +02:00
Brummel 366a8381c4 Restrict backend selection to admins
An empty `backend` form field now correctly falls back to the default. A
non-empty field submitted by a non-admin user is rejected with a 403
error, preventing potential tampering or issues arising from stale
sessions after role changes.
2026-05-03 18:35:49 +02:00
Brummel f90f78e78e Refactor system prompt for clarity 2026-05-03 18:30:33 +02:00
Brummel 0848e9581f Fix gpt-oss-120b with reasoning effort
This commit adjusts the configuration for the `gpt-oss-120b` model to
run in plain-text mode. This is necessary because when
`reasoning_effort` is set to "medium", the reasoning tokens combined
with schema-constrained decoding can exhaust the `max_completion_tokens`
budget, leading to an empty content response.

The change restores the previous behavior where `gpt-oss-120b` was
configured without `response_format` or `top_p`, ensuring the request
body sent to Ionos remains byte-identical to the pre-multi-backend
state. This prevents regressions and maintains stability for existing
production workflows.
2026-05-03 16:43:17 +02:00
Brummel 6fac8775e1 Update API documentation with Llama 3.1 quirks 2026-05-03 16:20:56 +02:00
Brummel 3243823b40 Add Llama 3.1 405B specific LLM parameters
Configure Llama 3.1 405B with specific temperature, top_p, and a format
instruction. This addresses issues with infinite repetition and empty
document generation observed with this model. The format instruction is
added as a separate system message to avoid modifying the core medical
prompt.
2026-05-03 16:15:43 +02:00
Brummel a2cde9406e fix: skip already-failed inputs in analyze recovery scan
`recovery::enqueue_pending_for_user` previously enqueued any case with
`analysis_input.json` and no `document.md`, ignoring the failure marker
that `auto_trigger::evaluate_case` already honours. A timed-out LLM
call (e.g. Llama 3.1 405B at ~20 tok/s with default 4096 max_tokens)
left the input in place, the next case-list reload re-enqueued it, and
the worker hung another full timeout window — starving fresh user
clicks behind a failed retry loop.

Recovery now mirrors the same skip: if a marker exists whose
`last_recording_mtime` matches the input's, treat it as "already
tried, do not retry until input changes". Fresh recordings bump the
mtime and recovery picks the case back up.
2026-05-03 15:23:44 +02:00
Brummel 23ef84d9d8 feat: multi-backend LLM layer with per-request choice on case page
Replace the single [llm] block in settings.toml with a curated catalog
of LLM "backends" (provider + model + sampling params + system prompt)
in server/src/analyze/backend.rs. Two backends ship initially:
gpt_oss_120b (default, reasoning_effort=medium) and llama_3_1_405b
(uses response_format: json_schema to sidestep the <|eot_id|> leak).

The case page now renders one submit button per available backend; the
clicked button's name=backend value rides through AnalysisInput.backend_id
to the worker, which looks it up via find_backend() with default fallback.
A submitted unknown backend id is rejected as 400. .analysis_failed.json
records the failed backend and the failure banner surfaces its label.

The API key now comes from IONOS_API_KEY (env), not settings.toml; the
[llm] section is read into a discarded field so old configs still load.
Backends without a satisfied requires_api_key are filtered from the UI
(any_backend_available()). Three end-to-end tests that mocked the LLM
endpoint via settings.llm.url are #[ignore]'d with a clear note — they
need per-test backend injection (Arc<Vec<LlmBackend>> through the worker)
before they can be re-enabled.
2026-05-03 14:57:21 +02:00
Brummel cbb072d0cc feat: Show LLM failure banner and retry button
When an LLM analysis fails, a `.analysis_failed.json` marker is created.
If no document exists yet and the auto-trigger is blocked by this
marker,
the case page must display a banner. This banner provides the failure
reason and a button to retry the analysis, ensuring users are not left
in a dead-end state.

The `read_failure_marker` function is made public to allow the web layer
to access this failure information. The `CasePageTemplate` is updated to
include `analysis_failed` data, which conditionally renders the new
`.failure-banner` HTML.

This change prevents cases from becoming unrecoverable due to transient
LLM errors.
2026-05-03 13:53:55 +02:00
Brummel bf6464d7e1 Add debug copy button and functionality
Includes a button for administrators to copy all transcripts and the
document markdown to the clipboard.
The data is embedded as JSON in a script tag and pre-sanitized to
prevent
`</script>` tag injection.
The JavaScript handles copying to the clipboard using the modern
Clipboard API,
with a fallback for older browsers.
2026-05-03 12:13:12 +02:00
Brummel 5ee276ba1d chore: ignore scripts/ build artefacts on main
Mirrors scripts/.gitignore from feature/mesh-vocab-pipeline so the
regenerable pipeline outputs under scripts/data/processed/ stop
showing up as untracked on main. Identical content avoids a merge
conflict when the feature branch lands.
2026-05-03 11:47:19 +02:00
Brummel daaace0e22 Refactor desktop client crate naming
The `client-desktop` crate is now located within the `clients/desktop/`
directory and renamed to `doctate-desktop`. This change improves
organization and clarity within the project structure.
2026-05-02 12:41:55 +02:00