Commit Graph

224 Commits

Author SHA1 Message Date
Brummel cea8ed8c06 Refactor Wear OS UI to use native surfaces 2026-04-23 22:00:12 +02:00
Brummel 66d13e741a Update Pixel Watch 2 testing status
The project plan has been updated to reflect the successful testing of
the Pixel Watch 2 hardware. The plan now includes details about
ADB-over-WiFi pairing and testing against a local development server. It
also mentions the successful manual verification of the end-to-end flow
on the real device.

Further testing, including LTE mode, doze mode, and long-term foreground
service behavior, is still pending.
2026-04-23 19:40:09 +02:00
Brummel cca41fe5ea chore: untrack .idea/ (IDE workspace state, not team-shared)
These files mutate on every IDE click and leak per-developer state
(last deployed device, recent activity) into commits. A single root
`.idea/` ignore replaces the scattered per-file rules in
watch/wearos/.gitignore.
2026-04-23 17:57:26 +02:00
Brummel 3429e8bdf7 feat(watch): support real Pixel Watch 2 via ADB-WiFi + LAN dev server
Real hardware has no USB and can't resolve 10.0.2.2, so we pair over
ADB-WiFi and point the app at the dev laptop's LAN URL. run.sh gains a
`watch`/`emulator` target prefix so both devices can be attached in
parallel without serial-picking roulette.

- network_security_config.xml: whitelist 192.168.178.27 for cleartext
- build.gradle.kts: resolveProp() threads Gradle CLI `-P` overrides
  into BuildConfig (SERVER_URL, API_KEY) ahead of local.properties
- run.sh: adb_cmd wrapper honours ADB_SERIAL; new `devices` and
  `connect [ip:port]` subcommands; `.watch_serial` caches the paired
  endpoint so `./run.sh watch install` just works on subsequent runs
- .gitignore: ignore `.watch_serial`
2026-04-23 17:54:15 +02:00
Brummel 2e3cb50ceb Update projektplan.md with current status 2026-04-23 16:44:27 +02:00
Brummel ed993a5c6d feat(watch): vertical PoC — record, upload, playback end-to-end
Completes the 5-second-record-and-upload flow:
- RecordingViewModel orchestrates recorder, countdown, and upload
- MediaRecorder wrapper produces 16kHz/AAC-LC m4a (Whisper-compatible)
- Compose RecordingScreen renders the state pyramid
  (Idle / RequestingPermission / Recording / Uploading / Success / Error)
- UiState + pure reducer with full JVM unit-test coverage
- run.sh "test" subcommand wraps the unit and instrumented tiers
  (set SKIP_INSTRUMENTED=1 to bypass the on-device tier)

Verified end-to-end: emulator captures "Hallo, hallo", lands at
tmpdata/<user>/<case>/*.m4a, Whisper transcribes, Ollama classifies,
document.md is written — full pipeline in ~5s.

Known: MockWebServer-based UploadClientTest hangs in setUp on the
Wear OS 34 emulator (likely SELinux socket policy). Deferred — the
real-server end-to-end already validates the wire protocol.
2026-04-23 16:30:57 +02:00
Brummel 8306dd8c47 feat(watch): scaffold upload client and app service locator
Builds the network foundation for the Wear OS vertical PoC:
OkHttp-based UploadClient mirroring doctate-client-core's wire protocol
(multipart /api/upload, X-API-Key, transient/terminal classification),
BuildConfig-backed Settings sourced from local.properties, CaseId factory,
DoctateApp service locator, MockWebServer-based UploadClientTest for
wire-format assertions, plus the manifest/permissions/network-security-
config plumbing for emulator-loopback cleartext.

MainActivity is still the placeholder UI; audio recording, ViewModel,
and recording screen come in follow-up commits.
2026-04-23 15:25:53 +02:00
Brummel 673e0aeb9d Add deviceManager.xml to project settings
Adds the deviceManager.xml file, which configures device sorting within
the IDE, to the `.idea` directory. This ensures consistent device
management across different developer environments.
2026-04-23 12:57:57 +02:00
Brummel 41adb89763 chore(watch): add run.sh helper for build/install/run
Inner-loop tooling so the dev cycle does not require Android Studio.
Subcommands: build, install, start, stop, logcat, shot, clean
(default = install + start). Auto-detects JAVA_HOME (bundled JBR) and
adb. Boots an emulator on demand when no device is attached: interactive
menu of available AVDs, with WEAR_AVD=<name> env-var bypass for
non-interactive use. Waits for sys.boot_completed (180s timeout) before
installing.
2026-04-23 12:29:11 +02:00
Brummel 9a72619f23 chore(watch): set header text to "Doctate!" 2026-04-23 12:29:06 +02:00
Brummel 8956b74c09 feat(watch): scaffold Wear OS app via Android Studio
Initial skeleton for the Pixel Watch dictation client (Plan Phase 5b).
Generated by Android Studio's "Empty Wear App" template, kept verbatim
to defer bikeshedding until real friction appears.

Toolchain pinned via wrapper / version catalog:
  AGP 9.2.0, Kotlin 2.2.10, Gradle 9.4.1, JDK 21 (auto-provisioned).
  compileSdk 36, minSdk 30 — covers Pixel Watch 1/2/3.

UI starter: Wear Compose Material3 1.5.6 with TransformingLazyColumn
and EdgeButton — already close to the planned per-case full-screen
list pattern.

Manifest declares the app standalone, so the Wear OS Network Proxy
will tunnel uploads transparently when the watch lacks LTE/WiFi.
No Phone-side companion module required.

Multi-module split (:core-domain, :core-audio, :core-sync, :core-http,
:core-storage, :app-mobile) intentionally postponed — current single
:app module is the smallest thing that compiles, which gives us a
known-good baseline before restructuring.

AS's default .gitignore left as-is. A handful of .idea/ project-config
files (compiler.xml, gradle.xml, misc.xml, codeStyles, runConfigurations)
are committed by AS's design; revisit if machine-specific paths or
multi-machine drift become painful.
2026-04-23 11:52:45 +02:00
Brummel e09108ec35 Update projektplan with CSRF and security headers 2026-04-23 09:38:56 +02:00
Brummel 813fb896ff refactor: consolidate bulk actions, URL assembly, and case-artefact filenames
Pulls three pattern groups into shared helpers so a rename or wire-format
tweak edits one file instead of 10+:

- BulkAction enum in doctate-common replaces the "close"/"analyze"/"reset"
  string literals that were duplicated between the bulk handler and 3
  attack/CSRF test files. FromStr preserves the exact "Unbekannte Aktion"
  error shape; the handler match is now exhaustive over the enum.
- join_url helper in doctate-common absorbs 7 identical
  trim_end_matches+format! call sites across client-core, client-desktop,
  server/transcribe (ollama, whisper), and server/analyze (llm).
- server/tests/common/artefacts.rs re-exports ONELINER_FILENAME
  (doctate-common), DOCUMENT_FILE + ANALYSIS_INPUT_FILE
  (doctate-server::analyze), and CLOSE_MARKER (doctate-server::paths) so
  test files reference the canonical name instead of inlining literals.

Also lifts client-desktop local duplication:
- paths.rs: project_path helper collapses 4 identical ProjectDirs chains
- main.rs: or_die helper replaces 4 eprintln!+exit(1) blocks
- app.rs: named RecordingContext struct replaces (Uuid, String) tuple
  at 3 sites around the ffmpeg-flush finalization path

Verification: 397 tests pass (baseline was 389; +8 for new unit tests on
BulkAction and join_url), 0 failed, 4 ignored (unchanged). clippy clean.
2026-04-22 12:09:46 +02:00
Brummel af3377a6bc refactor(tests): migrate remaining batches to tests/common/
Finishes the lift of shared helpers into `tests/common/`. Covered:
- health, web, oneliners_api, upload (31 tests; upload exercises the
  new `multipart_upload_body` helper driven by doctate_common field
  constants)
- sse_integration, sse_cleanup, transcribe, oneliner_heal_decoupled,
  silent_case_empty, failed_only_case_empty_oneliner,
  transient_failure_retries (24 tests)

Also applied cargo fmt across the test tree and fixed one clippy
needless_borrows_for_generic_args warning in analyze_test.

All 387 tests pass; 3 ignored (as before).

Side effect: health_test previously used a hardcoded `/tmp/doctate-test`
data path, which parallel `cargo test` runs could collide on. The
migration replaces it with the common unique-tmpdir pattern, removing
a latent flake.
2026-04-22 10:51:33 +02:00
Brummel f438e972d4 refactor(tests): introduce shared tests/common/ support module
Lift duplicated test-harness code (User factories, TestConfig builder,
login/CSRF flow, form/json helpers, case-directory seeding, URL path
builders) into `server/tests/common/` so future API changes touch one
file instead of every integration test.

Migrated batches: csrf_attack, auth, login, magic_link, security_headers
(28 tests); analyze, case_page, delete_recording, close_watermark,
retention_sweep (67 tests). All 95 tests still green.

Net line delta across these 10 files: +1113 / -1896 (~780 lines removed),
plus ~500 lines of new shared infrastructure under tests/common/.
2026-04-22 10:42:51 +02:00
Brummel 2e3b5efe86 feat: render CSRF token into state-changing forms
Adds the csrf_token hidden field to every POST form in the web UI
(logout, bulk, purge-closed, close, reopen, analyze, reset,
delete-recording). Login stays without a token — SameSite=Strict
already blocks the relevant attack shapes and forced-login CSRF
has no impact here.

Shape:
- New askama macro partials/csrf_field.html renders the hidden
  input; 3 templates import + {% call csrf::field(csrf_token) %}
  inside each <form method="POST">.
- AuthenticatedWebUser carries csrf_token (cloned out of the
  session once during extraction) so render handlers don't need a
  second store lookup.
- 3 template structs (MyCasesTemplate, CasePageTemplate,
  CaseRecordingsTemplate) gain the field; render sites pass
  user.csrf_token through.

Safety-net tests:
- Each rendered page must contain the exact session csrf_token in
  a hidden input — catches anyone adding a new form without the
  macro.
- Happy-path round-trip: fetch page, parse token from HTML, POST
  with token → 303. Catches drift between rendered and accepted
  token formats.
2026-04-22 10:04:21 +02:00
Brummel bf3e9ba6cc feat: server-side CSRF token validation
Introduces CsrfForm<T>: a FromRequest extractor that looks up the
session's csrf_token and constant-time-compares it to a csrf_token
field in the form body. Drop-in replacement for Form<T> on every
state-changing /web/ POST handler. Missing or expired session →
redirect to /web/login; malformed body → 400; token mismatch → 403.

WebSession carries csrf_token, minted alongside the session token at
login and magic-link consume. Not rotated per request — rotation
would break multi-tab use and buys little over SameSite=Strict
cookies.

Handlers: bulk, purge-closed, reset, close, reopen, analyze,
delete-recording, logout all now require CsrfForm<_>. Login stays
unprotected (SameSite=Strict alone is sufficient — forced-login CSRF
has no impact on this codebase). /api/... is header-auth, exempt.

Constant-time compare via subtle::ConstantTimeEq avoids timing
oracles on the token.

Template work is NOT in this commit — browser forms still post
without csrf_token, so the live web UI will 403 until Schritt 6
(templates render the hidden field).

Tests: all 12 red specs in csrf_attack_test.rs now green, #[ignore]
removed. Integration tests that POSTed to protected endpoints
switched to a new create_router_and_session_store entrypoint which
returns a handle to the store so tests can read csrf_token for the
active session.
2026-04-22 09:59:17 +02:00
Brummel f3d0380dbd feat: add defense-in-depth security-header layer
Attaches a SetResponseHeaderLayer stack to create_router_with_state
(not main.rs) so tests observe the same response shape as production.
`if_not_present` mode so per-route overrides (magic.rs already sets
its own Referrer-Policy) are preserved.

Sets: X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
Content-Security-Policy, Permissions-Policy. HSTS is deliberately
omitted until TLS termination is in place — a cached max-age on a
plain-HTTP deployment is irreversible.

CSP uses 'unsafe-inline' for script/style since templates contain
inline scripts; revisit if any user input ever renders unescaped.

Removes #[ignore] from the 10 attack-confirming header tests; two
regression anchors (no-HSTS, no-duplicated magic header) were already
green.
2026-04-22 09:33:46 +02:00
Brummel 34aa633d32 feat: Add CSRF and security headers tests
Adds comprehensive tests for CSRF protection and security headers to
ensure robust defense against common web attacks.
2026-04-22 09:19:25 +02:00
Brummel ba6aea06e5 Update project plan documentation
Add details about the `purge-closed` endpoint, including its admin-only
nature and confirmation requirement.
Clarify the admin-only checks for bulk actions, emphasizing that they
are now performed at the entry handler level.
Refine the explanation of the case list display, particularly the
`open_count/total_count` grouping and how closed cases are handled.
Introduce the concept of analysis preview in the case list, explaining
its implementation using CSS `line-clamp` and avoiding server-side
truncation.
Enhance the description of admin
2026-04-21 21:59:48 +02:00
Brummel 1182f4817e Enforce admin-only for bulk and purge actions
Move admin checks from individual bulk actions to the main handler for
`bulk.rs` and `case_actions.rs`. This consolidates the authorization
logic for these sensitive operations.

Additionally, refine the HTML template to conditionally render bulk
action UI elements and the purge form only when the user is an admin.
This ensures that non-admin users do not see or have access to these
administrative functions.

Update tests to reflect these changes, ensuring that non-admin users are
correctly rejected for these actions and that the UI is properly hidden.
2026-04-21 19:48:24 +02:00
Brummel d13bd5307e Refactor analysis display to full HTML
Replaces `analysis_preview` with `analysis_html` to display the full
markdown-rendered document content instead of just a plain-text preview.
This allows for richer content presentation and enables client-side
expansion for detailed views.

The HTML template has been updated to include a new `.analysis`
container that handles the expand/collapse functionality using
JavaScript. This approach avoids server-side truncation and relies on
CSS for presentation.
2026-04-21 18:21:04 +02:00
Brummel 87a04c4b27 Refactor case display for better readability
Introduce a dedicated case-link class for clickable case titles.
Enhance the line1 div to use flexbox for better alignment of time,
title, and recordings count.
Add a separate link for recordings, improving the structure and user
experience of the case list.
2026-04-21 17:47:42 +02:00
Brummel 75ede79d26 Refactor: Improve case grouping and counting logic
Introduce `count_closed_by_date` and `most_recent_date_label` to
efficiently count closed cases for display purposes when `show_closed`
is false.

Modify `group_by_utc_date` to accept a `closed_extra` map, allowing it
to correctly calculate both open and total case counts per day.

Update the `my_cases.html` template to display the new
`open_count/total_count` format for case groups.
2026-04-21 17:41:35 +02:00
Brummel 661ea6215e Add analysis preview to case list
Introduce a `preview_lines` setting in `users.toml` to control the
number of visible lines for the analysis preview in the case list. This
feature extracts the first paragraph of the `document.md` file, strips
markdown formatting, and displays it, capped by the configured
`preview_lines`. The actual line clamping is handled client-side via CSS
`line-clamp`.
2026-04-21 17:25:14 +02:00
Brummel 26b202ec07 Refine system prompt for keyword extraction 2026-04-21 17:10:55 +02:00
Brummel 3218fc62bd feat: server-authoritative case window via users.toml
The /api/oneliners time window is now read per-user from users.toml
(window_hours, default 72h). Clients no longer carry a window:
client.toml oneliner_window_hours, SyncConfig.window_hours, the
?hours=N query param, and the render_case_list cutoff filter are
gone. ETag suffix keeps the effective hours so an admin edit to
users.toml invalidates client caches on the next request.
OnelinersResponse.window_hours stays in the wire format, but now
exists solely to anchor client reconciliation.
2026-04-21 16:48:01 +02:00
Brummel c8186c9f88 style: align badge colors — open hellblau, Fehler capitalised
- my_cases.html: `.status-badge.open` from red (#e24a4a) to blue (#4a90e2),
  matching case_page.html (which was already blue) and the reopen-btn
  accent. Resolves the cross-template inconsistency and frees red for
  the Fehler-Badge.
- Both templates: `.status-badge.fehler` from deep red (#c00) to the
  established system red (#e24a4a), already used by `.bulk-bar.purge`
  and `.delete-btn:hover`.
- Badge label capitalised: "fehler" → "Fehler" (user-facing string
  only; CSS class stays `.fehler`).
2026-04-21 14:41:57 +02:00
Brummel 1f32d4dd23 fix: separate transient from permanent transcribe failures
Before, every Whisper error (5xx, timeout, Minerva down, corrupt audio,
4xx) renamed `<ts>.m4a` to `<ts>.m4a.failed` uniformly, turning transient
outages into permanent sackgassen that only a manual admin reset could
undo. Cases whose every recording was `.m4a.failed` stuck without a
persisted oneliner state; the UI masked them via a fallback in
`compute_oneliner_display`.

The core insight: a recoverable error is not an error. Transient Whisper
failures now leave the audio as plain `.m4a` so the existing page-load
heal (`enqueue_pending_for_user`) re-enqueues it on the next refresh —
which is exactly what happens when Minerva comes back. No new sidecar,
no retry count, no scheduler.

Changes:
- `WhisperError::is_transient` classifies `Http`, 5xx, 408, 429 as
  transient; `Io` and other 4xx as permanent.
- Transcribe worker: transient → info log + continue (audio stays .m4a);
  permanent → `mark_failed` as before.
- `has_any_transcript` counts `.m4a.failed` as terminal, so
  `update_oneliner` runs for failed-only cases and settles
  `OnelinerState::Empty` (analogous to the silent-only fix in 4531f85).
- New verdrängender `fehler`-Badge (#c00) in case list and detail when
  at least one `.m4a.failed` exists; recording-level message shortened
  to plain "Transkription fehlgeschlagen".

Tests:
- New integration: transient 503 leaves `.m4a` intact, heal recovers.
- New integration: `.m4a.failed`-only case settles to
  `OnelinerState::Empty` without calling Ollama.
- New unit: `is_transient` table test across relevant status codes.
- New unit: `has_any_transcript` returns true for `.m4a.failed`-only
  case and false for pending-only case.
- Existing worker test retargeted from 500 to 400 and renamed; added
  companion `worker_leaves_m4a_intact_on_transient_whisper_error`.
2026-04-21 14:39:31 +02:00
Brummel 4531f85b13 fix: settle silent-only cases to Empty oneliner state
Introduce `TranscriptState { Pending, Silent, Content }` in
doctate-common as the canonical three-way state of a recording's
`.transcript.txt` sidecar. Previously each call-site projected the
raw `Option<String>` / `.exists()` onto its own 2-state view and the
projections disagreed: the UI treated a 0-byte silent transcript like
`Content` while the oneliner worker treated it like `Pending`,
leaving silent-only cases stuck on "generiere Titel …" forever with
no persisted oneliner.json.

`update_oneliner` now settles a silent-only case to
`OnelinerState::Empty` when no `Content` transcript exists and no
recording is still `Pending`, so the UI resolves to "unbenannt" and
recovery treats it as terminal.

Single reader: `paths::read_transcript_state`. All call-sites
(scan_recordings, compute_oneliner_display, has_pending_recordings,
all_transcripts_joined, read_recordings, enqueue_pending_for_user,
scan_m4as, cases_needing_oneliner_in, case_recordings.html) go
through the same typed abstraction and must handle `Silent` via
exhaustive match.

Regression tests:
- silent_case_empty_test: heal path settles silent-only case to
  Empty without calling Ollama
- case_page_silent_only_shows_empty_not_generating: UI renders
  "unbenannt", not "generiere Titel …"
2026-04-21 13:37:51 +02:00
Brummel 3d42d1da82 fix: decouple oneliner-heal from request handler tasks
heal_orphans_if_idle called regenerate_missing_oneliners_for_user
inline on the request task, blocking the browser for up to N×60s
when orphans existed (observed with 16 cases after manual cleanup).

Now: CAS on new OnelinerHealBusy flag + tokio::spawn + BusyGuard
reset-on-drop. Handler returns immediately; the existing
OnelinerUpdated SSE event delivers results via the usual reload.
Startup recovery shares the flag so a concurrent page-load during
boot cannot fire a parallel regen loop against Ollama.

Regression test uses wiremock + timeout(150ms) + .expect(5) to
verify both non-blocking return and dedup of parallel invocations.
2026-04-21 12:56:02 +02:00
Brummel e5c8c5e3aa Refactor CLAUDE.md to add HTML section 2026-04-21 12:04:53 +02:00
Brummel 5255880d78 feat: preserve scroll position across case-action redirects
Clicking close/reopen or any bulk form submits a POST and follows the
303 to the listing, which is a fresh navigation — browsers do not
restore scroll on navigation, only on reload(). A user scrolled to
row 30 of a long list loses focus and has to scroll back manually.

Fix: small JS on my_cases.html that stashes window.scrollY in
sessionStorage before every form submit and restores it on the next
page load. Silent no-op when sessionStorage is blocked (strict
privacy mode), so the feature is purely additive.

No server changes — kept the redirect URLs clean. An earlier iteration
with a #case-<uuid> anchor on the redirect turned out to race with
the explicit scrollTo on any page where the referer scrollY was near
0; dropping the anchor made the JS the sole source of truth.
2026-04-21 12:01:51 +02:00
Brummel 8b1f58ba23 fix: preserve show_closed view + ungrey closed-case controls
Three UX polish items for the show-closed listing, grouped because
they are the same root problem in three surfaces:

* handle_reopen_case now redirects via resolve_return_path(&headers)
  instead of hard-coding /web/cases, so a reopen from the
  ?show_closed=1 listing stays in that view. Matches the pattern
  already used by analyze/reset/delete-recording.

* handle_close_case now redirects via the new helper
  resolve_list_return_path(&headers), which wraps resolve_return_path
  and additionally collapses a /web/cases/{uuid} detail path to
  /web/cases while keeping the query string. A close from the detail
  page previously would have tried to redirect back to the now-404
  detail, or when triggered from ?show_closed=1 would have dropped
  the query.

* Closed cases in my_cases.html now render the checkbox,
  Analysieren/Neu analysieren and Reset controls in disabled state
  instead of being hidden. Layout stays consistent with mixed-state
  listings, and the user can see what actions would be available
  after reopen. The handlers remain the authoritative guard via
  locate_case_or_404 -> 404 for closed cases, so the HTML disabled
  is a hint only.

Adds four unit tests for resolve_list_return_path and two integration
tests for the new redirect behaviour (close from listing with query,
close from detail with query).
2026-04-21 11:20:17 +02:00
Brummel 23e828df45 feat: show-closed toggle, closed badge, reopen button, bulk purge
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.
2026-04-21 10:58:00 +02:00
Brummel f358da215d feat: lazy retention sweep on /web/cases
New module retention::sweep_user_retention is invoked at the top of
handle_my_cases, using the current user's retention policy. Per case:
step 1 auto-closes when the newest .m4a mtime is older than
auto_close_days; step 2 auto-purges when closed_at is older than
auto_delete_days. Order matters - a case auto-closed in the same sweep
has closed_at ~= now and naturally survives the purge check,
preserving the full grace period after a vacation.

Race guard: purge re-reads the close marker immediately before
remove_dir_all so a concurrent upload that reopens the case cancels
the purge. Every automatic action emits an info! line with reason and
threshold; failures warn but never abort the sweep.

Seven integration tests cover both threshold paths, the disable-via-0
escape hatch, the newest-recording rule, the vacation scenario, and
the open-case skip. Tests age the comparison objects (mtime via
filetime, closed_at as handwritten RFC3339) instead of faking "now" -
no clock abstraction needed.
2026-04-21 10:51:27 +02:00
Brummel e5a62d8f10 feat: add per-user retention settings to users.toml
New [user.retention] TOML sub-block with auto_close_days and
auto_delete_days. Both default to 0, which disables the respective
sweep — a conservative default that preserves current behaviour for
existing deployments and lets admins test auto-close in isolation.

Only parsing + the test User literals are touched here; the actual
lazy sweep consuming these values lands in the next commit.
2026-04-21 10:48:24 +02:00
Brummel 8173ff2f26 feat: add POST /web/cases/purge-closed for bulk hard-delete
Iterates the user's data dir and removes every case directory that
carries a .closed marker. Requires form field confirm=yes as a server-
side guard against accidental browser-history re-POSTs. Emits CasePurged
per successfully removed case; per-case failures are logged but do not
abort the sweep.

UI button is deferred to the show-closed step so the endpoint is only
reachable after an explicit navigation. Three integration tests cover
the happy path, missing-confirm, and coexistence with open cases.
2026-04-21 10:46:07 +02:00
Brummel 396565a571 refactor: replace delete/undo flow with close/reopen endpoints
- 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.
2026-04-21 10:43:39 +02:00
Brummel 9410d6daaa refactor: rename soft-delete marker from .deleted to .closed
Internal rename: DELETE_MARKER -> CLOSE_MARKER, DeleteMarker ->
CloseMarker, is_deleted -> is_closed. The batch UUID is dropped;
undo now groups cases by their exact closed_at timestamp, which
bulk-close writes identically across its selection while per-case
close writes a unique stamp.

Behavior unchanged. No migration (project is pre-production);
legacy .deleted files in tmpdata were removed manually.
2026-04-21 10:36:55 +02:00
Brummel a8389a89db Update file marker for oneliner to json
The `oneliner.txt` file marker has been updated to `oneliner.json`. This
change reflects the new state management for the oneliner, which now
uses an internally-tagged enum (`OnelinerState`) to represent different
states (Ready, Empty, Error) along with a `generated_at` timestamp. This
provides a more robust way to handle different outcomes of the LLM call,
particularly differentiating between an intentionally empty response and
a transient error.
2026-04-20 17:39:17 +02:00
Brummel 5effa7c96e feat: Add endpoint to delete recordings
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.
2026-04-20 15:48:34 +02:00
Brummel 424330aad4 Fix player seek bar alignment
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.
2026-04-20 15:12:19 +02:00
Brummel 75c89c132f Refactor case page template and oneliner rendering
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.
2026-04-20 14:44:05 +02:00
Brummel 6886c20e26 Refactor OnelinerDisplay states
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.
2026-04-20 13:01:11 +02:00
Brummel d65720c671 Refactor oneliner storage to use enum
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.
2026-04-20 12:37:48 +02:00
Brummel 224ee60363 feat: Handle empty Ollama responses gracefully
Introduce `OllamaError::EmptyResponse` to represent cases where the
Ollama
model returns an empty result due to silence rules or lack of keywords.

This change prevents treating an empty response as a parse error and
logs
it at a lower severity level. A new integration test verifies this
behavior.
2026-04-20 11:24:44 +02:00
Brummel 3ad8c4c2e9 Refactor login page styling
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.
2026-04-20 10:55:16 +02:00
Brummel e795bdf6a5 feat: Display most recent recording time
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.
2026-04-20 10:51:01 +02:00
Brummel 1daf8db470 Refactor case action redirects and UI
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.
2026-04-20 10:39:21 +02:00