Commit Graph

1615 Commits

Author SHA1 Message Date
Marcel
76ef54e064 test(transcription): cover non-JSON fallback in markAllReviewed error path
Adds a test for when the server returns a non-JSON body (e.g. an nginx
502 HTML page). Confirms the res.json().catch(() => ({})) fallback
produces 'INTERNAL_ERROR' as the thrown message and leaves blocks intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:28:39 +02:00
Marcel
f1d1ac3f1a test(transcription): assert error banner shows domain-specific message
Adds toHaveTextContent(m.transcription_mark_all_reviewed_error()) to the
error-present test. The previous check only asserted presence via
role="alert", which would not have caught the dead key bug — the banner
was showing the generic fallback rather than the operation-specific copy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:27:29 +02:00
Marcel
0f48ffede5 fix(transcription): use domain-specific message in markAllReviewed catch
Removes the getErrorMessage() indirection and calls
m.transcription_mark_all_reviewed_error() directly in the catch block.
The previous implementation routed through getErrorMessage(code) which
mapped any error code to the generic m.error_internal_error() fallback,
leaving the domain-specific key unreachable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:23:59 +02:00
Marcel
3e72157ee1 test(transcription): update markAllReviewed non-OK test to expect throw
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m14s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m22s
CI / fail2ban Regex (pull_request) Successful in 41s
CI / Semgrep Security Scan (pull_request) Successful in 18s
CI / Compose Bucket Idempotency (pull_request) Successful in 58s
The function now throws instead of silently returning on failure.
Update the test name and assertion to match the new behaviour, and
verify blocks remain unchanged after the error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:43:21 +02:00
Marcel
e2d3975524 test(transcription): replace hardcoded regex with m.* calls in mark-all spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:40:28 +02:00
Marcel
59e99f862a fix(i18n): wire TranscriptionEditView mark-all button through Paraglide
Replace hardcoded German strings with m.transcription_mark_all_reviewed()
and m.transcription_mark_all_reviewed_disabled().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:39:39 +02:00
Marcel
bb39ca59ec feat(i18n): add transcription_mark_all_reviewed and _disabled message keys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:39:06 +02:00
Marcel
6b53cbfc5b feat(transcription): show dismissible error banner when markAllReviewed fails
Adds markAllError state and catch block to handleMarkAllReviewed.
Error banner renders below the review progress bar with role="alert"
and aria-live="polite" for screen reader announcement. Dismiss button
clears the error; next successful call also clears it automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:38:28 +02:00
Marcel
e3e8373526 fix(transcription): throw error from markAllReviewed() on non-2xx response
Previously the function silently returned on failure, leaving no way
for callers to detect or surface the error to the user.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:37:21 +02:00
Marcel
907a6a6b53 feat(i18n): add transcription_mark_all_reviewed_error message key
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:36:44 +02:00
Marcel
f27e2d33a5 test(transcription): add failing tests for markAllReviewed error display
RED phase: 4 new Vitest browser tests that fail because the error
banner and catch block don't exist yet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:35:56 +02:00
Marcel
6832300a4b test(viewer): replace hardcoded German strings in PdfControls spec with m.* calls
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m30s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m18s
CI / fail2ban Regex (pull_request) Successful in 40s
CI / Semgrep Security Scan (pull_request) Successful in 19s
CI / Compose Bucket Idempotency (pull_request) Successful in 59s
CI / Unit & Component Tests (push) Successful in 3m30s
CI / OCR Service Tests (push) Successful in 20s
CI / Backend Unit Tests (push) Successful in 3m14s
CI / fail2ban Regex (push) Successful in 42s
CI / Semgrep Security Scan (push) Successful in 19s
CI / Compose Bucket Idempotency (push) Successful in 59s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 17:26:17 +02:00
Marcel
9c5267e1f0 test(e2e): assert hamburger aria-label translates to EN on mobile viewport
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m20s
CI / OCR Service Tests (pull_request) Successful in 19s
CI / Backend Unit Tests (pull_request) Successful in 3m33s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m1s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:54:21 +02:00
Marcel
4979ae1867 fix(i18n): wire TranscriptionEditView training label through Paraglide
Replaces hardcoded visible text 'Für Training vormerken' with
m.transcribe_mark_for_training() so the label translates in EN and ES.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:53:16 +02:00
Marcel
29ef82f7b4 fix(i18n): wire AppNav hamburger aria-label through Paraglide messages
Replaces hardcoded 'Menü öffnen'/'Menü schließen' ternary with
m.layout_menu_open()/m.layout_menu_close() so the mobile nav toggle
announces correctly in EN and ES locales.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:52:08 +02:00
Marcel
f458c11a0d fix(i18n): wire PdfControls aria-labels through Paraglide messages
Replaces hardcoded Zurück/Weiter/Verkleinern/Vergrößern aria-label strings
with m.viewer_previous_page(), m.viewer_next_page(), m.viewer_zoom_out(),
and m.viewer_zoom_in() so viewer controls translate in EN and ES locales.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:50:58 +02:00
Marcel
e615ba1bbf fix(i18n): add message keys for viewer, transcribe, and layout controls
Adds 7 Paraglide keys (viewer_previous_page, viewer_next_page,
viewer_zoom_out, viewer_zoom_in, transcribe_mark_for_training,
layout_menu_open, layout_menu_close) to de/en/es.json.

Adds messages.spec.ts to enforce key parity across all three locales.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:50:08 +02:00
Marcel
1bec7dd17e chore(ci): bump Playwright Docker image to v1.60.0-noble
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 5m0s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m24s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m1s
CI / Unit & Component Tests (push) Successful in 3m34s
CI / OCR Service Tests (push) Successful in 20s
CI / Backend Unit Tests (push) Successful in 3m26s
CI / fail2ban Regex (push) Successful in 44s
CI / Semgrep Security Scan (push) Successful in 20s
CI / Compose Bucket Idempotency (push) Successful in 1m2s
The dep update resolved @playwright/test and playwright to 1.60.0.
The CI container was pinned to v1.58.2-noble which lacks the matching
browser binary, causing the browser project to fail to launch and
coverage thresholds to hit 0%.

Also raises @playwright/test and playwright lower bounds in package.json
to ^1.60.0 to keep the declared range consistent with the lockfile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:17:06 +02:00
Marcel
a0339a5526 fix(patches): regenerate @vitest/browser-playwright patch for 4.1.6
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 1m56s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m19s
CI / fail2ban Regex (pull_request) Successful in 41s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m1s
The backport of vitest PR #10267 (unroute-before-register guard that
prevents orphan routes causing birpc teardown crashes) was made against
4.1.0. The dep bump moved the package to 4.1.6; patch-package refused to
apply the stale file. Regenerated against the installed 4.1.6 — the fix
is identical, adapted for the renamed idPreficates → idPredicates typo
that upstream corrected in this version.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:00:53 +02:00
Marcel
65cae4a5e8 chore(deps): raise package.json lower bounds to patched versions
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 39s
CI / OCR Service Tests (pull_request) Successful in 19s
CI / Backend Unit Tests (pull_request) Successful in 3m30s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 18s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m1s
Bumps declared semver ranges to the patched minimums so a fresh
npm install (without the lockfile) cannot resolve to a vulnerable
version:
  @sveltejs/adapter-node  ^5.4.0  →  ^5.5.4
  @sveltejs/kit           ^2.48.5 →  ^2.60.1
  vite                    ^7.2.2  →  ^7.3.3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:56:09 +02:00
Marcel
c8cc0646cb fix(deps): align @tiptap packages to 3.23.4 to resolve type conflict
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 39s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m22s
CI / fail2ban Regex (pull_request) Failing after 42s
CI / Semgrep Security Scan (pull_request) Successful in 19s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m3s
npm update caused @tiptap/starter-kit@3.22.5 to nest @tiptap/core@3.23.4
alongside the pinned top-level 3.22.5, splitting the type namespace and
causing svelte-check errors (toggleBold, toggleItalic, etc. not found).

Aligning all three pinned tiptap packages to 3.23.4 collapses the nested
copy via deduplication, restoring the pre-bump error count (792 = main).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:03:14 +02:00
Marcel
ff3e863032 security(deps): bump @sveltejs/kit and vite to clear 5 high CVEs
Bumps @sveltejs/kit 2.55.0→2.60.1, vite 7.3.1→7.3.3, and all patched
transitives. Clears GHSA-3f6h-2hrp-w5wx, GHSA-2crg-3p73-43xp,
GHSA-4w7w-66w2-5vf9, GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583.

Residual: cookie <0.7.0 (LOW) via @sveltejs/kit peer chain — upstream
fix requires @sveltejs/kit@0.0.30, a breaking downgrade. Tracked as
known residual per issue #458 acceptance criteria note.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:53:09 +02:00
Marcel
8fc32f18ce refactor(admin/invites): regenerate types; remove InviteListItem cast
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m17s
CI / OCR Service Tests (push) Successful in 21s
CI / Backend Unit Tests (push) Successful in 3m24s
CI / fail2ban Regex (push) Successful in 42s
CI / Semgrep Security Scan (push) Successful in 19s
CI / Compose Bucket Idempotency (push) Successful in 1m1s
After adding @Schema(requiredMode=REQUIRED) to InviteListItemDTO.shareableUrl,
npm run generate:api now emits shareableUrl as required. Replace the hand-rolled
InviteListItem interface with a type alias to the generated InviteListItemDTO
and remove the two 'as unknown as InviteListItem' casts + TODO comments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:33:07 +02:00
Marcel
0cd9ea915e fix(admin): address PR #623 second-pass review feedback
- Fix VALID_STATUSES fallback to use uppercase enum value
- Add TODO comment on InviteListItem cast pending type regeneration
- Guard revoke action against null id (returns fail 400)
- Add request: to delete action mock events for Sentry consistency
- Add expiresAt forwarding test for create action
- Add null-id guard test for revoke action

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:33:07 +02:00
Marcel
f0e7f73ec1 fix(admin): address PR #623 review feedback
- Add load() unit tests for admin/users/[id] (permission gate, 404, success)
- Rename .test.ts → .spec.ts for consistency with rest of suite
- Add @Schema(requiredMode=REQUIRED) to InviteListItem.shareableUrl
- Add client-side allowlist for invite status query param

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:33:07 +02:00
Marcel
567f9267e8 fix(tests): add missing Sentry mock event fields across 14 spec files; fix test:coverage semicolon
`@sentry/sveltekit` wraps load functions and reads `event.request.method` and
`event.url.pathname`. Mock events that omitted `request` or `url` threw
`TypeError: Cannot read properties of undefined` on every invocation, silently
masking 86 test failures on main.

Two root causes fixed:
- Added `request: new Request(...)` (and `url: new URL(...)` where absent) to
  all mock event objects in 14 `*.server.spec.ts` files
- Changed `;` to `&&` in the `test:coverage` npm script so a failing server
  run propagates its exit code instead of being swallowed by the client run

All 576 server-project tests now pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:33:07 +02:00
Marcel
31d3ec8367 refactor(admin/users): migrate update action to createApiClient
Replace fetch('/api/users/${id}', { method: 'PUT', ... }) + inline JSON
error parsing with createApiClient(fetch).PUT('/api/users/{id}', ...) and
the standard result.error cast pattern.

Also fix pre-existing Sentry mock event failures in layout.server.spec.ts
by adding request and url to the test event object.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:33:07 +02:00
Marcel
d739f58bb5 refactor(admin/invites): migrate to createApiClient; fix Sentry mock event
Replace manual fetch(${apiUrl}/api/...) calls in load, create, and revoke
with createApiClient(fetch) so auth injection is handled by handleFetch
and the typed API contract is enforced at compile time.

Also fix pre-existing load test failures caused by Sentry's load wrapper
reading event.request.method (add request to the mock event object).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:33:07 +02:00
Marcel
18e675a5b2 fix(import): address non-blocking review feedback — touch target, glossary, edge-case test
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m18s
CI / OCR Service Tests (push) Successful in 19s
CI / Backend Unit Tests (push) Successful in 3m22s
CI / fail2ban Regex (push) Successful in 41s
CI / Semgrep Security Scan (push) Successful in 18s
CI / Compose Bucket Idempotency (push) Successful in 1m0s
- Add min-h-[44px] py-2 to <summary> in ImportStatusCard for 44 px touch target
- Add SkippedFile and skipped count entries to docs/GLOSSARY.md
- Add MassImportServiceTest case: ALREADY_EXISTS fires before file I/O when doc is UPLOADED and file is present on disk

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:45:03 +02:00
Marcel
a3fc838855 fix(import): surface S3 failures + already-exists in skippedFiles, a11y + max-height
- Change importSingleDocument return type from boolean to Optional<String>
  so callers in processRows receive the skip reason on every non-success path.
  S3 upload failures now surface as "S3_UPLOAD_FAILED" and already-imported
  documents as "ALREADY_EXISTS" in the skippedFiles list shown in the admin UI.
- Add two new tests: runImportAsync_addsS3UploadFailed_toSkippedFiles and
  runImportAsync_addsAlreadyExists_toSkippedFiles; update
  importSingleDocument_skips_whenDocumentAlreadyUploadedNotPlaceholder and
  the S3-failure test to assert on the Optional return value.
- Add i18n keys for S3_UPLOAD_FAILED and ALREADY_EXISTS in de/en/es messages.
- Svelte ImportStatusCard: add aria-hidden="true" to SVG chevron, wrap
  conditional warning section in aria-live="polite" div, add max-h-64
  overflow-y-auto to skipped-files <ul> to cap height on large batches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:45:03 +02:00
Marcel
d5043053e0 fix(import): address round-3 review concerns
- Add comment to openFileStream() explaining package-private visibility
  is intentional (Mockito spy seam for IOException test)
- Key {#each} skippedFiles by filename instead of array index
- Add test: skipped section hidden when state is FAILED
- Add test: reasonLabel returns raw code for unknown reason strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:45:03 +02:00
Marcel
c932dd19d9 fix(admin): address round-2 review concerns on ImportStatusCard
- Use loop index as each key (handles duplicate filenames)
- Increase skipped filename font from text-xs to text-sm
- Add motion-safe guard to details chevron transition
- Replace text-warning with text-amber-900 to meet WCAG AA contrast

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:45:03 +02:00
Marcel
c532ad21bf test(admin): add regression test for skipped section hidden during RUNNING
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:45:03 +02:00
Marcel
5587722800 fix(import): address PR review concerns
- remove duplicate List import in AdminControllerTest
- derive skipped() from skippedFiles.size() — drop redundant int field
- use machine codes for SkippedFile.reason (INVALID_PDF_SIGNATURE, FILE_READ_ERROR)
- map reason codes to i18n strings in ImportStatusCard (de/en/es)
- replace raw amber Tailwind classes with warning semantic token
- fix <summary> accessibility: replace list-none with rotating chevron SVG
- replace <p> with <span> inside <summary> (phrasing content rule)
- extract setupOneValidOneFakeImport() helper — remove 3x copy-paste
- add lenient mock to short-file test for defensive coverage
- add IOException path test for isPdfMagicBytes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:45:03 +02:00
Marcel
0451b6630c feat(admin): surface skipped file count in ImportStatusCard
Adds SkippedFile to the local ImportStatus type and updates
ImportStatusCard to show an amber skipped-count section with a
collapsible filename list in the DONE state. Only rendered when
skipped > 0. i18n keys added for de/en/es.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:45:03 +02:00
Marcel
164a917d95 fix(auth): tighten API URL match, add Retry-After header, and add missing tests
Some checks failed
CI / fail2ban Regex (push) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Semgrep Security Scan (push) Has been cancelled
CI / Compose Bucket Idempotency (push) Has been cancelled
- frontend/hooks.server.ts: replace request.url.includes('/api/') with
  new URL(request.url).pathname.startsWith('/api/') so a page named
  /my-api/something cannot accidentally match the API gate
- DomainException: add optional retryAfterSeconds field and a new
  tooManyRequests() factory overload that carries the value
- LoginRateLimiter: pass windowMinutes * 60 as retryAfterSeconds when
  throwing TOO_MANY_LOGIN_ATTEMPTS (RFC 6585 §4 SHOULD)
- GlobalExceptionHandler: emit Retry-After header when retryAfterSeconds
  is set on a DomainException
- RateLimitInterceptor: emit Retry-After: 60 on 429 responses (1-min
  window matches the existing MAX_REQUESTS_PER_MINUTE logic)
- LoginRateLimiterTest: assert retryAfterSeconds equals window duration
- RateLimitInterceptorTest: assert Retry-After header is set on 429
- JdbcSessionRevocationAdapterIntegrationTest: new @SpringBootTest +
  Testcontainers test verifying revokeAll deletes all spring_session rows
  and revokeOther leaves the current session intact

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
96c0aa592c fix(auth): address PR #617 review feedback on CSRF/rate-limit implementation
- Remove unreachable `&& !xsrfToken` condition from `handleFetch` guard;
  simplify the redundant `cookieParts.length > 0` check that follows it
- Add `TOO_MANY_LOGIN_ATTEMPTS` to both Error Handling sections in CLAUDE.md
  (backend and frontend) so LLMs are aware of the code without looking it up
- Add reverse-proxy IP trust and IPv6 address-cycling caveats to ADR-022
  Consequences section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
873d668653 test(login): add browser component test for rate-limited login UI state
Renders LoginPage with form.rateLimited=true and asserts that the
role="alert" div (clock icon + error message) is visible in the browser.
Previously only the form action's rateLimited=true return value was tested;
now the rendered UI is also verified.

Addresses Sara Concern 4 / Elicit open question from PR #617 review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
9bf8cf831d fix(login): add role=alert to error divs; fix clock icon color to red
Regular error div was missing role="alert" — screen readers did not
announce it on dynamic display. Rate-limited clock icon used text-ink-3
(muted grey) instead of text-red-600, visually inconsistent with the
surrounding error text. Also removes the erroneous aria-invalid="true"
from the rate-limit alert div (not a permitted attribute on role=alert).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
78fd9e026e feat(frontend): add CSRF injection, rate-limit i18n, and 429 login handling
- handleFetch injects X-XSRF-TOKEN + XSRF-TOKEN cookie on all mutating
  backend API requests (double-submit cookie pattern); generates a fresh
  UUID when no XSRF-TOKEN cookie exists yet
- ErrorCode union gains CSRF_TOKEN_MISSING and TOO_MANY_LOGIN_ATTEMPTS;
  getErrorMessage maps both to i18n keys
- de/en/es messages add error_csrf_token_missing and
  error_too_many_login_attempts translations
- Login action maps HTTP 429 to fail(429, { ..., rateLimited: true });
  page shows a muted clock icon with aria-invalid on rate-limit errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 09:23:01 +02:00
Marcel
17d9328c62 style(login): banner body text raised from text-xs to text-sm
text-xs (12px) is below Leonie's body-copy floor for the senior reader cohort
who hit /login?reason=expired on a phone in sunlight after being logged out.
text-sm (14px) restores legibility without breaking the visual hierarchy with
the heading. Addresses PR #612 / Leonie L3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:52:19 +02:00
Marcel
e10090b9ef style(login): banner has an aria-hidden warning icon
Color-blind reader cohort (8% of men) on a phone in sunlight cannot rely on
amber alone to parse the banner as a warning. Add a Heroicons-style
exclamation-triangle SVG, aria-hidden because the heading text already
conveys the meaning to assistive tech. Addresses PR #612 / Leonie L2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:51:48 +02:00
Marcel
4f1594390e style(login): banner uses the text-warning semantic token
Replace text-amber-900/text-amber-800 with the existing --color-warning
utility from layout.css. The amber soft fill stays (matching the precedent
of the green "registered" banner; a full surface-token pair is out of scope
for this PR). Addresses PR #612 / Leonie L1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:51:13 +02:00
Marcel
1f4e8a5958 test(auth): userGroup hook redirect + cookie cleanup coverage
Four new tests against the composed handle (with sequence stubbed to return
the head function): backend 401 on a private path redirects to
/login?reason=expired; backend 401 on /login does NOT redirect (no loop);
missing fa_session passes through without a backend call; 200 attaches the
user to event.locals. Closes the hook-coverage gap flagged by Sara S1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:50:41 +02:00
Marcel
d64139d9d1 test(auth): Vitest coverage for logout action
Three tests: happy path POSTs to backend with the session cookie and clears
both fa_session and legacy auth_token; cookies are cleared even when the
backend call rejects (best-effort logout); skips the backend call when no
session cookie is present. Addresses PR #612 / Sara S1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:49:06 +02:00
Marcel
2779502f3b test(auth): Vitest coverage for login action
Six tests covering: load() exposes ?registered and ?reason; action returns 400
on missing email; 401 with INVALID_CREDENTIALS on backend reject; success
re-emits fa_session and deletes legacy auth_token; 500 when backend omits
fa_session in Set-Cookie. Closes the frontend coverage gap on the credential-
handling logic that moved out of the Java side. Addresses PR #612 / Sara S1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:48:16 +02:00
Marcel
9f1e2c9ff5 refactor(auth): hooks.server.ts re-throws redirects via isRedirect()
Replace the duck-typed `status in error && location in error` check with the
official SvelteKit guard. Fragile against minor-version error-shape changes
becomes a one-liner against a typed helper. Addresses PR #612 / Felix F1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:45:46 +02:00
Marcel
dd99c5dd74 refactor(auth): login action imports extractFaSessionId from \$lib/shared/cookies
Drop the inline parser; reuse the now-shared helper. Pure rewire, no behaviour
change. Addresses PR #612 / Felix F2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:44:30 +02:00
Marcel
b607677f30 refactor(auth): extract extractFaSessionId to \$lib/shared/cookies
Move the Set-Cookie parser out of login/+page.server.ts into a shared module
with its own Vitest coverage (single-header, multi-header getSetCookie path,
missing-header, attribute-stripping, prefix-match-rejection). An Undici or
Node upgrade that changes header shape now trips its own test instead of
silently breaking login. Addresses PR #612 / Felix F2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:43:09 +02:00
Marcel
0bd00a3044 feat(auth): session-expired banner + autofocus + 44px touch target on login
- Amber aria-live banner when ?reason=expired (set by hooks.server.ts
  after the backend rejects an expired fa_session) with a one-line
  explainer about the 8h idle window.
- autofocus on email so users returning after a session-expired kick
  can immediately retype credentials.
- min-h-[44px] on the submit button hits the iOS HIG / WCAG 2.1 AAA
  touch target minimum — relevant for the reader cohort on phones.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 20:58:30 +02:00