Fixes WCAG 2.1 AA contrast failure (#341): text-accent (#a1dcd8) on light
PDF control bar was 1.52:1 — well below the 4.5:1 AA minimum. text-primary
resolves to #012851 in light mode (14.5:1) and #a1dcd8 in dark mode (9:1) —
both states pass AA in both themes.
Adds PdfControls.svelte.spec.ts with 5 tests covering toggle visibility,
label strings, and the contrast-safe class assertion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#344
## What was implemented
### Commit 1 — `feat(nav): add cursor-pointer and tooltip to notification bell`
- Extracted `bellLabel` as `$derived` in `NotificationBell.svelte` — eliminates the duplicated inline ternary and keeps tooltip/label in sync reactively
- Added `title={bellLabel}` to the bell `<button>` — native tooltip mirrors `aria-label` in both zero and non-zero unread states
- Added `cursor-pointer` to the bell button's class list
- Added global `button { cursor: pointer; }` rule in `@layer base` of `layout.css` — prevents future regressions (global scope per Decision Queue)
- Added 3 component tests in `NotificationBell.svelte.spec.ts`: cursor-pointer class present, title equals aria-label when unread=0, title equals aria-label when unread=3
### Commit 2 — `fix(nav): replace hardcoded ThemeToggle title with Paraglide i18n keys`
- Added `theme_toggle_to_light` / `theme_toggle_to_dark` keys to `de/en/es` messages
- Extracted `themeLabel` as `$derived` in `ThemeToggle.svelte` and bound both `aria-label` and `title` to it
- Fixes the pre-existing hardcoded English strings (`'light mode'` / `'dark mode'`) per Decision Queue resolution
Touch target size was descoped per the Decision Queue.
## Decision Queue resolutions (from issue #344)
- **cursor-pointer scope**: global via `@layer base` ✅
- **ThemeToggle scope**: fixed in this issue ✅
- **Touch target**: descoped ✅
## Test results
All 5 `NotificationBell` tests pass.
Co-authored-by: Marcel <marcel@familienarchiv>
Reviewed-on: http://heim-nas:3005/marcel/familienarchiv/pulls/351
Regression guards verifying that Spring Security returns 401 (not 200) when
no credentials are provided, complementing the existing 403 permission tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the wait+clear cycles that existed only to drain the audit events
emitted by createUserOrUpdate(null, ...). Timeouts increased 5 → 10 s to
reduce CI flakiness under load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
createUserOrUpdate(UUID actorId, ...) is always called from the controller with
a real authenticated actor. createUserForBootstrap() handles seeding/test setup
without emitting an audit event, making the two contracts unambiguous.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates a real actor user first (needed for audit_log FK constraint),
then creates and deletes a target user, asserts USER_DELETED is newest
and USER_CREATED is second via findRecentUserManagementEvents.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds findRecentByKinds JPQL query to AuditLogQueryRepository and
findRecentUserManagementEvents(int limit) to AuditLogQueryService,
returning the N most recent USER_CREATED/USER_DELETED/GROUP_MEMBERSHIP_CHANGED
events ordered newest-first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds actorId param to adminUpdateUser(), captures beforeGroups before
mutation, computes added/removed group names, emits logAfterCommit only
when the group set actually changes. Payload contains group names, not
permission strings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds actorId param to deleteUser(), captures email before deletion,
emits logAfterCommit(USER_DELETED) with userId+email in payload.
Updates UserController to resolve and pass actorId.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds USER_CREATED, USER_DELETED, GROUP_MEMBERSHIP_CHANGED to AuditKind.
Injects AuditService into UserService; changes createUserOrUpdate to
accept actorId and emits logAfterCommit(USER_CREATED) only on the
new-user branch. Updates UserController to resolve and pass actorId.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds jsonPath("$.code").value("INVALID_PERSON_TYPE") to verify the full
error response shape, not just the HTTP status.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
validatePersonFields now returns a PersonValidationKey instead of a
hardcoded German string. resolveValidationMessage() translates the key
through Paraglide so English and Spanish locale users no longer see
German error text. Adds validation_last_name_required and
validation_first_name_required to all three message files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes four independent PersonType type declarations and the duplicated
TYPES/PERSON_TYPES arrays. normalizePersonType moves from the edit route
module into the shared lib so page.server.test.ts no longer imports from a
route. Both server actions now use normalizePersonType for personType
extraction instead of an inline type cast.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
radioGroupNav now accepts an onChange callback; PersonTypeSelector passes
select() as the callback so ArrowLeft/Right navigation updates the hidden
input value. aria-live region starts empty and announces only on user
interaction (fixes initial page-load announcement).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New src/lib/person-validation.ts exports validatePersonFields (pure function)
- 8 unit tests covering: valid PERSON, lastName missing/undefined,
firstName missing/undefined for PERSON, non-PERSON types without firstName
- Both edit and new-person server actions now call the shared helper instead
of inline if-chains, making the logic testable and non-duplicated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests now import from production code instead of a local copy, giving real
regression protection if the inline logic is changed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PersonController trims title (both create + update) matching the existing firstName/lastName trim pattern
- PersonControllerTest: verifies title is trimmed before service call (ArgumentCaptor)
- PersonControllerTest: verifies createPerson returns 400 when personType is SKIP
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces hardcoded brand-navy/brand-sand/white classes with semantic
tokens (bg-primary/text-primary-fg, bg-surface/text-ink, border-line,
ring-focus-ring) so the segmented control adapts correctly in dark mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Words like "Wille" stem to "will" via the German Snowball stemmer, which is
also a German stop word. The prefix-transform step (websearch_to_tsquery text →
regexp_replace → to_tsquery) was passing already-stemmed lexemes back through
the German dictionary, causing them to be silently dropped as stop words. Using
the 'simple' configuration skips stop-word processing entirely while the
tsvector @@ tsquery comparison still works because lexemes are matched by
string value, not by configuration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move result count, bulk-edit button, and new-document link into a shared
flex row so they appear on the same line. Adds an edit icon to the
bulk-edit button to visually match the existing plus icon on the add link.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Felix C2 — `BatchMetadataRequest` controller now uses `@Valid` so future
@Size/etc. annotations on the record actually fire.
Felix C3 — Auto-clear `$effect` in `+layout.svelte` reads
`bulkSelectionStore.size` inside `untrack()` so the effect only re-fires on
route change, not on every checkbox toggle.
Felix C4 — `BulkDocumentEditLayout` edit-mode hydration loop now lives
inside `onMount` (not at top-level script) so the SvelteMap mutation is
unambiguously tied to instance lifecycle, matching the pattern used by
`WhoWhenSection`/`DescriptionSection` after the cycle-2 fix.
Felix C5 — Replaced fully-qualified `java.util.LinkedHashSet` in
`DocumentController` with a top-of-file import.
Sara coverage — six new spec files / blocks pin the cycle-1 and cycle-2
behaviours that were previously untested:
- `WhoWhenSection.svelte.spec.ts` — onMount seeding from initialDateIso /
initialLocation; doesn't stomp parent-bound dateIso; hideDate / editMode
branch
- `DescriptionSection.svelte.spec.ts` — onMount seeding from initialTitle /
initialDocumentLocation; doesn't stomp parent-bound values; archive-box /
archive-folder fields visible only in editMode
- `BulkSelectionBar.svelte.spec.ts` — Esc-scope guard tests for `<dialog>`
open and `aria-expanded` popover present
- `BulkDocumentEditLayout.svelte.spec.ts` — topbar reads
"Massenbearbeitung" + "werden bearbeitet" in edit mode (not the
upload-flavoured "hochladen"/"werden erstellt" copy)
- `DocumentControllerTest.patchBulk_returns400_whenArchiveBoxExceeds255Chars`
— pins the @Size validator on archiveBox via the @Valid wiring
Refs #225, PR #331
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>