Commit Graph

1661 Commits

Author SHA1 Message Date
Marcel
a7b1dcb5e1 fix(stammbaum): JOIN FETCH persons in relationship queries
Both /api/network and /api/persons/{id}/relationships threw
LazyInitializationException when toDTO read Person.getDisplayName():
the read-side service methods aren't @Transactional, so the session
closed before the proxy could initialize.

Eagerly fetch r.person and r.relatedPerson in the two queries used
by these endpoints, keeping the no-@Transactional convention for
read methods.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
f382bd9974 test(stammbaum): E2E spec + extend person load mock
- frontend/e2e/stammbaum.spec.ts covers four journeys:
  1) /briefwechsel still resolves with a 2xx after the nav swap.
  2) /stammbaum shows the page heading.
  3) /stammbaum renders either the empty state (with the Personenliste
     link) or at least one node[role=button] in the SVG.
  4) The person edit card surfaces the year-range error when Bis < Von.

- persons/[id]/page.server.spec.ts gains two extra mockResolvedValueOnce
  entries per scenario to match the new relationships +
  inferred-relationships GETs that the page load now performs.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
d7f4f6f163 feat(stammbaum): person detail Beziehungen card
- persons/[id]/+page.server.ts loads relationships and
  inferred-relationships in the existing parallel fetch.
- New PersonRelationshipsCard renders direct chips (mint) and the
  top-5 derived chips (grey) on /persons/{id}, both linked to the
  other person's page. Empty state shows
  "Noch keine Beziehungen bekannt." in muted serif.
- Card sits in the right column above the document lists.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
242e10179d feat(stammbaum): /stammbaum page — SVG tree + side panel + empty state
- /stammbaum/+page.server.ts loads GET /api/network (already filtered
  to family members on the backend) and returns nodes + edges.
- +page.svelte holds the page shell, manages selectedId (with
  ?focus={id} deep-link support) and zoom state, renders the empty
  state when nodes.length === 0 (icon + heading + body + link to
  /persons), or the tree + side panel otherwise.
- StammbaumTree.svelte: BFS-based generation assignment from roots,
  spouses promoted to the deeper generation so couples sit on the same
  row, alphabetical sort within row, simple grid layout. SVG nodes are
  role="button" + aria-label="{name}, {birth}–{death}" +
  aria-expanded={selected}, with click + Enter/Space activation. Solid
  parent→child connectors; mint spouse line with midpoint circle, dashed
  if SPOUSE_OF.toYear is set (former spouse). Zoom maps to viewBox.
- StammbaumSidePanel.svelte: lazily loads
  /api/persons/{id}/relationships and /inferred-relationships when the
  selection changes; shows direct chips (mint), top-5 derived chips
  (grey), and a "Zur Personenseite →" link. Escape closes the panel.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
aaf885cafd feat(stammbaum): person edit Stammbaum & Beziehungen card
New StammbaumCard rendered below the Namensverlauf card on
/persons/{id}/edit:
- Header with "Als Familienmitglied" toggle (form action
  toggleFamilyMember → PATCH /api/persons/{id}/family-member).
- "Erscheint im Stammbaum" banner with deep-link to
  /stammbaum?focus={id} when familyMember is true.
- Direct relationships list grouped by type, then year. Chip text is
  direction-aware: storage subject reads "Elternteil von", storage
  object reads "Kind von" (new relation_child_of i18n key in all 3
  locales). Symmetric and non-family types use their own keys.
- + Beziehung hinzufügen reveals an inline form with type select
  (grouped Familie / Sozial), a PersonTypeahead with the new
  excludePersonId prop (self-rel prevention, Elicit blocker 1), and
  Von / Bis year fields.
- Year validation lives client-side via $derived: empty/empty is OK,
  Bis < Von shows a red text-red-700 error wired with aria-describedby
  and disables submit (Sara blocker 3).
- Self-rel inline error mirrors the typeahead exclusion in case the
  user submits the personId regardless.
- Abgeleitete Beziehungen section (top 5) collapsed by default.

+page.server.ts loads relationships + inferred relationships in the
existing parallel fetch and adds three actions: toggleFamilyMember,
addRelationship (with year-range guard), deleteRelationship.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
b658a13247 feat(stammbaum): show inferred relationship in the document drawer
- New presentational RelationshipBadge component (labelFromA → arrow →
  labelFromB) wired into DocumentMetadataDrawer's Personen column,
  rendered after the receivers block when both endpoints are family
  members.
- DocumentTopBar gains an optional inferredRelationship prop and
  passes it through.
- documents/[id]/+page.server.ts loads the badge: only when sender is
  a family member, exactly one receiver, and that receiver is also a
  family member; 404 (no path) → null.
- relationshipLabels.ts maps the backend label keys (parent/child/...)
  to localised strings, so the server load returns badge-ready strings.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
6bed617959 feat(stammbaum): swap nav slot from /briefwechsel to /stammbaum
Both desktop and mobile nav rows now point at /stammbaum and read
m.nav_stammbaum(). The /briefwechsel route stays intact — only the
nav anchor changes.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
51db976348 feat(stammbaum): add i18n keys (de/en/es) + mirror error codes
In each of de/en/es:
- nav_stammbaum
- 9 relation_<type>_of keys for the stored relation types
- 17 relation_inferred_<label> keys covering everything LABEL_MAP emits
  (parent/child/spouse/sibling, grand*, great-grand*, uncle/aunt,
  niece/nephew, in-laws, cousin, distant)
- doc_details_field_relationship — badge label "Verwandtschaft"
- stammbaum_empty_*, stammbaum_panel_*, stammbaum_zoom_*,
  stammbaum_generations
- relation_error_* (inline form errors), relation_year_error_*,
  relation_label_*, relation_btn_*
- person_relationships_heading + person_relationships_empty
- error_relationship_not_found / error_circular_relationship /
  error_duplicate_relationship for the centralised error mapper

frontend/src/lib/errors.ts mirrors the backend's three new ErrorCodes
and routes them through getErrorMessage().

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
fc46704144 chore(stammbaum): regenerate TS API types for relationship endpoints
openapi-typescript pulled the Stammbaum schemas: Person now has
familyMember (required), plus PersonNodeDTO, NetworkDTO, RelationshipDTO,
InferredRelationshipDTO, InferredRelationshipWithPersonDTO,
CreateRelationshipRequest, FamilyMemberPatchDTO. Routes:
/api/network, /api/persons/{id}/relationships,
/api/persons/{id}/inferred-relationships,
/api/persons/{aId}/relationship-to/{bId}, and the family-member PATCH.

Test fixtures in PersonMultiSelect, briefwechsel page, and DocumentList
specs gained familyMember: false where they otherwise typed Person
end-to-end. Pre-existing "missing lastName/personType" fixture errors
in DocumentRow.spec are out of scope.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
050f2bc929 test(stammbaum): integration tests for relationship constraints
@DataJpaTest + Postgres Testcontainer; 7 cases per Sara blocker 1:
- addRelationship_stores_and_is_readable
- addRelationship_throws_409_when_duplicate (unique_rel)
- addRelationship_throws_409_when_circular_parent
- deleteRelationship_throws_403_when_rel_belongs_to_different_person
- deleteRelationship_succeeds_for_symmetric_type_from_either_side
- setFamilyMember_true_makes_person_appear_in_network
- delete_person_cascades_to_relationships

Service now uses saveAndFlush so the unique_rel violation surfaces
synchronously inside the @Transactional method (otherwise the
DataIntegrityViolation fires at commit time, outside the try-catch).
Unit-test mocks updated accordingly.

Backend suite: 1406/1406 green.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
f29f4d3f5b feat(stammbaum): RelationshipController for the Stammbaum API
Seven endpoints in one controller, two roots:
- GET  /api/network                                  → NetworkDTO
- GET  /api/persons/{id}/relationships               → List<RelationshipDTO>
- GET  /api/persons/{id}/inferred-relationships
- GET  /api/persons/{aId}/relationship-to/{bId}      → 200 or 404
- POST /api/persons/{id}/relationships               WRITE_ALL
- DEL  /api/persons/{id}/relationships/{relId}       WRITE_ALL, 204
- PATCH /api/persons/{id}/family-member              WRITE_ALL

PersonController is intentionally untouched. Controller-boundary
validation via RelationType.valueOf catches unknown types as 400 before
the service is invoked. FamilyMemberPatchDTO is a one-field record for
the family-member toggle.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
790c6f5b02 feat(stammbaum): RelationshipService + family_member toggle (TDD)
- Add PersonService.setFamilyMember (write, @Transactional) and
  findAllFamilyMembers; PersonRepository gains the
  findByFamilyMemberTrueOrderBy projection.
- RelationshipService orchestrates PersonService + the inference
  service; never reaches into PersonRepository directly. addRelationship
  guards self-relationship, year range, circular PARENT_OF (Nora B2),
  and DataIntegrityViolation→DUPLICATE_RELATIONSHIP. deleteRelationship
  enforces ownership from either side (Nora B1).
- Extend RelationshipDTO with personDisplayName + birth/death year so
  the frontend can render rows from either viewpoint.
- 8 unit tests, written against a stub (red), then green: FORBIDDEN
  delete, CIRCULAR add, DUPLICATE add, self-relationship, year range,
  happy-path persistence, ownership-from-object, RELATIONSHIP_NOT_FOUND.

Full backend suite: 1399/1399 green.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
acea4a60f2 feat(stammbaum): inference service with BFS + LABEL_MAP (TDD)
RelationToken enum (UP/DOWN/SPOUSE/SIBLING) with reverse(), and
RelationshipInferenceService with:
- Bidirectional adjacency map: PARENT_OF emits UP and DOWN, SPOUSE_OF
  and SIBLING_OF both directions.
- Virtual SIBLING edges derived from shared parents — no SIBLING_OF
  row required for siblings to appear.
- BFS with MAX_DEPTH=8.
- 17-entry LABEL_MAP covering parent, child, spouse, sibling, grand*,
  great-grand*, uncle/aunt, niece/nephew, great-uncle/aunt, great-niece/
  nephew, in-law parent/child, sibling-in-law (both paths), cousin_1.
- "distant" fallback for any path not in LABEL_MAP.
- Two-sided labels via path reversal.

18 unit tests written first against a stub; all 18 confirmed red, then
green after implementation. PersonControllerTest's anonymous DTO updated
for the new isFamilyMember() projection.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
25f62ce93b feat(stammbaum): add backend data layer for family network
- RelationType enum (9 values), PersonRelationship entity with
  @ToString(exclude = "notes") and LAZY person FKs.
- PersonRelationshipRepository with the network bulk fetch, the
  per-person subgraph fetch, and the existsBy check for the circular
  PARENT_OF guard.
- Six DTO records: CreateRelationshipRequest, RelationshipDTO,
  PersonNodeDTO, NetworkDTO, InferredRelationshipDTO,
  InferredRelationshipWithPersonDTO. @Schema(REQUIRED) on every
  always-populated field so OpenAPI/TS codegen stays accurate.
- Person entity gains familyMember, PersonSummaryDTO gains
  isFamilyMember, both PersonRepository projections select
  p.family_member.
- Three new ErrorCodes: RELATIONSHIP_NOT_FOUND, CIRCULAR_RELATIONSHIP,
  DUPLICATE_RELATIONSHIP.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
Marcel
df6175ed2c feat(stammbaum): add V54 migration for family network
Adds persons.family_member flag and person_relationships table with
ON DELETE CASCADE on both FKs, no_self_rel check, unique_rel composite,
indexes on both person columns, and partial unique index for symmetric
SIBLING_OF pairs (LEAST/GREATEST trick).

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
f6cf2e0e42 feat(transcription): add "Alle als fertig markieren" bulk action (#345) (#352)
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m15s
CI / OCR Service Tests (push) Successful in 51s
CI / Backend Unit Tests (push) Failing after 3m13s
## Summary

Implements the bulk "Alle als fertig markieren" action for the transcription panel requested in #345.

### Backend

- Added `PUT /api/documents/{documentId}/transcription-blocks/review-all` endpoint to `TranscriptionBlockController`, guarded with `@RequirePermission(Permission.WRITE_ALL)`
- Added `markAllBlocksReviewed(UUID documentId, UUID userId)` to `TranscriptionService` — `@Transactional`, single DB round-trip via `blockRepository.saveAll()`, emits one `BLOCK_REVIEWED` audit event per previously-unreviewed block
- Returns full updated block list (same shape as `listBlocks`) for a clean frontend update pass
- 5 new `TranscriptionServiceTest` unit tests (idempotency, audit events, empty document)
- 5 new `TranscriptionBlockControllerTest` `@WebMvcTest` tests (401, 403, 200 happy path, 200 empty, 401 user not found)
- All 68 backend tests pass

### Frontend

- Added `onMarkAllReviewed?: () => Promise<void>` prop to `TranscriptionEditView` (optional, consistent with `onTriggerOcr` pattern)
- Button placed in sticky progress header, right-aligned next to `reviewedCount / totalCount geprüft`
- Button is **disabled** (not hidden) when all blocks are already reviewed — `title="Alle Blöcke sind bereits als fertig markiert"` (Decision 1)
- Loading spinner replaces checkmark icon during operation — always shown (Decision 4, no threshold)
- Handler `markAllReviewed()` added to `documents/[id]/+page.svelte`, wired as `onMarkAllReviewed`
- 5 new `TranscriptionEditView.svelte.spec.ts` Vitest Browser component tests; all 25 tests pass

### Decisions applied

| # | Question | Choice |
|---|---|---|
| 1 | Button when all reviewed | **Disabled** with `title` tooltip |
| 2 | Audit log | **N individual BLOCK_REVIEWED events** (one per unreviewed block) |
| 3 | Atomicity | **All-or-nothing** via `@Transactional` |
| 4 | Loading indicator | **Always show** during operation |

Closes #345

Co-authored-by: Marcel <marcel@familienarchiv>
Reviewed-on: http://heim-nas:3005/marcel/familienarchiv/pulls/352
2026-04-28 08:34:26 +02:00
Marcel
33ca2df45b docs(specs): add Stammbaum UI specs — tree, document badge, person edit
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m12s
CI / OCR Service Tests (push) Successful in 29s
CI / Backend Unit Tests (push) Failing after 3m1s
Three standalone HTML spec files covering the initial Stammbaum release:
- stammbaum-tree-spec.html — desktop/tablet/mobile tree canvas with side panel, light + dark
- stammbaum-doc-badge-spec.html — inline relationship pill on document detail
- stammbaum-person-edit-spec.html — relationship editor card on person edit page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 13:09:47 +02:00
Marcel
0979302205 Revert "docs: add Stammbaum feature design spec"
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m5s
CI / OCR Service Tests (push) Successful in 35s
CI / Backend Unit Tests (push) Failing after 2m58s
This reverts commit 9fb2c025cf.
2026-04-27 09:58:35 +02:00
Marcel
9fb2c025cf docs: add Stammbaum feature design spec
Some checks failed
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has started running
Covers: person_relationships table, family_member flag,
RelationshipInferenceService (BFS path-to-label), /stammbaum
SVG page (generational + D3-Force toggle), relationship badge
on document detail, relationship editor on person edit page,
and nav swap Briefwechsel → Stammbaum.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 09:57:15 +02:00
Marcel
ee2de8135b fix(persons): align PersonMergePanel padding with other edit page cards
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m5s
CI / OCR Service Tests (push) Successful in 31s
CI / Backend Unit Tests (push) Failing after 2m52s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 09:06:25 +02:00
Marcel
fe13df574a test(persons): fix E2E flakiness — replace waitForTimeout with waitForListbox, remove conditional assertions, fix data-hydrated selector
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m7s
CI / OCR Service Tests (push) Successful in 36s
CI / Backend Unit Tests (push) Has started running
Addresses three blockers raised in PR #350 review (Felix, Sara, Tobias):

1. Replace all waitForTimeout(400) calls with waitForListbox() which uses
   waitForSelector('[role="listbox"]', { state: 'visible' }) — auto-waits
   for the debounce to resolve, faster on fast machines and reliable under CI.

2. Remove all conditional if (hasResults) / if (hasDropdown) wrappers.
   Tests now use unconditional expect(dropdown).toBeVisible() assertions so
   a missing-data condition causes an explicit failure instead of a silent
   green run.

3. Replace waitForSelector('[data-hydrated]') with waitForLoadState('networkidle')
   in getDocumentEditUrl — the data-hydrated attribute does not exist in the
   app markup and would cause a 30s timeout on every test.

4. Extract page: Page type import from @playwright/test and introduce
   waitForListbox(page: Page) helper to avoid repeating the selector pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 09:01:44 +02:00
Marcel
a9080e9dab test(persons): add ArrowDown forward-wrap unit test for keyboard navigation
Adds the missing 'ArrowDown from last wraps to first option' test to
close the asymmetric coverage gap noted by Sara (QA) in the review of
PR #350. The ArrowUp backward-wrap test already existed; this test
verifies the % modulo wrap works in the forward direction too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 09:01:44 +02:00
Marcel
e8a1cc82ff fix(persons): fix PersonTypeahead dropdown clipping with fixed positioning
The dropdown was clipped by parent containers using overflow, transform,
or stacking context via shadow-sm + z-index combinations. Adopts the same
fixed-position strategy as PersonMultiSelect: binds to the input element,
computes position via getBoundingClientRect(), and registers svelte:window
scroll/resize listeners to keep it current.

Also adds full ARIA combobox pattern (role=combobox, aria-expanded,
aria-haspopup, aria-controls, aria-activedescendant) and keyboard
navigation (ArrowDown/Up, Enter, Escape) matching TagInput's reference
implementation.

Removes the now-dead z-30/z-10 z-index workarounds from ConversationFilterBar.

Closes #343

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 09:01:44 +02:00
Marcel
5b18b87450 test(security): add 403 permission test for annotation DELETE endpoint
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m4s
CI / OCR Service Tests (push) Successful in 34s
CI / Backend Unit Tests (push) Failing after 3m0s
Confirms that DELETE /api/documents/{id}/annotations/{id} requires at
least ANNOTATE_ALL; a user with only READ_ALL receives 403 Forbidden.
Closes the permission audit raised during PR review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:56:37 +02:00
Marcel
bfa8b9c147 fix(viewer): move delete button inside annotation bounds to prevent edge clipping
Repositioning from top:-8px/right:-8px to top:4px/right:4px ensures the
44px touch target stays fully within the annotation shape. Annotations drawn
near the top or right edge of the PDF page no longer risk the button being
obscured or inaccessible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:56:37 +02:00
Marcel
3a94d62c74 test(viewer): verify delete button click does not bubble to onclick
Documents the stopPropagation guarantee: clicking the trash button must
not trigger the annotation's onclick (which opens the block detail panel)
while the delete confirm is in progress.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:56:37 +02:00
Marcel
163e99016a fix(viewer): check res.ok on orphaned annotation DELETE to surface errors
Without the guard, a failed DELETE (4xx/5xx) was silently swallowed and
annotationReloadKey was incremented anyway, leaving the annotation visible
and the user with no feedback. Now matches the deleteBlock() pattern
immediately above.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:56:37 +02:00
Marcel
d6f3ca5c43 feat(viewer): show delete icon on annotation for direct block deletion (#339)
Adds a trash icon button (44×44 px touch target) directly on each annotation shape in transcription mode so users can delete a block without navigating through the sidebar. Includes keyboard support (Delete key), confirm dialog via ConfirmService, prop-chain wiring through DocumentViewer → PdfViewer → AnnotationLayer → AnnotationShape, and orphaned-annotation fallback (calls DELETE /annotations/{id} when no block is linked). Backend security regression test added for deleteBlock 403 on READ_ALL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:56:37 +02:00
Marcel
108edff8d2 feat(persons): show merge panel inline on edit page, remove Gefahrenzone accordion
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
Closes #342. The PersonDangerZone collapsible wrapper is removed; PersonMergePanel
is now rendered directly in the edit page with its own red border (border-red-200),
preserving the {#key person.id} state-reset behaviour and the two-step merge flow.

Fix PersonTypeahead mock to use Svelte 5 functional stub (not Svelte 3/4 $$ internals).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:54:45 +02:00
Marcel
3d3fe8d626 fix(pagination): add sr-only span to preserve aria-current on mobile AT
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
When the mobile label is aria-hidden and the desktop button container is
display:none (below sm:), mobile screen reader users had no aria-current
indicator. Added a sr-only span with aria-current="page" that stays in
the AT tree at all breakpoints regardless of CSS display state.

On desktop the active page button also carries aria-current — both
announce the same page information, which is acceptable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:53:17 +02:00
Marcel
31e5573eab fix(pagination): hide mobile page label from AT tree with aria-hidden
The mobile 'Seite X von Y' span had aria-current='page', which created two
elements announcing the current page on wide screens: the hidden mobile label
and the active desktop button. On sm:+ screens the mobile span is display:none
(removed from AT tree), but on small screens both the span and the desktop
button were redundant.

Replace aria-current with aria-hidden='true' on the mobile label so AT always
relies on the desktop button's aria-current. Updates spec test accordingly and
adds a second assertion in a broader test context (Decision Queue #1).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:53:17 +02:00
Marcel
934a00feb3 fix(pagination): use stable key in {#each} and fix duplicate page number bug
Replaces position-based key `i` with `entry === null ? 'ellipsis-' + i : entry`
so DOM reconciliation is stable when the window shifts (Decision Queue #2).

The index-based key was masking a duplicate-push bug in pageWindow: when
windowStart === first+1 or windowEnd === last-1, the loop already included that
number, causing Svelte to throw `each_key_duplicate` once stable keys are used.
Fixed the bridge-page conditions to use first+2 / last-2 thresholds so the loop
and the bridge branches never push the same page number.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:53:17 +02:00
Marcel
be27489618 test(pagination): fix test name typo and add totalPages===2 boundary test
Renames 'page button buttons' → 'page buttons container' (Decision Queue #3).
Adds 'renders both pages without ellipsis when totalPages is 2' to cover the
boundary between the 1-page (hidden) and full-ellipsis-window cases (Decision Queue #5).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:53:17 +02:00
Marcel
4e486a31cf feat(pagination): add numbered page-jump buttons to document search
Adds an ellipsis-style numbered page button row (1 … 4 5 6 … 12) to
Pagination.svelte. Buttons are hidden on mobile (sm: breakpoint) and fall
back to the existing prev/next layout. Active page uses brand-navy
background. Client-side clamping via makeHref(entry - 1) satisfies AC3.
i18n key pagination_page_button added for de/en/es.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:53:17 +02:00
Marcel
2c5877ea9e fix(a11y): fix ProgressRing text label contrast and add no-restricted-syntax lint rule for text-accent
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
ProgressRing used text-accent (#a1dcd8) on a percentage text label —
same WCAG 2.1 AA failure as #341. Switched to text-primary.

Also adds ESLint no-restricted-syntax rule (scoped to *.svelte files) that
blocks future text-accent usage in JavaScript string literals inside Svelte
class expressions. The rule caught both violations at once; both are now fixed.
The rule is scoped to .svelte files so test assertions against 'text-accent'
strings in .spec.ts files are unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:46:44 +02:00
Marcel
cfbe33140c fix(viewer): replace text-accent with text-primary on annotation toggle inactive state
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>
2026-04-26 21:46:44 +02:00
e8d1835ae1 feat(nav): add tooltip and cursor:pointer to notification bell, fix ThemeToggle i18n (#344) (#351)
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
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
2026-04-26 21:45:40 +02:00
Marcel
ce41e96a45 test(audit): add 401 unauthenticated tests for createUser, adminUpdateUser, deleteUser
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m1s
CI / OCR Service Tests (pull_request) Successful in 34s
CI / Backend Unit Tests (pull_request) Failing after 3m0s
CI / Unit & Component Tests (push) Failing after 2m59s
CI / OCR Service Tests (push) Successful in 40s
CI / Backend Unit Tests (push) Failing after 2m55s
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>
2026-04-26 17:44:03 +02:00
Marcel
a6c8af0971 test(audit): replace null-actorId bootstrap calls with createUserForBootstrap(), increase timeouts to 10s
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>
2026-04-26 17:41:56 +02:00
Marcel
6d9910b805 refactor(audit): extract createUserForBootstrap() to make null actorId contract explicit
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>
2026-04-26 17:39:09 +02:00
Marcel
1dd6e054fc test(audit): add GROUP_MEMBERSHIP_CHANGED integration test with payload assertions
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m59s
CI / OCR Service Tests (push) Successful in 36s
CI / Backend Unit Tests (push) Failing after 2m57s
CI / Unit & Component Tests (pull_request) Failing after 3m0s
CI / OCR Service Tests (pull_request) Successful in 34s
CI / Backend Unit Tests (pull_request) Failing after 3m3s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:53:55 +02:00
Marcel
23cff1cdd7 refactor(audit): drop @DirtiesContext, add @BeforeEach, use existsByKind in wait conditions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:53:55 +02:00
Marcel
11d93919b2 refactor(audit): replace LIMIT :limit JPQL with Pageable in audit query
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:53:55 +02:00
Marcel
f6bcc4f72a refactor(audit): extract actorId() helper in UserController
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:53:55 +02:00
Marcel
f4a4436eda test(audit): add 403 permission tests for createUser, adminUpdateUser, deleteUser
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:53:55 +02:00
Marcel
1d3a3b3338 refactor(audit): extract groupChangePayload() from adminUpdateUser
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:53:55 +02:00
Marcel
77affcfb4f test(audit): integration test — create + delete user produces ordered audit entries
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m4s
CI / OCR Service Tests (pull_request) Successful in 34s
CI / Backend Unit Tests (pull_request) Failing after 3m2s
CI / Unit & Component Tests (push) Failing after 3m1s
CI / OCR Service Tests (push) Successful in 35s
CI / Backend Unit Tests (push) Failing after 3m2s
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>
2026-04-26 15:16:29 +02:00
Marcel
36529f7e11 feat(audit): add findRecentUserManagementEvents query method
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>
2026-04-26 15:16:29 +02:00
Marcel
eb8f9d4dc4 feat(audit): emit GROUP_MEMBERSHIP_CHANGED when admin updates user groups
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>
2026-04-26 15:16:29 +02:00
Marcel
a736b7399a feat(audit): emit USER_DELETED when admin removes a user
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>
2026-04-26 15:16:29 +02:00