The person-add-event link wraps a single text label, so the flex gap
never applies — only the sibling edit link (icon + text) needs it.
Removing the dead utility per the UI/UX review nit.
Refs #842
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A curator on a person's page can now seed a timeline event from that
person: a gated link to #781's /zeitstrahl/events/new?personId={id},
which prefills the person and returns to /persons/{id} on save. Hidden
for a Reader. New i18n key person_add_event in de/en/es.
Refs #842
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes#837
Makes `PersonRelationship` fully editable (type, related person, dates, notes), migrates its dates from `Integer fromYear/toYear` to `LocalDate + DatePrecision` (mirroring the #773 person pattern, ADR-039 / V76), activates the previously-dead `notes` column, and gives the Zeitstrahl's derived **Heirat** events full date precision for free.
Both Open Decisions confirmed as adopted: **no `@Version`** (last-write-wins, single-writer archive) and **`DELETE` ownership-mismatch aligned 403 → 404** (anti-enumeration, matching the new `PUT`).
## What's in it
- **V78** migrates `person_relationships.from_year/to_year` → `from_date`/`to_date` + NOT-NULL `*_date_precision` (default `UNKNOWN`); pre-check abort on corrupt years, `YYYY-01-01`/`YEAR` backfill, 5 named CHECK constraints, year columns dropped.
- **`PUT /api/persons/{id}/relationships/{relId}`** (`@RequirePermission(WRITE_ALL)`) re-runs every create invariant (self / coherence / order / reverse-PARENT_OF / duplicate) and re-flags family membership; orientation preserved per viewpoint.
- New `ErrorCode.INVALID_RELATIONSHIP_DATES` registered in all four sites (§3.6).
- `TimelineEventService` sources the derived marriage date from `SPOUSE_OF.fromDate` + precision.
- Frontend: `RelationshipDateField` (DAY/MONTH/YEAR), upsert-capable `AddRelationshipForm` (pre-fill + notes + in-flight submit lock), `RelationshipChip` Edit affordance, `updateRelationship` server action, read-view date range + notes, `formatRelationshipDateRange` helper. `api.ts` regenerated.
- Docs: ADR-044, db-orm/db-relationships diagrams, DEPLOYMENT §5 deploy note, RTM REQ-001…REQ-019.
## Requirements
All 19 EARS requirements implemented red/green and marked `Done` in `.specify/rtm.md`.
## Test plan
- **Backend** (targeted, green): `RelationshipMigrationTest` (Testcontainers pg16, 8), `RelationshipServiceTest` (22), `RelationshipControllerTest` (15), `RelationshipServiceIntegrationTest` (real DB, 10), `DerivedEventsAssemblyTest` (17), `ArchitectureTest` (14); `clean package` builds.
- **Frontend** (green): `relationshipDates.spec.ts`, `AddRelationshipForm.svelte.spec.ts`, `RelationshipChip.svelte.spec.ts`, `PersonRelationshipsCard.svelte.test.ts`, `page.server.spec.ts`, `messages.spec.ts`. `npm run check` = 798 (below the ~834 baseline); `npm run lint` clean.
## Notes for reviewers
- **Spec deviation:** the edit form was built by making `AddRelationshipForm` upsert-capable rather than a duplicate `EditRelationshipForm` (DRY); RTM rows reference `AddRelationshipForm.svelte.spec.ts`.
- `api.ts` regenerated from the live spec; only relationship-relevant hunks remain (one springdoc `PageableObject` field-reorder pruned).
- **Deploy:** V78 is one-way and not rolling-deploy-safe — stop old JAR → start new JAR (Flyway runs first); targeted `pg_restore -t person_relationships` for rollback. No maintenance window.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Marcel <marcel@familienarchiv>
Reviewed-on: #841
A partial date (e.g. "14.03.") left the hidden ISO input empty, so
saving the edit form silently cleared a stored date. PersonLifeDateField
now delegates to the shared DateInput primitive (inline format error,
calendar validation) and sets a custom validity while the error is
present, so the browser blocks native submission for both person forms.
A full clear stays submittable - that is the intentional clear path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New PersonLifeDateField (German date input + hidden ISO + DAY/MONTH/YEAR
precision select, min-h-44px, sm: side-by-side) used for birth and death
in both forms. Legacy APPROX precision seeds the select as YEAR so an
untouched save never claims DAY. Server actions send date+precision
pairs or omit both; obsolete year i18n keys removed, 9 form keys added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cards compose aria-hidden * / † glyphs in markup so screen readers only
announce the dates; PersonSummaryDTO list card stays year-shaped by
design (ADR-039). MentionDropdown subtitle wraps instead of truncating
so DAY-precision ranges fit at 320px.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes Phase 2a: geschichten/[id], persons/[id]/edit and admin/tags/[id]
page specs now provide a real createConfirmService() via render context
instead of mocking confirm.svelte. Zero confirm.svelte vi.mocks remain
across the client suite (AC#4). Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
toHaveAttribute compares by equality, so passing a regex asserted against
the literal RegExp object and failed. Assert the full title against
m.person_correspondents_search_title(...) instead — it names both persons
and avoids retyping the copy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Häufige Korrespondenten" card linked into the standalone Briefwechsel
view. Retarget each chip to the existing document search pre-filtered by
sender and receiver (/documents?senderId=A&receiverId=B), naming both
persons in a search-action title, swapping the chat-bubble icon for a
magnifier, and clarifying that the ×N badge counts shared letters in both
directions (not the unidirectional search result count).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PersonEditForm.svelte gains a G 0…G 6 select inside the {#if isPerson}
block. min-h-[44px] meets WCAG 2.5.8 / dual-audience touch target.
generationStr is initialised via $state(untrack(...)) so prop reruns
never reset an in-progress edit (same pattern as selectedType).
Both /persons/[id]/edit and /persons/new form actions read the field
without the conditional-spread idiom — generation always lands in the
PUT/POST body. G 0 is a valid family-tree-root value the spread would
silently drop, and an empty option sends null so a human can clear the
field back to "unset".
i18n adds person_label_generation / person_option_generation_unset /
person_hint_generation in de/en/es. Drops the dead stammbaum_generations
key (zero callsites after the filter-chip removal in the spec).
Tests: dropdown render + hydration in the component, generation=0/3/null
arriving in the API body in the server actions.
Refs #689
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
All route spec files that mock $lib/shared/api.server were missing
extractErrorCode from the mock factory, causing a vitest "No export defined"
error after the refactor introduced the new export.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`@sentry/sveltekit` wraps load functions and reads `event.request.method` and
`event.url.pathname`. Mock events that omitted `request` or `url` threw
`TypeError: Cannot read properties of undefined` on every invocation, silently
masking 86 test failures on main.
Two root causes fixed:
- Added `request: new Request(...)` (and `url: new URL(...)` where absent) to
all mock event objects in 14 `*.server.spec.ts` files
- Changed `;` to `&&` in the `test:coverage` npm script so a failing server
run propagates its exit code instead of being swallowed by the client run
All 576 server-project tests now pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five test files mocked $lib/shared/services/confirm.svelte under BOTH
spellings (.svelte and .svelte.js) within the same file; two more mocked
only the .svelte.js form. Both resolve to the same module URL but register
two distinct Playwright route handlers in @vitest/browser-playwright. The
cleanup logic only removes one, leaving an orphan that fires when the next
session loads the module — crashing the run with
"[birpc] rpc is closed, cannot call resolveManualMock".
This is the exact trigger fixed upstream by vitest PR #10267 (issue #9957).
Normalise every confirm.svelte mock to the no-extension form, matching
production imports and the source file basename (confirm.svelte.ts).
After this commit: 8 confirm.svelte mocks across 8 spec files, all under
one canonical ID. A meta-test (next commit) prevents the duplicate-id
pattern from reappearing.
Refs: #553 · vitest-dev/vitest#9957 · vitest-dev/vitest#10267
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Production code referenced $lib/shared/services/confirm.svelte under two
spellings — 4 files with the .js extension and one without. Standardise on
the no-extension form to match Svelte 5 rune-module convention and the
source file basename (confirm.svelte.ts).
Why this matters: vitest browser mode's @vitest/browser-playwright resolves
both spellings to the same module URL but registers a separate Playwright
route per spelling. The route-cleanup logic only unregisters the latest,
leaving an orphan that crashes the next session with
"[birpc] rpc is closed, cannot call resolveManualMock". Fixed upstream in
vitest PR #10267 (merged, not yet released). Normalising the spelling
removes the trigger from our side.
Refs: #553. Companion test-file changes follow in the next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds single-word name (one-initial) and leading-space edge cases
for the initials function.
2 new tests covering ~4 branches.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-correspondents derived from received-document senders, self-skip
branch when sender == current person, GeschichtenCard rendered when
geschichten array is non-empty, 5-entry cap on co-correspondents.
4 new tests covering ~10 branches.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four tests: discard link href, save button label, form attribute
wiring, formaction. Small focused component.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PersonEditForm: PERSON vs INSTITUTION/GROUP visibility matrix (firstName,
title, alias, birth/deathYear toggle), lastName label switch, prop
hydration of all populated fields, fallback to PERSON for unknown type,
empty-string handling for null fields. 10 tests, ~30 branches.
SegmentationTrainingCard: trainingInfo null vs populated, block count
display, button disabled-state matrix (training × tooFewBlocks ×
serviceDown), too-few-blocks and service-down hints, success message
after a mocked fetch, training history heading. 10 tests, ~25 branches.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PersonDocumentList: empty/populated, year-range derivation across
no-date/single-year/multi-year inputs, sort toggle visibility (>1 doc),
sort-direction round trip, preview-limit + show-more expansion,
title→originalFilename fallback, no-date and no-location branches.
persons/new: PERSON vs INSTITUTION/GROUP visibility matrix
(firstName/alias/life-year fields toggle), lastName label switching
between Vorname/Nachname/Name, form-error banner, prior-form hydration,
cancel link href, fallback to PERSON for unknown personType.
24 tests across two files, hitting the 32+28 = 60 branches at the top
of the issue's leverage list.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CorrespondentSuggestionsDropdown: empty list still renders the static
heading and 'Alle Korrespondenten' row, populated rows when not loading,
loading hides correspondent rows, initials fallback (lastName-only when
firstName is null), click + keyboard selection, Escape closes.
PersonCard: full matrix of conditional UI — title visibility for PERSON
vs non-PERSON, avatar initials path (firstName+lastName vs lastName-only
fallback), PersonTypeBadge presence for non-PERSON types, alias, life
dates, notes, and the canWrite=true/false branches that gate the edit
link (Nora's authorization-rendering rule).
21 tests covering ~50 branches.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The /persons/[id] +page.server.ts now fetches geschichten in parallel with
the other endpoints. Each test in this spec mocks the typed-client's GET
call sequentially, so each chain needs one extra resolved value.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Person detail (/persons/[id]):
- Server load fetches GET /api/geschichten?status=PUBLISHED&personId={id}
in parallel with the existing person/document queries.
- Renders <GeschichtenCard> below the received-documents list when the
person has at least one published story.
Document detail (/documents/[id]):
- Server load adds the same parallel call with documentId={id}.
- DocumentTopBar gains geschichten + canBlogWrite props that flow through
to DocumentMetadataDrawer.
- DocumentMetadataDrawer's grid expands to lg:grid-cols-4 when the
Geschichten column should appear (stories exist OR user can author),
and shows "+ Geschichte anhängen" / "Alle anzeigen" links following the
>= 3-story threshold from issue comment #5758.
Co-Authored-By: Claude Opus 4.7 <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>
- /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>
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>
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>
validatePersonFields now returns a PersonValidationKey instead of a
hardcoded German string. resolveValidationMessage() translates the key
through Paraglide so English and Spanish locale users no longer see
German error text. Adds validation_last_name_required and
validation_first_name_required to all three message files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes four independent PersonType type declarations and the duplicated
TYPES/PERSON_TYPES arrays. normalizePersonType moves from the edit route
module into the shared lib so page.server.test.ts no longer imports from a
route. Both server actions now use normalizePersonType for personType
extraction instead of an inline type cast.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New src/lib/person-validation.ts exports validatePersonFields (pure function)
- 8 unit tests covering: valid PERSON, lastName missing/undefined,
firstName missing/undefined for PERSON, non-PERSON types without firstName
- Both edit and new-person server actions now call the shared helper instead
of inline if-chains, making the logic testable and non-duplicated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests now import from production code instead of a local copy, giving real
regression protection if the inline logic is changed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>