Two regression guards on the "iff different" semantics in updatePerson.
Person.alias and Person.notes are not part of getDisplayName() — they live
outside DisplayNameFormatter — so changing only those fields must not fire
PersonDisplayNameChangedEvent. If a future refactor accidentally pulls
either field into the display name (or trips the comparison), these tests
catch it before transcription blocks get rewritten with stale "@OldAlias"
text.
Refs #362
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PersonService now emits a domain event whenever Person.getDisplayName()
flips during an update. The snapshot is taken before the setter chain so we
compare like-for-like against the post-save value, and the event only
publishes when the two strings differ.
The test captures the published event via ArgumentCaptor and asserts the
title flip from "Herr" to "Frau" reaches the publisher with the correct
personId, oldDisplayName, and newDisplayName. Title participates in
DisplayNameFormatter, so this is the canonical case for "rename triggered
by something other than first/last name."
Implements PR-A tasks 9 and 10 as one red→green cycle (the test drove the
production change). Subsequent commits cover the negative cases (alias /
notes only) and the propagation listener that consumes the event.
Refs #362
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Regression guard for the @NotNull on PersonMention.personId paired with
@Valid on the DTO field. The wiring was added in the previous commit; this
test ensures dropping either annotation in the future causes a loud test
failure rather than silently allowing payloads with no personId to reach
the service layer (where the listener relies on the UUID being present).
Refs #362
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires @Valid on the @RequestBody parameter of TranscriptionBlockController's
createBlock and updateBlock methods so JSR-303 actually fires for incoming
DTOs. With @Valid on the field-level mentionedPersons in the DTO (added in
the previous commit), Jakarta validation now recurses into each
PersonMention element and rejects displayName values past the @Size(max=200)
ceiling.
The test posts a 201-char displayName and asserts the global handler maps
the resulting MethodArgumentNotValidException to 400 + code:VALIDATION_ERROR.
Refs #362
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CreateTranscriptionBlockDTO and UpdateTranscriptionBlockDTO gain a
List<PersonMention> mentionedPersons field. @Valid is on the field itself,
not just on the controller method, so JSR-303 recurses into the list
elements when the controller boundary calls @Valid on the @RequestBody. The
collection defaults to an empty ArrayList via @Builder.Default; existing
constructor call sites in TranscriptionServiceTest are extended with
List.of() to match the new @AllArgsConstructor signature.
The controller-side @Valid wiring lands in the next commit alongside the
length-201 validation test.
Refs #362
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@DataJpaTest + Testcontainers exercises the V56 migration plus the
@ElementCollection wiring end-to-end. Saves a block with two PersonMention
entries, clears the persistence context, reloads, asserts both entries
return with their personId + displayName intact. Second test guards the
@Builder.Default — a block without explicit mentions reloads with an empty
list, not null.
Refs #362
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ElementCollection(LAZY) on List<PersonMention>, mapped to V56's
transcription_block_mentioned_persons via explicit @CollectionTable that
matches the migration name byte-for-byte (immune to Hibernate naming-strategy
changes). @Builder.Default keeps the field initialized to an empty list, so
existing transcription block construction stays untouched.
Refs #362
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Carries personId + oldDisplayName + newDisplayName so transcription-side
listeners can rewrite block.text and sidecar entries when a person is
renamed. First custom application event in this codebase — the only prior
@EventListener consumes Spring's built-in ApplicationReadyEvent. Class doc
sets the convention for future cross-domain decoupling.
Refs #362
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Value object held in TranscriptionBlock.mentionedPersons via @ElementCollection.
Carries the personId UUID (so renamed persons can be located) and the
displayName text (so block.text rewrites match exactly via "@" + name). Both
fields are non-null; displayName capped at 200 chars to match the V56 column
and bound the rename propagation cost.
Refs #362
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Child table for @-mentions inside transcription block text. Each row binds
one block to one person via personId + displayName; the literal "@DisplayName"
stays in block.text. No FK on person_id so deleted persons degrade gracefully
to plain unlinked text rather than cascade-deleting the block. Indexed on
person_id for the future "blocks mentioning person X" query and on block_id
for the @ElementCollection load.
Schema choice diverges from document_comments.comment_mentions (many-to-many
to AppUser): the latter cascades, this one degrades. Mirrors the established
UserGroup.permissions / group_permissions @ElementCollection pattern.
Refs #362
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Spring deserializes the enum directly; invalid values are caught by the
HttpMessageNotReadableException → 400 handler added in 99d00537, returning
a structured VALIDATION_ERROR. The manual parseType() helper is therefore
redundant and removed. Tests updated to construct requests with the enum.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the 86-line duplicated inline add-relationship form with
<AddRelationshipForm onSubmit={handleAddRelationship}>. The {#key node.id}
wrapper resets the form's open state when the selected tree node changes.
Year inputs now have <label> elements (WCAG 1.3.1) via the shared component.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When onSubmit is provided the form has no server action and calls the
callback with typed RelFormData instead. Uses a shared {#snippet} for
the form body so the two submission paths share one template.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Proves the in-memory filter correctly drops edges where one Person is
not in findAllFamilyMembers(), preventing non-family relationships from
leaking into the graph.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CSS box-shadow rings (focus-visible:ring-*) are invisible inside SVG.
Replace with a conditional <rect> drawn at -3px offset that renders in
all browsers. Name font-size bumped from 14 to 16px for the 60+
transcriber audience (WCAG readability, Leonie medium concerns).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The focus deep-link is a one-time load param — $derived + $effect caused
a deferred write that left the node unselected on first paint. Initialising
$state inline reads the URL once at component mount with no reactive cycle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8 hops covers great-grandparents ↔ great-great-grandchildren and second
cousins — the practical horizon for a 1899–1950 archive. Prevents future
blind tuning of the constant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All four tests skipped with a reference to issue #363 which tracks
adding the Playwright Chromium install + Docker Compose startup step
to the CI workflow. Remove the skip once #363 is resolved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The <span> in the derived-relationships list is replaced with <a href>
so keyboard and pointer users can navigate directly from the edit card,
consistent with PersonRelationshipsCard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
h-8 w-8 (32px) replaced with h-11 w-11 (44px) to meet the minimum
touch target for the 60+ transcriber audience. Test added to prevent
regression.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hardcoded 'Stammbaum & Beziehungen' heading replaced with
m.stammbaum_relationships_heading(); new key added to all
three message files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes local duplicates of the switch-statement label logic already
exported from $lib/relationshipLabels.ts. Adds two direction-sensitive
tests proving the Elternteil-von / Kind-von branch is covered.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses @markus/@nora suggestion: makes explicit that the missing
@RequirePermission on read endpoints is intentional — all authenticated
family members may read the family graph; unauthenticated access is still
blocked by Spring Security's anyRequest().authenticated() rule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses @leonie blocker: zoom buttons in /stammbaum had no visible focus
indicator for keyboard users. Applied focus-visible:ring-2 focus-visible:ring-focus-ring
focus-visible:outline-none matching the pattern used on nav links.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses @sara blocker: RelationshipControllerTest now has 6 tests covering
the two previously untested @RequirePermission(WRITE_ALL) endpoints. Prevents
silent permission regression if the controller is refactored.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses @sara blocker: documents that Spring Security's anyRequest().authenticated()
guards these read endpoints and provides regression protection against accidental
@PermitAll additions in future.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses @felix blocker: removes the verbatim duplicate switch+2-line helper
from StammbaumCard.svelte and StammbaumSidePanel.svelte; both now import from
the shared $lib/relationshipLabels helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses @felix blocker: both functions were duplicated verbatim in
StammbaumCard.svelte and StammbaumSidePanel.svelte. Now exported from
$lib/relationshipLabels.ts with perspectivePersonId as an explicit param.
8 unit tests added (red→green).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split StammbaumCard from 366 to 196 lines by extracting:
- RelationshipChip.svelte — single relationship list item with optional delete
- AddRelationshipForm.svelte — self-contained add-relationship form with open/close state
Both components have browser-mode spec tests covering rendering and interaction.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>