When PersonService renames a person while a transcriber is editing a
block that mentions them, the block-save endpoint returns 409 (carrying
the new ErrorCode.PERSON_RENAME_CONFLICT from PR-A). saveBlock now:
1. Refetches the latest server snapshot of the block.
2. Calls mergeBlockOnConflict to combine: server's mentionedPersons
(post-rename displayNames win) + transcriber's unsaved text + any
local-only mentions added since the last save.
3. Updates the local block state with the merged result.
4. Re-throws so the autosave indicator surfaces the conflict and the
pending payload is preserved for retry (B12).
The merge logic is a pure function so it can be unit-tested in
isolation and reused for any future conflict-resolution scenarios.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Locks in the behaviour added with the saveFn signature widening: a
rejected save keeps the in-flight payload around so handleRetry resends
it without the caller having to re-pass anything.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TranscriptionBlockData now carries mentionedPersons (matches backend
schema added in PR-A).
- useBlockAutoSave.saveFn signature widens to (blockId, text, mentions);
pendingMentions is tracked alongside pendingTexts and is preserved on
failure so a retry resends the in-flight payload (B12).
- TranscriptionBlock.svelte renders <PersonMentionEditor>, exposing the
textarea node back through a captureTextarea callback so the existing
quote-selection feature still works.
- saveBlock in routes/documents/[id]/+page.svelte forwards mentions on
PUT.
- flushOnUnload sends mentions in the keepalive payload too.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors MentionEditor for users but searches /api/persons?q=, allows
multi-word queries (delegated to detectPersonMention), displays life
dates next to each result, and uses min-h-[44px] rows for WCAG 2.2 AA
touch targets. Selection writes both the @DisplayName text and a
{personId, displayName} sidecar entry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the 3 keys mandated by the plan (open_link, hover_hint, load_error)
plus the editor's popup_empty + btn_label so PersonMentionEditor mirrors
the existing user-mention editor's i18n pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment mentions stop at a space; person mentions must accept spaces
because historical display names are commonly multi-word.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
openapi-typescript regenerated against the dev backend now exposes:
- components.schemas.PersonMention with personId + displayName
- TranscriptionBlock and CreateTranscriptionBlockDTO/UpdateTranscriptionBlockDTO
carry the optional mentionedPersons array
- (No new path entries: hover-card and typeahead reuse existing endpoints
GET /api/persons, GET /api/persons/{id}, GET /api/persons/{id}/relationships.)
Sealed inside PR-A so the frontend PR-B can import the new types from main
without rebasing across an unrelated regen. Per Tobias' chain-tightening
note in the consolidation summary.
Refs #362
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the structured error code returned when a rename rolls back because a
referenced transcription block was edited concurrently (OptimisticLockException
on transcription_blocks.version). Mirrors the contract in
frontend src/lib/errors.ts and adds the localised message keys
error_person_rename_conflict in de/en/es so the UI surfaces a retry hint
instead of a generic 500.
The actual translation of OptimisticLockException → DomainException
(PERSON_RENAME_CONFLICT) lands in the next commit alongside the integration
test that proves the rollback semantics.
Refs #362
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>
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>
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 @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 @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>
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>
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>
- /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>
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>
- 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>
- 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>
- /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>
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>
- 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>
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>
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>