Commit Graph

291 Commits

Author SHA1 Message Date
Brummel 9efa052b6d plan: copy-close on case page
Bite-sized implementation plan for spec 0002 (case-page half of #14):
add a gated "Kopieren + Schließen" control below the document (Task 1,
with a shared copyDocText() refactor), a bare "Schließen" for
documentless open cases (Task 2), then remove the H1 trash-can close
(Task 3), and a whole-suite + clippy + fmt gate (Task 4). Ordered so
each task leaves the suite green.

refs #14
2026-06-01 14:29:01 +02:00
Brummel 43ff2f20e1 spec: copy-close on case page
Design spec for the case-page half of #14: a single "Kopieren +
Schließen" text button below the document that copies the document
text to the clipboard and then closes the case, landing on /cases.
The H1 trash-can close button is removed; the standalone icon copy
button stays. A failed clipboard write aborts the close so a finished
case never disappears with an empty clipboard.

Frontend-only (one Askama template + its inline script); no Rust
route change — the close handler already strips the case-page Referer
to /cases. The case-list half of #14 is deferred.

refs #14
2026-06-01 14:20:14 +02:00
Brummel a4ecad08ba chore(skills): add dev-cycle profile and switch docs to counter-prefix naming
Activate the skills plugin for this project with a project profile at
.claude/dev-cycle-profile.yml: server-focused inner-loop build/test,
scripts/check.sh as the audit regression gate, and the Gitea tracker
wired as the boss forward queue.

Set the naming policy to stable_per_directory_4digit (0001-slug.md) for
docs/specs and docs/plans, and rename the existing date-prefixed
artefacts to match. The one internal spec cross-link is updated.
2026-06-01 13:56:58 +02:00
Brummel f957d9ddc6 fix(web): disable reverse-proxy buffering on SSE /events
Reverse proxies (OpenResty in front of minerva) buffer text/event-stream
by default, holding events until the response ends. An SSE stream never
ends, so the browser stays silent and WebUI pages never live-reload when
reached via app.doctate.de — while a direct minerva.lan:3000 client
(bypassing the proxy) works, explaining the desktop/Chromebook split.

Emit X-Accel-Buffering: no on the /events response so the proxy streams
it unbuffered; a direct client ignores the header. Return type widens
from Sse<..> to impl IntoResponse to carry the header tuple.

Regression guard: sse_integration asserts the header is present.

refs #12
2026-06-01 13:22:42 +02:00
Brummel 5e86cb59b0 fix(web): make closed cases fully viewable (discovery-gate, not access-gate)
Follow-up to 6d1ca71: that commit made the recordings sub-page render for a
closed case by threading ?show_closed=1, but the page's own links and the
audio player still 404'd — playback was dead and the back link led nowhere.
Root cause: the `.closed` marker was implemented as an ACCESS gate (every
case-scoped read 404s a closed case unless the opt-in flag is threaded
through), while it is documented as a DISCOVERY gate ("hidden from the
default listing"). Threading the flag through every link/redirect is
whack-a-mole; the next new link forgets it.

Resolved in favour of discovery-gate semantics: the `.closed` marker only
filters the default case LIST (scan_user_cases). Direct/deep-link access to
a known, owned case and all its sub-resources is closed-agnostic. IDOR is
unchanged — it comes from the per-user root + existence check, orthogonal
to closed-ness.

Scope 1 — closed cases are fully viewable:
- handle_audio: drop the is_closed -> 404 gate (web.rs). Audio is a
  capability URL behind the same per-user path + slug/UUID/filename
  validation as an open case.
- handle_case_page / handle_case_recordings: always resolve via
  locate_closed_case_or_404; show_closed is no longer an access gate, so
  back links (recordings -> case, case -> list) reach live pages without
  threading the flag.
- the "back to list" links carry ?show_closed=1 when the case is closed
  (computed server-side from the marker) so the user returns to the
  closed-inclusive list, not the default list that hides the case.

Scope 2 — closed means read-only:
- case_page.html withholds the analyze / reset / re-analyze / retry forms
  on closed cases; only the reopen affordance remains.
- case_recordings.html withholds the per-recording delete form on closed
  cases (CaseRecordingsTemplate gains is_closed).
- the mutating handlers (analyze/reset/delete/oneliner) keep rejecting
  closed cases via locate_case_or_404 as defense-in-depth behind the
  now-hidden buttons.

Tests (RED-first):
- web_test: audio serves a closed case's file (200, not 404).
- case_page_test: closed case page + recordings render without
  ?show_closed=1; closed case hides analyze/reset; open case still shows
  delete; closed case hides delete.
- analyze_test: closed_case_returns_404_on_detail rewritten to
  closed_case_detail_renders_read_only — the old test pinned the
  now-superseded access-gate contract.

cargo test (440 passed), clippy, and fmt all clean.

closes #17
2026-06-01 01:42:25 +02:00
Brummel 6d1ca71d61 fix(web): serve closed-case recordings page instead of 404
Following "Aufnahmen anzeigen" on a closed case returned HTTP 404
{"error":"Case not found"} — an apparently empty page. handle_case_recordings
unconditionally resolved the case via locate_case_or_404, which rejects any
case carrying the .closed marker, so the recordings (which stay on disk after
a close — only a marker is written) became unreachable.

Fix mirrors handle_case_page: the handler now takes Query<CaseListQuery> and
branches to locate_closed_case_or_404 when show_closed=1 is requested, falling
back to the IDOR-guarded locate_case_or_404 otherwise. The in-app link in
case_page.html now carries ?show_closed=1 for closed cases (mirroring
my_cases.html), so navigation from a closed case's detail page no longer drops
the flag and 404s.

Reuses the existing CaseListQuery/include_closed() extractor; no parallel type.
Minimal fix, no surrounding refactor.

RED+GREEN combined: regression test case_recordings_honours_show_closed_for_closed_case
in server/tests/case_page_test.rs seeds a closed case with a recording and
asserts the route returns 200 with the recording listed, not 404. Full
case_page_test suite green (18 passed); cargo clippy clean; cargo fmt clean.

closes #17
2026-06-01 01:11:28 +02:00
Brummel 8d9fd22bdc fix(analyze): fold document output to ISO 8859-15 for Medical Office paste
German practice software (INDAMED MEDICAL OFFICE) stores free text in
ISO 8859-15, an 8-bit code. Pasting our cleaned document there rendered
typographic Unicode the LLM emits — en-/em-dashes, curly quotes,
ellipses, fraction slashes — as replacement boxes, while umlauts, the
micro sign, guillemets and the euro sign (all within 8859-15) pasted
fine. The KBV xDT data standard confirms ISO 8859-15 as the canonical
charset for German practice systems.

Add `analyze::charset::fold_to_iso8859_15`, applied in the worker right
after the gazetteer and before `content_md` is persisted: representable
chars pass through verbatim, the residual is transliterated via the
`deunicode` table (en-dash -> '-', ellipsis -> '...', '1/2' -> "1/2"),
and unmapped chars are left untouched rather than dropped. Because
representable chars never reach deunicode, its ASCII-only nature does
not flatten umlauts or the micro sign.

refs #13
2026-06-01 00:54:43 +02:00
Brummel a37a1ae363 feat(web): make server WebUI usable on phone viewports
The server-rendered pages worked on desktop but were near-unusable on a
phone: no viewport meta tag (mobile browsers laid out in a ~980px canvas
and zoomed out, rendering everything tiny) and a fixed `max-width: 900px`
desktop body with zero responsive breakpoints.

Fix is CSS-only and purely additive:
- Add `<meta name="viewport" content="width=device-width, initial-scale=1">`
  to all four page templates (each owns its own <head> — askama uses
  import/include here, no shared base layout, so the tag lands in 4 places).
- Append a single `@media (max-width: 640px)` block to the three
  desktop-width pages (my_cases, case_recordings, case_page): case rows
  and action bars reflow to a single column, the audio player goes
  full-width, and primary touch targets (delete/reopen/play) get a 44px
  minimum. login.html already has a 360px column layout, so it needs the
  viewport tag only.

Desktop layout is preserved by construction: a `max-width: 640px` query
never matches at >=900px, so the desktop render is byte-identical. The
diff adds lines only — no existing rule is touched.

Breakpoint chosen at 640px (well below the 900px body width) so all
portrait phones, including the ~390px target, fall under it while
tablets keep the desktop layout.

New integration test server/tests/responsive_test.rs pins the two
textual acceptance criteria that survive without a headless browser:
every page ships the viewport meta, and the three desktop-width pages
ship an @media breakpoint. The visual criteria (operable at 390px, 44px
touch targets) are CSS-driven and verified by eye — deliberately not
covered by a browser-automation dependency.

Verified: cargo test -p doctate-server --test responsive_test (4/4
green); web/login/case_page regression suites green; diff is additive.

closes #10
2026-05-30 15:44:53 +02:00
Brummel 67d26eaf4b plan: responsive WebUI on phone viewports
Plan for issue #10. CSS-only: viewport meta tag on every page plus a
@media (max-width: 640px) breakpoint on the three desktop-width pages.
Desktop (>=900px) render stays byte-identical.
2026-05-30 15:40:23 +02:00
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