Commit Graph

1621 Commits

Author SHA1 Message Date
Marcel
1f62f9eda8 fix(stammbaum): WCAG min font-size and 44px touch targets
Raise chip labels from 10px to 12px (text-xs) in StammbaumCard,
StammbaumSidePanel and StammbaumTree SVG text. Widen zoom buttons
from 32px to 44px for senior-audience touch targets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 10:26:56 +02:00
Marcel
9a33d1ae71 fix(stammbaum): i18n for year-range labels in StammbaumCard
Replaces hardcoded German strings "ab {from}" / "bis {to}" in yearRange()
with parameterized Paraglide keys relation_year_from / relation_year_to,
added to all three message files (de/en/es).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 10:23:49 +02:00
Marcel
d6a5cdc41b fix(stammbaum): structured error codes in RelationshipController
getRelationshipBetween now throws DomainException with RELATIONSHIP_NOT_FOUND
instead of ResponseStatusException, so the frontend receives a typed error code.
Removed redundant validateRelationType() guard — RelationshipService.parseType()
already handles this with the same DomainException/VALIDATION_ERROR path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 10:19:09 +02:00
Marcel
dcdf270bf6 fix(stammbaum): resolve persons via PersonService in RelationshipInferenceService
Removes direct PersonRepository injection from the relationship domain,
routing cross-domain person resolution through PersonService.getAllById()
per the layering rules.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 10:14:41 +02:00
Marcel
a57fd81c5f style(stammbaum): widen side panel to 320px so longer names don't clip
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m7s
CI / OCR Service Tests (push) Successful in 39s
CI / Backend Unit Tests (push) Failing after 3m3s
CI / Unit & Component Tests (pull_request) Failing after 3m16s
CI / OCR Service Tests (pull_request) Successful in 38s
CI / Backend Unit Tests (pull_request) Failing after 2m59s
The 268px width came from the spec mock; real names plus the
relationship pill ("Eugenie de Gruyter" + "Elternteil") need more
breathing room.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 08:36:01 +02:00
Marcel
800bddd604 fix(stammbaum): iterative generation + spouse-adjacent block layout
Two distinct bugs surfaced once a 3-generation tree was loaded
(Walter+Eugenie → Hans+Clara, Hans married to Hilde with child Lili):

1. Generation BFS was non-iterative. Hilde was visited as a "root"
   first, assigning Lili = gen 1, then Hilde was pulled to gen 1 to
   match her spouse Hans — but Lili's depth was never recomputed,
   leaving her on the same row as her parents. Replaced the BFS with
   an iterative longest-path assignment that re-runs (max parent gen
   + 1) and the spouse-shared-row rule together until stable.

2. No spouse adjacency. Hilde (no parents in the graph) ended up in
   her own block on the far left, with Hans + Clara to her right and
   the spouse line drawn straight across Clara's box. Replaced the
   per-parent-set grouping with a block model:
     - sibling-blocks group children of the same parent set
     - loose spouses attach on the outer edge of their partner's block
     - dual-loose spouse pairs merge into one 2-person block
     - each block is centred so its parented members' average sits
       exactly under the parent midpoint, keeping all connectors at 90°

Adds a regression test for the full Walter/Eugenie/Hans/Clara/Hilde/
Lili scenario (Lili in a deeper row, Hans+Hilde adjacent, no slanted
segments) and rewrites the viewBox tests to be position-agnostic via
a rect-centroid helper that reads the per-node `<g transform>`.

Tracked the eventual move to dagre (multi-marriage / cross-cousin /
~50+ nodes) in #361.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 08:35:32 +02:00
Marcel
14ac63981c feat(stammbaum): inline add-relationship form in side panel
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m21s
CI / OCR Service Tests (push) Successful in 32s
CI / Backend Unit Tests (push) Failing after 3m7s
CI / Unit & Component Tests (pull_request) Failing after 3m17s
CI / OCR Service Tests (pull_request) Successful in 38s
CI / Backend Unit Tests (pull_request) Failing after 3m4s
Implements the inline-edit affordance from
docs/specs/stammbaum-tree-spec.html (section 3): a low-opacity
"+ Beziehung hinzufügen" button below the direct relationships list
expands into a compact form (type select, person typeahead,
optional Von/Bis Jahr inputs, Abbrechen + Speichern). On save the
form POSTs to /api/persons/{id}/relationships, reloads the panel's
own data, and calls invalidateAll() so the tree picks up the new
edge without a hard refresh.

The panel takes a new canWrite prop, plumbed through from the
+layout.server.ts data already exposed on page.data.

Also pins the /stammbaum canvas to the viewport (-my-6 cancels
<main>'s py-6, h-[calc(100dvh-4.25rem)] subtracts the navbar) so
the page no longer overflows below the fold.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 21:44:51 +02:00
Marcel
09d1a7a85c feat(stammbaum): tree visual polish + parent-midpoint layout
Aligns the SVG tree with docs/specs/stammbaum-tree-spec.html:

- Node outline: var(--c-primary) at stroke-width=1.5 (was the much
  paler --c-line at 1) and selected text uses var(--c-primary-fg)
  so it remains readable on the dark/light primary fill
- Spouse line and parent-child line now share the same stroke style;
  spouse keeps the midpoint dot (radius bumped to 4.5 per spec)
- When two parents are connected by SPOUSE_OF, draw a single shared
  parent-pair → child line from the spouse midpoint instead of two
  diverging lines
- ViewBox: enforces a 1200×800 minimum and centers the content so a
  single node no longer scales up to fill the whole canvas in the
  top-left
- Children are positioned at the average of their parents' x and
  packed left-to-right per row, keeping connectors close to vertical

Adds component tests for the centring, the shared parent-pair link
(verified vertical), and the fallback to two lines when parents are
not spouses.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 21:44:19 +02:00
Marcel
7b3d3f8b36 feat(documents): inline relationship pills next to person names
Replaces the standalone "Beziehung" badge at the bottom of the
metadata drawer's Personen column with small inline pills attached
to each personCard — sender gets labelFromA, the single receiver
gets labelFromB. Matches docs/specs/stammbaum-doc-badge-spec.html.

Drops the now-unused RelationshipBadge component.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 21:43:20 +02:00
Marcel
82cf47ae6d style(stammbaum): tighten vertical rhythm around relationship cards
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m9s
CI / OCR Service Tests (push) Successful in 39s
CI / Backend Unit Tests (push) Failing after 3m12s
CI / Unit & Component Tests (pull_request) Failing after 3m17s
CI / OCR Service Tests (pull_request) Successful in 40s
CI / Backend Unit Tests (pull_request) Failing after 3m8s
- /stammbaum: drop the global py-6 top gap so the page header butts
  up against the navbar, matching its full-bleed canvas layout
- person detail: add mt-6 around the document lists so they don't
  sit flush against the Beziehungen card
- person edit: add mt-6 to PersonMergePanel so the merge box doesn't
  collide with the StammbaumCard above it

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 19:48:19 +02:00
Marcel
6f867e5548 fix(stammbaum): drop inferred relationships that are already direct
A spouse listed as a direct PersonRelationship was also being
emitted as an inferred SPOUSE chip below, so the same person
appeared twice in the Beziehungen card.

Filter the inferred list against the IDs already shown as direct
edges before slicing the top 5. Added a component test that
renders red without the filter and green with it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 19:47:49 +02:00
Marcel
c103aafa6d 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-27 19:47:19 +02:00
Marcel
1ff8393ad6 test(stammbaum): E2E spec + extend person load mock
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m6s
CI / OCR Service Tests (push) Successful in 44s
CI / Backend Unit Tests (push) Failing after 3m9s
CI / Unit & Component Tests (pull_request) Failing after 3m19s
CI / OCR Service Tests (pull_request) Successful in 35s
CI / Backend Unit Tests (pull_request) Failing after 2m58s
- 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-27 15:07:37 +02:00
Marcel
5aaac849c2 feat(stammbaum): person detail Beziehungen card
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m7s
CI / OCR Service Tests (push) Successful in 31s
CI / Backend Unit Tests (push) Failing after 3m2s
- 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-27 14:58:43 +02:00
Marcel
709a9d6224 feat(stammbaum): /stammbaum page — SVG tree + side panel + empty state
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
- /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-27 14:56:41 +02:00
Marcel
ac2f1070aa feat(stammbaum): person edit Stammbaum & Beziehungen card
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m5s
CI / OCR Service Tests (push) Successful in 29s
CI / Backend Unit Tests (push) Has started running
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-27 14:51:54 +02:00
Marcel
eea3035ddc feat(stammbaum): show inferred relationship in the document drawer
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m5s
CI / OCR Service Tests (push) Successful in 37s
CI / Backend Unit Tests (push) Has been cancelled
- 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-27 14:45:38 +02:00
Marcel
a75a818adf feat(stammbaum): swap nav slot from /briefwechsel to /stammbaum
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
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-27 14:41:25 +02:00
Marcel
8016cb6905 feat(stammbaum): add i18n keys (de/en/es) + mirror error codes
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
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-27 14:40:25 +02:00
Marcel
0e23b49b7a chore(stammbaum): regenerate TS API types for relationship endpoints
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m8s
CI / OCR Service Tests (push) Successful in 32s
CI / Backend Unit Tests (push) Has been cancelled
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-27 14:36:11 +02:00
Marcel
c4a050da86 test(stammbaum): integration tests for relationship constraints
Some checks failed
CI / Unit & Component Tests (push) Failing after 4m10s
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
@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-27 14:28:48 +02:00
Marcel
19366f78e4 feat(stammbaum): RelationshipController for the Stammbaum API
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m6s
CI / OCR Service Tests (push) Successful in 33s
CI / Backend Unit Tests (push) Has been cancelled
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-27 14:22:34 +02:00
Marcel
616089e65e feat(stammbaum): RelationshipService + family_member toggle (TDD)
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
- 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-27 14:20:15 +02:00
Marcel
efa020eceb feat(stammbaum): inference service with BFS + LABEL_MAP (TDD)
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m7s
CI / OCR Service Tests (push) Successful in 38s
CI / Backend Unit Tests (push) Has been cancelled
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-27 14:12:51 +02:00
Marcel
d77f0e2203 feat(stammbaum): add backend data layer for family network
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m0s
CI / OCR Service Tests (push) Successful in 37s
CI / Backend Unit Tests (push) Failing after 1m52s
- 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-27 14:05:49 +02:00
Marcel
759012f696 feat(stammbaum): add V54 migration for family network
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m7s
CI / Backend Unit Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
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-27 14:02:27 +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