page.server.spec.ts picked up an unrelated reader-dashboard test case via
a cross-session staging race; restore it to match main so this PR only
touches the import-normalizer tool + docs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Use documentService.getDocumentById() in detail_stillReturnsTrainingLabels
so the Document.full entity graph eager-loads trainingLabels
- Flatten makeItem() factory in DocumentList.svelte.test.ts (nested
document: {} overrides broke item.id / item.documentDate access)
- Remove { document: {} } wrapper from DocumentMultiSelect.svelte.spec.ts
mock responses — component now reads body.items directly as flat items
- Flatten single nested item in page.svelte.test.ts document list test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All components, specs, and the generated API client now use the new
DocumentListItem shape — flat access (item.title, item.sender) instead of
the removed item.document.* nesting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All route spec files that mock $lib/shared/api.server were missing
extractErrorCode from the mock factory, causing a vitest "No export defined"
error after the refactor introduced the new export.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ChronikFuerDichBox: move update() inside the failure branch so success
path skips it, matching NotificationDropdown's pattern
- NotificationDropdown test: add role=alert assertion for mark-all-read
failure to match existing dismiss-failure coverage in ChronikFuerDichBox
- +page.server.ts: use getErrorMessage(undefined) instead of null so the
missing-notificationId 400 goes through the same i18n pipeline as other errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace 'as string | null' cast (which silently accepts File values) with an explicit
typeof check. Use error: null instead of hardcoded German so the client falls through
to the generic i18n-keyed error banner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Casting null to string caused PATCH to fire against /api/notifications/null/read
when the field was absent. Added an early-return fail(400) and a test that
submitting an empty form returns 400 without calling the API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dismiss (X) button and mark-all-read button now submit forms to
/aktivitaeten?/dismiss-notification and /aktivitaeten?/mark-all-read respectively.
Props renamed onMarkRead/onMarkAllRead → optimisticMarkRead/optimisticMarkAllRead.
aktivitaeten/+page.svelte drops the now-deleted onMarkRead/onMarkAllRead wrapper functions
and passes notificationStore.optimisticMarkRead/optimisticMarkAllRead directly to the box.
Tests: $app/forms enhance mock added to both spec files so dismiss and mark-all assertions
work synchronously against form-submit events.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds two SvelteKit form actions to /aktivitaeten/+page.server.ts so the
notification bell can POST there instead of calling the backend directly
from the browser.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
- 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>
- 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>
`@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>
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>
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>
- 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>
- 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>
- 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>
- 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>
- 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>
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>
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>
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>
- 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>
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>
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>
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>
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>
- 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>
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>
- 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>
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>
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>
- 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>