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>
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>
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>
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>
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>
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>
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>
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>
- 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>
With the Spring Session model the browser forwards fa_session itself —
the proxy no longer needs to translate auth_token → Authorization: Basic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
userGroup: GET /api/users/me with Cookie: fa_session=<id>. On 401, drop
the stale cookie and redirect to /login?reason=expired (unless already
on a public path) so the user sees an explainer instead of a silent kick.
handleFetch: forward fa_session as a Cookie header on every API call
except the public auth endpoints. Drops the old auth_token injection.
Also adds a one-off cleanup of any lingering auth_token cookie from
pre-migration sessions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The backend POST invalidates the spring_session row and writes the
LOGOUT audit entry; the client cookie is deleted unconditionally so a
network blip during logout still logs the user out locally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the Basic-credentials-in-cookie flow with the Spring Session model:
1. POST {email, password} as JSON to /api/auth/login
2. Map 401 → INVALID_CREDENTIALS (or SESSION_EXPIRED if the backend returns it)
3. Parse Set-Cookie for fa_session=<opaque> and re-emit to the browser
4. Drop the legacy auth_token cookie
load() now also exposes ?reason= so the page can show the
session-expired banner (Task 21 wires it into the .svelte file).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors the backend ErrorCode additions from commit 393a3c25.
Adds error_session_expired_explainer for the login-page banner that
will surface when ?reason=expired.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Lower tracesSampleRate from 1.0 to 0.1 in both hooks (errors still captured
at 100%; trace volume reduced for self-hosted GlitchTip on shared VPS)
- Add comment explaining VITE_SENTRY_DSN is a write-only ingest key, safe in
client bundle — prevents accidental rotation as if it were a password
- Restore HTTP status code prominence: text-4xl font-bold (was text-xs text-ink-3)
- Add min-w-[44px] to copy button for WCAG 2.2 minimum touch target
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The handleError callback in hooks.server.ts is now gated by the 80% branch
coverage threshold along with the rest of the server-side logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two tests matching the existing hooks.server.test.ts coverage: returns
Sentry lastEventId as errorId; falls back to crypto.randomUUID when
lastEventId returns undefined.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds availability guard (navigator.clipboard may be undefined in non-HTTPS
contexts) and a rejection handler so clipboard-denied errors are silently
caught rather than becoming unhandled promise rejections. Tests cover the
success feedback and the silent-failure path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5646e739 added svelte-kit sync before lint so .svelte-kit/tsconfig.json
always exists. This activated projectService: true for every run, which
builds the full TypeScript language service for all .svelte files and
caused CI lint to take 7+ minutes.
None of the rules in the Svelte-specific block need type information —
they are all AST-selector-based no-restricted-syntax checks. Removing
projectService restores the previous fast path without losing any lint
coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the sentrySvelteKit() Vite plugin as the first plugin in vite.config.ts.
When SENTRY_AUTH_TOKEN is set at build time, source maps are uploaded to
GlitchTip so error stack traces show original TypeScript source and line number.
When SENTRY_AUTH_TOKEN is absent (CI, dev builds), upload is disabled via
autoUploadSourceMaps: false — the build succeeds normally.
Resolves Felix's review blocker on PR #591.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds @sentry/sveltekit to hooks.client.ts and hooks.server.ts.
When VITE_SENTRY_DSN is unset (default), Sentry is fully disabled.
When set to a GlitchTip JavaScript project DSN, browser exceptions
and SSR handleError events are forwarded automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The production stage runs npm ci --omit=dev to install runtime deps for
the pre-built SvelteKit app. The postinstall script calls patch-package,
which is a devDependency, so it is absent and causes exit code 127.
--ignore-scripts is the correct npm-native fix: no lifecycle scripts are
needed when installing into a pre-built image.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use untrack() for intentional one-time prop seed in UserGroupsSection.
Add explicit LoadData type alias in page.server.test to avoid void|Record<string,any> union.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When groups load successfully but the list is empty, render a quiet
"Keine Gruppen vorhanden." message rather than a blank section that
leaves users uncertain whether groups failed to load.
Adds admin_new_invite_no_groups i18n key to de/en/es.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- legend uses m.admin_new_invite_groups() instead of hardcoded "Gruppen"
so screen readers announce the correct string in en/es locales
- label gets min-h-[44px] for WCAG 2.2 touch target compliance
- add test asserting fieldset accessible name comes from i18n key
- add test documenting empty-groups-no-error renders no checkboxes/banner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace hand-copied load/action replicas with direct imports of the
real module. Mock $env/dynamic/private so the tests cover the actual
production code paths, not a duplicate that can drift.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Screen readers now announce the amber warning when it appears after
the form expands, without requiring the user to navigate to it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bind:group requires a writable $state variable; $derived is read-only
in Svelte 5, so every click was silently reset to unchecked, making
the group picker non-functional.
Also wraps checkboxes in <fieldset>/<legend> for WCAG 1.3.1 compliance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- load() fetches /api/groups in parallel with /api/invites; returns
sorted groups array and groupsLoadError for partial failures
- create action forwards groupIds[] to POST /api/invites so invited
users are placed in the selected groups on registration
- +page.svelte: group checkboxes via UserGroupsSection inside the form;
amber warning banner when groups could not be loaded
- page.svelte.test.ts: groups checkboxes + warning banner tests
- page.server.test.ts: parallel fetch, sorting, error fallback,
groupIds in POST body
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the error code to the ErrorCode union and getErrorMessage() switch.
Adds admin_new_invite_groups, admin_invite_groups_load_error, and
error_group_has_active_invites to all three locale files (de/en/es).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract ImportStatus type to types.ts — removes duplication across
+page.svelte, ImportStatusCard.svelte, and test file (Felix blocker)
- Fix H2 to match CLAUDE.md card pattern: text-xs uppercase tracking-widest
text-ink-3 mb-5 (Leonie blocker 1)
- Add font-sans to RUNNING and DONE status labels (Leonie blocker 2)
- Add data-testid="processed-count" to count elements in both states
- Replace document.querySelector with locator API in spinner tests
- Tighten getByText('7') to getByTestId('processed-count') (Felix/Sara)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
toBeAttached() is not in the vitest-browser matcher set; toBeVisible() was
previously ruled out because the spinner is 0x0 px. Mirror the querySelector
pattern already used for the negative case in the same file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CI Chromium runs with German locale so hardcoded English strings like
'No spreadsheet file found.' never matched. Use m.admin_system_import_*()
to assert whatever locale the browser resolves to.
Spinner test used toBeVisible() on an empty <span> whose dimensions come
entirely from Tailwind CSS. Without layout CSS the span is 0×0 and fails
the visibility check; toBeAttached() asserts DOM presence, which is the
right semantic here.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three test files were written against the old API shape (raw `message` field) before
the statusCode i18n field was introduced, or used the wrong `expect` import path:
- ImportStatusCard.svelte.test.ts: `@vitest/browser/context` does not export `expect`
in this project's Vitest setup — use `vitest` like every other test file.
- page.svelte.spec.ts: FAILED mock lacked `statusCode`; assertion matched old German
raw message instead of the i18n string for IMPORT_FAILED_NO_SPREADSHEET.
- page.svelte.test.ts: same pattern — mock lacked `statusCode`; assertion checked for
raw backend string "database error" instead of the rendered i18n text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove dead `message` field from both frontend ImportStatus types
(field is now @JsonIgnore'd on the backend)
- Extract failure message ternary into `$derived` — business logic off
the template (Felix)
- Add motion-reduce:animate-none to spinner — WCAG 2.1 SC 2.3.3 (Leonie)
- Replace text-green-600 with text-green-800 — WCAG AA contrast 6.1:1
on bg-green-50 (Leonie)
- Add min-h-[44px] to all three buttons — WCAG 2.2 44px touch target (Leonie)
- Add 6 missing tests: IMPORT_FAILED_INTERNAL path, IDLE state text,
null importStatus, ontrigger called on DONE/FAILED/IDLE buttons (Sara)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts the mass-import block from +page.svelte into ImportStatusCard.svelte.
Changes per the three UX fixes from issue #533:
- RUNNING: animated spinner (animate-spin) + processed count at text-base;
auto-poll at 2 s was already in place
- DONE: processed count at text-base, label at text-xs uppercase tracking-widest
- FAILED: maps statusCode (IMPORT_FAILED_NO_SPREADSHEET / IMPORT_FAILED_INTERNAL)
to Paraglide messages — no raw German backend string rendered
Adds vitest-browser tests covering spinner visibility, count display,
and per-statusCode FAILED message selection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the {message} interpolation (raw German backend string) with
two distinct error keys: IMPORT_FAILED_NO_SPREADSHEET and
IMPORT_FAILED_INTERNAL. Also removes the {count} parameter from the
done message and adds admin_system_import_status_done_label so the
processed count can be rendered separately at text-base size.
All three locales (de / en / es) updated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
testTimeout: 30_000 causes Vitest to fail a hanging browser test
within 30 s when Chromium crashes mid-load instead of silently
occupying the CI slot for 14+ min.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use [key: string]: unknown index signature so TS does not reject the
extra fields (location, status) passed to the redirect/failure result
in the spec helpers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirror the groups/new fix: replace inline beforeNavigate/isDirty with
createUnsavedWarning() + UnsavedWarningBanner and add an enhance callback
that calls clearOnSuccess() before update() on redirect results.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use createUnsavedWarning() + UnsavedWarningBanner to replace the inline
beforeNavigate/isDirty pattern, and add an enhance callback that calls
clearOnSuccess() before update() so the guard is disarmed before
SvelteKit's internal goto() fires on a redirect result.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace && with ; in test:coverage so the client vitest run is not
short-circuited when the server run exits non-zero (e.g. threshold
violation or test failure). Without this the upload-artifact step
only ever sees coverage/server.
Also updates the stale CLAUDE.md comment that said server-only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CDP-based Playwright clicks (locator.click()) do not reliably trigger
Svelte 5 onclick handlers — documented in commit 0c765d81 which fixed
13 other specs. The layout dropdown tests were missed in that pass.
Applies the same pattern: ((await locator.element()) as HTMLElement).click()
for button interactions, and native KeyboardEvent dispatch for the Escape
test (dispatched on the button so it bubbles to the parent div's onkeydown).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>