feat(stammbaum): show maiden name (geb. Schmidt) below person name in tree and side panel #364

Open
opened 2026-04-28 18:34:39 +02:00 by marcel · 11 comments
Owner

Goal

Display the maiden name as geb. Schmidt below the person's display name in the Stammbaum tree nodes and the side-panel relationship list. The data already exists as a PersonNameAlias with type = MAIDEN_NAME.

Where it appears

  • Stammbaum graph nodesStammbaumCard / the D3 force graph node labels
  • StammbaumSidePanel — the relationship list entries (inferred relationships)
  • PersonRelationshipsCard — the direct relationship chips already show displayName; maiden name would appear there too if a PersonNodeDTO is used

Backend changes

1. Add maidenName to PersonNodeDTO

public record PersonNodeDTO(
        UUID id,
        String displayName,
        String maidenName,      // nullable — only set when MAIDEN_NAME alias exists
        Integer birthYear,
        Integer deathYear,
        boolean familyMember
) {}

2. Avoid N+1 — single batch query

Person.nameAliases is lazy-loaded. Calling it per-person in a loop fires one query per person.

Instead, after loading the list of family members, do one extra query:

// PersonNameAliasRepository
List<PersonNameAlias> findByPersonIdInAndType(Collection<UUID> personIds, PersonNameAliasType type);

Then build a Map<UUID, String> and look up during PersonNodeDTO construction. Net cost: +1 query for the entire network load.

Apply the same pattern wherever PersonNodeDTO is built:

  • RelationshipService.getFamilyNetwork()
  • RelationshipService.getInferredRelationships() (via RelationshipInferenceService)

3. Regenerate TypeScript types

cd frontend && npm run generate:api

Frontend changes

Anywhere a person's name is rendered in a Stammbaum context, add:

{#if node.maidenName}
  <span class="text-xs text-ink-2 font-sans">geb. {node.maidenName}</span>
{/if}

Components to update:

  • StammbaumSidePanel.svelte — relationship list entries
  • The D3 node label renderer in StammbaumPage.svelte (SVG <text> element or foreignObject)
  • PersonRelationshipsCard.svelte — relationship chip sub-labels if applicable

Add i18n key person_maiden_name_prefix = "geb." (de), "née" (en/es).

Acceptance criteria

  • A person with a MAIDEN_NAME alias shows geb. {lastName} below their name in the Stammbaum tree
  • A person without a maiden name alias is unaffected
  • GET /api/network response includes maidenName field (nullable) on each node
  • No N+1 query regression — verified by checking Hibernate SQL log for a tree with 5+ persons
  • Frontend type-checks pass (npm run check)
## Goal Display the maiden name as `geb. Schmidt` below the person's display name in the Stammbaum tree nodes and the side-panel relationship list. The data already exists as a `PersonNameAlias` with `type = MAIDEN_NAME`. ## Where it appears - **Stammbaum graph nodes** — `StammbaumCard` / the D3 force graph node labels - **StammbaumSidePanel** — the relationship list entries (inferred relationships) - **PersonRelationshipsCard** — the direct relationship chips already show `displayName`; maiden name would appear there too if a `PersonNodeDTO` is used ## Backend changes ### 1. Add `maidenName` to `PersonNodeDTO` ```java public record PersonNodeDTO( UUID id, String displayName, String maidenName, // nullable — only set when MAIDEN_NAME alias exists Integer birthYear, Integer deathYear, boolean familyMember ) {} ``` ### 2. Avoid N+1 — single batch query `Person.nameAliases` is lazy-loaded. Calling it per-person in a loop fires one query per person. Instead, after loading the list of family members, do one extra query: ```java // PersonNameAliasRepository List<PersonNameAlias> findByPersonIdInAndType(Collection<UUID> personIds, PersonNameAliasType type); ``` Then build a `Map<UUID, String>` and look up during `PersonNodeDTO` construction. Net cost: +1 query for the entire network load. Apply the same pattern wherever `PersonNodeDTO` is built: - `RelationshipService.getFamilyNetwork()` - `RelationshipService.getInferredRelationships()` (via `RelationshipInferenceService`) ### 3. Regenerate TypeScript types ```bash cd frontend && npm run generate:api ``` ## Frontend changes Anywhere a person's name is rendered in a Stammbaum context, add: ```svelte {#if node.maidenName} <span class="text-xs text-ink-2 font-sans">geb. {node.maidenName}</span> {/if} ``` Components to update: - `StammbaumSidePanel.svelte` — relationship list entries - The D3 node label renderer in `StammbaumPage.svelte` (SVG `<text>` element or foreignObject) - `PersonRelationshipsCard.svelte` — relationship chip sub-labels if applicable Add i18n key `person_maiden_name_prefix` = `"geb."` (de), `"née"` (en/es). ## Acceptance criteria - [ ] A person with a `MAIDEN_NAME` alias shows `geb. {lastName}` below their name in the Stammbaum tree - [ ] A person without a maiden name alias is unaffected - [ ] `GET /api/network` response includes `maidenName` field (nullable) on each node - [ ] No N+1 query regression — verified by checking Hibernate SQL log for a tree with 5+ persons - [ ] Frontend type-checks pass (`npm run check`)
marcel added the P2-mediumfeatureperson labels 2026-04-28 18:34:46 +02:00
Author
Owner

🏛️ Markus Keller — Senior Application Architect

Observations

  • Domain boundary violation in the proposed implementation. The issue suggests calling PersonNameAliasRepository.findByPersonIdInAndType() from RelationshipService. But PersonNameAliasRepository lives in the person domain. RelationshipService already calls PersonService for cross-domain access — the pattern is established. Injecting the alias repository directly into RelationshipService would break that boundary.
  • Two PersonNodeDTO build sites. RelationshipService.getFamilyNetwork() (line 62) and RelationshipInferenceService.findAllFor() (line 98) both construct PersonNodeDTO inline. Both need updating. Since they share the same enrichment logic, they should share the same helper — not duplicate it.
  • PersonNodeDTO as a record — adding a nullable String maidenName field is clean and idiomatic. No concerns there.

Recommendations

  • Move the batch alias lookup into PersonService, not RelationshipService. Add a method like:

    // PersonService
    public Map<UUID, String> findMaidenNames(Collection<UUID> personIds) {
        return aliasRepository.findByPersonIdInAndType(personIds, PersonNameAliasType.MAIDEN_NAME)
            .stream()
            .collect(Collectors.toMap(
                a -> a.getPerson().getId(),
                PersonNameAlias::getLastName,
                (a, b) -> a  // keep first if duplicates exist
            ));
    }
    

    Then RelationshipService calls personService.findMaidenNames(familyIds) — cross-domain access through the service layer, as the architecture requires.

  • Extract a private helper in the relationship package to avoid duplicating the DTO construction logic:

    private PersonNodeDTO toNode(Person p, Map<UUID, String> maidenNames) {
        return new PersonNodeDTO(p.getId(), p.getDisplayName(),
            maidenNames.get(p.getId()), p.getBirthYear(), p.getDeathYear(), true);
    }
    

    Both getFamilyNetwork() and RelationshipInferenceService.findAllFor() use it.

  • Add findByPersonIdInAndType to PersonNameAliasRepository — this is a Spring Data derived query, no custom JPQL needed:

    List<PersonNameAlias> findByPersonIdInAndType(Collection<UUID> personIds, PersonNameAliasType type);
    
  • Remember to regenerate TypeScript types after the DTO change.

## 🏛️ Markus Keller — Senior Application Architect ### Observations - **Domain boundary violation in the proposed implementation.** The issue suggests calling `PersonNameAliasRepository.findByPersonIdInAndType()` from `RelationshipService`. But `PersonNameAliasRepository` lives in the person domain. `RelationshipService` already calls `PersonService` for cross-domain access — the pattern is established. Injecting the alias repository directly into `RelationshipService` would break that boundary. - **Two `PersonNodeDTO` build sites.** `RelationshipService.getFamilyNetwork()` (line 62) and `RelationshipInferenceService.findAllFor()` (line 98) both construct `PersonNodeDTO` inline. Both need updating. Since they share the same enrichment logic, they should share the same helper — not duplicate it. - **`PersonNodeDTO` as a record** — adding a nullable `String maidenName` field is clean and idiomatic. No concerns there. ### Recommendations - **Move the batch alias lookup into `PersonService`**, not `RelationshipService`. Add a method like: ```java // PersonService public Map<UUID, String> findMaidenNames(Collection<UUID> personIds) { return aliasRepository.findByPersonIdInAndType(personIds, PersonNameAliasType.MAIDEN_NAME) .stream() .collect(Collectors.toMap( a -> a.getPerson().getId(), PersonNameAlias::getLastName, (a, b) -> a // keep first if duplicates exist )); } ``` Then `RelationshipService` calls `personService.findMaidenNames(familyIds)` — cross-domain access through the service layer, as the architecture requires. - **Extract a private helper** in the relationship package to avoid duplicating the DTO construction logic: ```java private PersonNodeDTO toNode(Person p, Map<UUID, String> maidenNames) { return new PersonNodeDTO(p.getId(), p.getDisplayName(), maidenNames.get(p.getId()), p.getBirthYear(), p.getDeathYear(), true); } ``` Both `getFamilyNetwork()` and `RelationshipInferenceService.findAllFor()` use it. - **Add `findByPersonIdInAndType` to `PersonNameAliasRepository`** — this is a Spring Data derived query, no custom JPQL needed: ```java List<PersonNameAlias> findByPersonIdInAndType(Collection<UUID> personIds, PersonNameAliasType type); ``` - Remember to regenerate TypeScript types after the DTO change.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Observations

  • SVG layout blocker. StammbaumTree.svelte (not StammbaumPage.svelte — that file doesn't exist) uses NODE_H = 56. The existing two text lines sit at y = NODE_H/2 - 6 = 22 (name) and y = NODE_H/2 + 12 = 40 (dates). A third line for maiden name at y ≈ 52–54 would overflow the 56px box. The issue proposes adding a maiden name line without addressing this constraint. Decide before implementing: either (a) increase NODE_H to ~76, (b) render maiden name only in the side panel, not on the SVG node card itself, or (c) use foreignObject. Option (b) is the simplest and avoids a layout algorithm overhaul.

  • StammbaumSidePanel relationship list entries use RelationshipDTO, not PersonNodeDTO. The side panel's relationship list renders names via otherName(rel, node.id) which reads personName/relatedPersonName from RelationshipDTO. Adding maidenName to PersonNodeDTO will NOT make maiden names appear there. The inferred relationships section (topDerived) uses derived.person.displayName — that IS PersonNodeDTO, so maiden name there is achievable. Direct relationship entries are out of scope unless RelationshipDTO is also updated.

  • Existing i18n key. person_born_name_prefix = "geb." already exists in messages/de.json (line 431). Adding a new person_maiden_name_prefix key with the same value creates a duplicate. Reuse person_born_name_prefix or justify the distinction.

  • Component naming. The issue mentions "D3 force graph" — the current implementation uses a custom SVG layout in StammbaumTree.svelte with no D3 dependency. The issue should reference StammbaumTree.svelte specifically.

Recommendations

  • Write the failing test first: should_render_maiden_name_below_display_name_when_alias_exists() in StammbaumTree.svelte.test.ts (red), then implement. Same for should_not_render_maiden_name_line_when_absent().
  • For PersonNodeDTO construction in RelationshipInferenceService (line 98), the maiden name lookup map must be built before the loop — not inside it.
  • In the side panel, scope the maiden name display to: (a) the node.maidenName in the panel header (easy — just add a <p> below the <h2>), and (b) inferred relationship entries via derived.person.maidenName. Remove "relationship list entries" from scope unless RelationshipDTO is extended.
  • Update the aria-label on SVG tree nodes to include maiden name:
    aria-label="{node.displayName}{node.maidenName ? `, geb. ${node.maidenName}` : ''}{...}"
    
## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Observations - **SVG layout blocker.** `StammbaumTree.svelte` (not `StammbaumPage.svelte` — that file doesn't exist) uses `NODE_H = 56`. The existing two text lines sit at `y = NODE_H/2 - 6 = 22` (name) and `y = NODE_H/2 + 12 = 40` (dates). A third line for maiden name at `y ≈ 52–54` would overflow the 56px box. The issue proposes adding a maiden name line without addressing this constraint. **Decide before implementing:** either (a) increase `NODE_H` to ~76, (b) render maiden name only in the side panel, not on the SVG node card itself, or (c) use `foreignObject`. Option (b) is the simplest and avoids a layout algorithm overhaul. - **`StammbaumSidePanel` relationship list entries use `RelationshipDTO`, not `PersonNodeDTO`.** The side panel's relationship list renders names via `otherName(rel, node.id)` which reads `personName`/`relatedPersonName` from `RelationshipDTO`. Adding `maidenName` to `PersonNodeDTO` will NOT make maiden names appear there. The inferred relationships section (`topDerived`) uses `derived.person.displayName` — that IS `PersonNodeDTO`, so maiden name there is achievable. Direct relationship entries are out of scope unless `RelationshipDTO` is also updated. - **Existing i18n key.** `person_born_name_prefix` = `"geb."` already exists in `messages/de.json` (line 431). Adding a new `person_maiden_name_prefix` key with the same value creates a duplicate. Reuse `person_born_name_prefix` or justify the distinction. - **Component naming.** The issue mentions "D3 force graph" — the current implementation uses a custom SVG layout in `StammbaumTree.svelte` with no D3 dependency. The issue should reference `StammbaumTree.svelte` specifically. ### Recommendations - Write the failing test first: `should_render_maiden_name_below_display_name_when_alias_exists()` in `StammbaumTree.svelte.test.ts` (red), then implement. Same for `should_not_render_maiden_name_line_when_absent()`. - For `PersonNodeDTO` construction in `RelationshipInferenceService` (line 98), the maiden name lookup map must be built **before** the loop — not inside it. - In the side panel, scope the maiden name display to: (a) the `node.maidenName` in the panel header (easy — just add a `<p>` below the `<h2>`), and (b) inferred relationship entries via `derived.person.maidenName`. Remove "relationship list entries" from scope unless `RelationshipDTO` is extended. - Update the `aria-label` on SVG tree nodes to include maiden name: ```svelte aria-label="{node.displayName}{node.maidenName ? `, geb. ${node.maidenName}` : ''}{...}" ```
Author
Owner

🔒 Nora Steiner — Application Security Engineer

Observations

  • maidenName is historical personal data of deceased persons (1899–1950). In the project's context (family-only archive, private deployment), this is low-risk. The data is already stored and displayed in other parts of the app (PersonNameAlias is already exposed on the person detail page). No new surface area for privacy concerns.
  • The only endpoint affected is GET /api/network. It's already authenticated. maidenName: null for persons without an alias, so no empty-string leakage.
  • The new findByPersonIdInAndType is a Spring Data derived query — parameterized by design, injection-safe. No custom JPQL string concatenation.
  • No new write path. This is display-only. No deserialization, no input validation needed for the new field.

Recommendations

  • No security changes required. This is a clean read-only enrichment of an existing authenticated endpoint.
  • Confirm the maidenName field is not accidentally marked @Schema(requiredMode = REQUIRED) in the DTO — it must remain optional in the OpenAPI spec so TypeScript generates it as string | undefined (nullable), preventing frontend crashes when the field is absent on older cached responses.
  • If you later extend this pattern to expose maidenName via the inferred-relationships endpoint (GET /api/persons/{id}/inferred-relationships), verify the same auth guards apply there — they do today, but worth a sanity check after the change.
## 🔒 Nora Steiner — Application Security Engineer ### Observations - **`maidenName` is historical personal data of deceased persons (1899–1950).** In the project's context (family-only archive, private deployment), this is low-risk. The data is already stored and displayed in other parts of the app (`PersonNameAlias` is already exposed on the person detail page). No new surface area for privacy concerns. - **The only endpoint affected is `GET /api/network`.** It's already authenticated. `maidenName: null` for persons without an alias, so no empty-string leakage. - **The new `findByPersonIdInAndType` is a Spring Data derived query** — parameterized by design, injection-safe. No custom JPQL string concatenation. - **No new write path.** This is display-only. No deserialization, no input validation needed for the new field. ### Recommendations - No security changes required. This is a clean read-only enrichment of an existing authenticated endpoint. - Confirm the `maidenName` field is not accidentally marked `@Schema(requiredMode = REQUIRED)` in the DTO — it must remain optional in the OpenAPI spec so TypeScript generates it as `string | undefined` (nullable), preventing frontend crashes when the field is absent on older cached responses. - If you later extend this pattern to expose `maidenName` via the inferred-relationships endpoint (`GET /api/persons/{id}/inferred-relationships`), verify the same auth guards apply there — they do today, but worth a sanity check after the change.
Author
Owner

🧪 Sara Holt — QA Engineer & Test Strategist

Observations

  • The N+1 acceptance criterion is manual. "Verified by checking Hibernate SQL log for a tree with 5+ persons" means running locally and eyeballing the log. This is not a regression gate — the next change can silently re-introduce the N+1 and it won't be caught. This needs an automated test.
  • Two PersonNodeDTO construction sites (RelationshipService.getFamilyNetwork() and RelationshipInferenceService.findAllFor()) means two separate unit test targets — each needs its own happy path and no-alias path test.
  • Frontend test gap: The SVG tree node label is rendered inside a raw <text> SVG element. Vitest browser tests can assert getByRole('button', { name: /geb\./ }) against the composite aria-label on the <g> element (which already includes name + years) — this is testable today once the aria-label includes the maiden name.

Recommendations

Backend — unit tests (Mockito):

// RelationshipServiceTest
@Test
void getFamilyNetwork_includes_maiden_name_when_MAIDEN_NAME_alias_exists()

@Test
void getFamilyNetwork_sets_null_maiden_name_when_no_alias_exists()

Backend — integration test (Testcontainers) for N+1:

  • Seed a 5-person family network via @Sql
  • Use Hibernate's StatementInspector or Statistics to assert exactly 2 SQL queries fire during getFamilyNetwork(): one for persons, one for aliases. This is automatable and belongs in the test suite permanently.

Frontend — vitest-browser component tests:

// StammbaumTree.svelte.test.ts
it('includes maiden name in aria-label when maidenName is set')
it('omits geb. line in aria-label when maidenName is null')

For StammbaumSidePanel: test that the panel header shows geb. Schmidt below the display name when node.maidenName is set. Use getByText('geb. Schmidt').

CI note: Add npm run check to the PR CI step if it isn't already — the acceptance criterion mentions it but it only catches issues if it runs on the branch.

## 🧪 Sara Holt — QA Engineer & Test Strategist ### Observations - **The N+1 acceptance criterion is manual.** "Verified by checking Hibernate SQL log for a tree with 5+ persons" means running locally and eyeballing the log. This is not a regression gate — the next change can silently re-introduce the N+1 and it won't be caught. This needs an automated test. - **Two `PersonNodeDTO` construction sites** (`RelationshipService.getFamilyNetwork()` and `RelationshipInferenceService.findAllFor()`) means two separate unit test targets — each needs its own happy path and no-alias path test. - **Frontend test gap:** The SVG tree node label is rendered inside a raw `<text>` SVG element. Vitest browser tests can assert `getByRole('button', { name: /geb\./ })` against the composite `aria-label` on the `<g>` element (which already includes name + years) — this is testable today once the aria-label includes the maiden name. ### Recommendations **Backend — unit tests (Mockito):** ```java // RelationshipServiceTest @Test void getFamilyNetwork_includes_maiden_name_when_MAIDEN_NAME_alias_exists() @Test void getFamilyNetwork_sets_null_maiden_name_when_no_alias_exists() ``` **Backend — integration test (Testcontainers) for N+1:** - Seed a 5-person family network via `@Sql` - Use Hibernate's `StatementInspector` or `Statistics` to assert exactly 2 SQL queries fire during `getFamilyNetwork()`: one for persons, one for aliases. This is automatable and belongs in the test suite permanently. **Frontend — vitest-browser component tests:** ```typescript // StammbaumTree.svelte.test.ts it('includes maiden name in aria-label when maidenName is set') it('omits geb. line in aria-label when maidenName is null') ``` **For `StammbaumSidePanel`:** test that the panel header shows `geb. Schmidt` below the display name when `node.maidenName` is set. Use `getByText('geb. Schmidt')`. **CI note:** Add `npm run check` to the PR CI step if it isn't already — the acceptance criterion mentions it but it only catches issues if it runs on the branch.
Author
Owner

🎨 Leonie Voss — UX Designer & Accessibility Strategist

Observations

  • SVG layout cannot fit three text lines at NODE_H=56. Current node: name at y=22, dates at y=40, box bottom at y=56. A third line at y≈52–54 clips at the border. Any font-size below 12px to compensate falls below the minimum for our senior audience (60+). This is a blocking layout issue that needs a decision before frontend work starts.

  • The side panel header is the right primary location for maiden name. StammbaumSidePanel.svelte renders the name in a <h2> with an HTML layout — adding a <p> below is straightforward and the correct semantic structure. The tree node card is the wrong place for dense text given space constraints.

  • Accessibility: SVG node aria-label is the only text a screen reader gets. Currently: "{displayName}, {birthYear}–{deathYear}". Maiden name must be included here even if it's not visible on the node face.

Recommendations

Preferred approach: side-panel first, SVG node optional

For the side panel header (no layout risk):

<h2 class="font-serif text-lg text-ink">{node.displayName}</h2>
{#if node.maidenName}
  <p class="font-sans text-xs text-ink-3">geb. {node.maidenName}</p>
{/if}

For the SVG tree node — if maiden name must appear there — increase NODE_H to 76 and add:

<text
  x={NODE_W / 2}
  y={NODE_H / 2 + 26}
  text-anchor="middle"
  font-family="sans-serif"
  font-size="12"
  fill={isSelected ? 'var(--c-primary-fg)' : 'var(--c-ink-3)'}
  opacity={isSelected ? 0.7 : 1}
>
  geb. {node.maidenName}
</text>

Note: a NODE_H increase affects the entire layout algorithm spacing — test the tree with a 10+ person family after the change.

Accessibility fix regardless of approach:

aria-label="{node.displayName}{node.maidenName ? `, geb. ${node.maidenName}` : ''}{node.birthYear || node.deathYear ? `, ${node.birthYear ?? '?'}${node.deathYear ?? ''}` : ''}"

i18n note: Use m.person_born_name_prefix() — that key already returns "geb." in de.json. No need for a new key.

Open Decisions

  • SVG tree node vs. side panel only. Showing maiden name directly on the tree node card requires increasing NODE_H (layout algorithm impact). Showing it only in the side panel header is lower risk. Which scope is intended? The decision affects NODE_H, layout spacing, and test coverage.
## 🎨 Leonie Voss — UX Designer & Accessibility Strategist ### Observations - **SVG layout cannot fit three text lines at NODE_H=56.** Current node: name at `y=22`, dates at `y=40`, box bottom at `y=56`. A third line at `y≈52–54` clips at the border. Any `font-size` below 12px to compensate falls below the minimum for our senior audience (60+). This is a **blocking layout issue** that needs a decision before frontend work starts. - **The side panel header is the right primary location for maiden name.** `StammbaumSidePanel.svelte` renders the name in a `<h2>` with an HTML layout — adding a `<p>` below is straightforward and the correct semantic structure. The tree node card is the wrong place for dense text given space constraints. - **Accessibility: SVG node `aria-label` is the only text a screen reader gets.** Currently: `"{displayName}, {birthYear}–{deathYear}"`. Maiden name must be included here even if it's not visible on the node face. ### Recommendations **Preferred approach: side-panel first, SVG node optional** For the side panel header (no layout risk): ```svelte <h2 class="font-serif text-lg text-ink">{node.displayName}</h2> {#if node.maidenName} <p class="font-sans text-xs text-ink-3">geb. {node.maidenName}</p> {/if} ``` For the SVG tree node — if maiden name must appear there — increase `NODE_H` to 76 and add: ```svelte <text x={NODE_W / 2} y={NODE_H / 2 + 26} text-anchor="middle" font-family="sans-serif" font-size="12" fill={isSelected ? 'var(--c-primary-fg)' : 'var(--c-ink-3)'} opacity={isSelected ? 0.7 : 1} > geb. {node.maidenName} </text> ``` Note: a `NODE_H` increase affects the entire layout algorithm spacing — test the tree with a 10+ person family after the change. **Accessibility fix regardless of approach:** ```svelte aria-label="{node.displayName}{node.maidenName ? `, geb. ${node.maidenName}` : ''}{node.birthYear || node.deathYear ? `, ${node.birthYear ?? '?'}–${node.deathYear ?? ''}` : ''}" ``` **i18n note:** Use `m.person_born_name_prefix()` — that key already returns `"geb."` in de.json. No need for a new key. ### Open Decisions - **SVG tree node vs. side panel only.** Showing maiden name directly on the tree node card requires increasing `NODE_H` (layout algorithm impact). Showing it only in the side panel header is lower risk. Which scope is intended? The decision affects NODE_H, layout spacing, and test coverage.
Author
Owner

📋 Elicit — Requirements Engineer

Observations

  • Component naming is imprecise. The issue references "D3 force graph node labels", "StammbaumPage.svelte", and "StammbaumCard". None of these exist. The actual rendering file is StammbaumTree.svelte (custom SVG layout, no D3 dependency). Precise file names prevent wasted developer time.

  • Scope gap: side-panel relationship list. The issue says "StammbaumSidePanel — the relationship list entries (inferred relationships)." The direct relationship entries use RelationshipDTO (specifically otherName(rel, node.id)personName/relatedPersonName) — these are not PersonNodeDTO fields. Adding maidenName to PersonNodeDTO will only affect the inferred relationships section (which does use PersonNodeDTO via InferredRelationshipWithPersonDTO), not the direct relationship entries. This scope statement will create confusion during implementation.

  • Duplicate i18n key. person_born_name_prefix = "geb." already exists in messages/de.json. Adding person_maiden_name_prefix with the same value creates a redundant key. If the semantic distinction matters (maiden name vs. birth alias), document it explicitly; otherwise reuse the existing key.

  • Missing edge case: long maiden names. What happens when maidenName is 25+ characters (e.g., "von Schönhausen-Bülow")? In the SVG tree node (160px wide), this will overflow. Truncation behavior should be specified.

  • Missing NFR: mobile viewport. The Stammbaum tree is used by the reader audience on phones. After NODE_H adjustment, the tree must still render correctly at 375px width.

Recommendations

Suggested acceptance criteria additions:

  • A person whose maidenName is > 20 characters is truncated in the SVG node card (with full name visible in the side panel)
  • The inferred relationship entries in StammbaumSidePanel show geb. {lastName} below each person name where a maiden name exists
  • The direct relationship entries in StammbaumSidePanel are not expected to show maiden names (different DTO — clarify this explicitly to avoid scope creep)

Suggested spec corrections:

  • Replace "StammbaumPage.svelte (SVG <text> element or foreignObject)" → "StammbaumTree.svelte"
  • Replace "StammbaumCard / D3 force graph node labels" → "SVG <g> node elements in StammbaumTree.svelte"
  • Replace "Add i18n key person_maiden_name_prefix" → "Reuse existing key person_born_name_prefix"
## 📋 Elicit — Requirements Engineer ### Observations - **Component naming is imprecise.** The issue references "D3 force graph node labels", "StammbaumPage.svelte", and "StammbaumCard". None of these exist. The actual rendering file is `StammbaumTree.svelte` (custom SVG layout, no D3 dependency). Precise file names prevent wasted developer time. - **Scope gap: side-panel relationship list.** The issue says "StammbaumSidePanel — the relationship list entries (inferred relationships)." The direct relationship entries use `RelationshipDTO` (specifically `otherName(rel, node.id)` → `personName`/`relatedPersonName`) — these are not `PersonNodeDTO` fields. Adding `maidenName` to `PersonNodeDTO` will only affect the **inferred relationships** section (which does use `PersonNodeDTO` via `InferredRelationshipWithPersonDTO`), not the direct relationship entries. This scope statement will create confusion during implementation. - **Duplicate i18n key.** `person_born_name_prefix` = `"geb."` already exists in `messages/de.json`. Adding `person_maiden_name_prefix` with the same value creates a redundant key. If the semantic distinction matters (maiden name vs. birth alias), document it explicitly; otherwise reuse the existing key. - **Missing edge case: long maiden names.** What happens when `maidenName` is 25+ characters (e.g., `"von Schönhausen-Bülow"`)? In the SVG tree node (160px wide), this will overflow. Truncation behavior should be specified. - **Missing NFR: mobile viewport.** The Stammbaum tree is used by the reader audience on phones. After NODE_H adjustment, the tree must still render correctly at 375px width. ### Recommendations **Suggested acceptance criteria additions:** - [ ] A person whose `maidenName` is `> 20 characters` is truncated in the SVG node card (with full name visible in the side panel) - [ ] The inferred relationship entries in `StammbaumSidePanel` show `geb. {lastName}` below each person name where a maiden name exists - [ ] The direct relationship entries in `StammbaumSidePanel` are **not** expected to show maiden names (different DTO — clarify this explicitly to avoid scope creep) **Suggested spec corrections:** - Replace "StammbaumPage.svelte (SVG `<text>` element or foreignObject)" → "StammbaumTree.svelte" - Replace "StammbaumCard / D3 force graph node labels" → "SVG `<g>` node elements in StammbaumTree.svelte" - Replace "Add i18n key `person_maiden_name_prefix`" → "Reuse existing key `person_born_name_prefix`"
Author
Owner

🚀 Tobias Wendt — DevOps & Platform Engineer

Observations

  • No infrastructure changes required. This is a pure application feature — no new services, no Docker Compose changes, no environment variables.
  • The OpenAPI type regeneration step is load-bearing. The issue documents it correctly (npm run generate:api). Without it, the TypeScript types won't include maidenName and the svelte-check gate will pass on stale types while the frontend silently ignores the new field.
  • Hibernate Statistics for query count is already available in the stack (Spring Boot Actuator + Hibernate metrics). If the N+1 regression test is formalized as Sara recommends, it can use EntityManagerFactory.unwrap(SessionFactory.class).getStatistics() — no extra infra needed.

Recommendations

  • The API type regeneration requires the backend to be running with --spring.profiles.active=dev — document this prerequisite in the PR checklist so it doesn't get skipped.
  • Confirm the CI pipeline runs npm run check on the frontend. If it doesn't, this PR is a good occasion to add it as a mandatory step — it's the gate for TypeScript and svelte-check issues.
  • No concerns from a deployment, config, or observability perspective. Ship it.
## 🚀 Tobias Wendt — DevOps & Platform Engineer ### Observations - **No infrastructure changes required.** This is a pure application feature — no new services, no Docker Compose changes, no environment variables. - **The OpenAPI type regeneration step is load-bearing.** The issue documents it correctly (`npm run generate:api`). Without it, the TypeScript types won't include `maidenName` and the svelte-check gate will pass on stale types while the frontend silently ignores the new field. - **Hibernate Statistics for query count** is already available in the stack (Spring Boot Actuator + Hibernate metrics). If the N+1 regression test is formalized as Sara recommends, it can use `EntityManagerFactory.unwrap(SessionFactory.class).getStatistics()` — no extra infra needed. ### Recommendations - The API type regeneration requires the backend to be **running with `--spring.profiles.active=dev`** — document this prerequisite in the PR checklist so it doesn't get skipped. - Confirm the CI pipeline runs `npm run check` on the frontend. If it doesn't, this PR is a good occasion to add it as a mandatory step — it's the gate for TypeScript and svelte-check issues. - No concerns from a deployment, config, or observability perspective. Ship it.
Author
Owner

🗳️ Decision Queue — Action Required

1 decision needs your input before implementation starts.

Frontend Architecture

  • SVG tree node card vs. side panel only for maiden name display. The current StammbaumTree.svelte SVG nodes have NODE_H = 56 with two text lines already at y=22 (name) and y=40 (dates). A third line for maiden name overflows the box. Options:

    • A) Side-panel only — Show geb. Schmidt in the StammbaumSidePanel header and inferred relationship entries. No change to NODE_H or the layout algorithm. Lower risk, faster to ship.
    • B) On the SVG node card — Increase NODE_H to ~76. The layout algorithm uses NODE_H as a global constant so this affects spacing for all nodes — needs testing with a 10+ person tree. Also need to decide whether the maiden name line is conditional (only shown when present) or whether all nodes grow to the new height.

    This decision affects: NODE_H value, layout algorithm testing scope, accessibility aria-label update, and which test cases Sara needs to write. (Raised by: Leonie, Felix, Elicit)

## 🗳️ Decision Queue — Action Required _1 decision needs your input before implementation starts._ ### Frontend Architecture - **SVG tree node card vs. side panel only for maiden name display.** The current `StammbaumTree.svelte` SVG nodes have `NODE_H = 56` with two text lines already at `y=22` (name) and `y=40` (dates). A third line for maiden name overflows the box. Options: - **A) Side-panel only** — Show `geb. Schmidt` in the `StammbaumSidePanel` header and inferred relationship entries. No change to `NODE_H` or the layout algorithm. Lower risk, faster to ship. - **B) On the SVG node card** — Increase `NODE_H` to ~76. The layout algorithm uses `NODE_H` as a global constant so this affects spacing for all nodes — needs testing with a 10+ person tree. Also need to decide whether the maiden name line is conditional (only shown when present) or whether all nodes grow to the new height. This decision affects: `NODE_H` value, layout algorithm testing scope, accessibility `aria-label` update, and which test cases Sara needs to write. _(Raised by: Leonie, Felix, Elicit)_
Author
Owner

🎨 Leonie Voss — UX/Accessibility follow-up discussion

Decisions reached on the four open UX items:

1. Maiden name on the SVG tree node card

Show geb. Schmidt directly on the node card in StammbaumTree.svelte — not side-panel only. Option B.

2. Uniform NODE_H=76, vertically centered text block

All nodes grow to NODE_H = 76 regardless of whether a maiden name is present. The text block (1–3 lines) is centered vertically using a dynamic yBase:

{@const hasDate = node.birthYear || node.deathYear}
{@const hasMaiden = !!node.maidenName}
{@const lineCount = 1 + (hasDate ? 1 : 0) + (hasMaiden ? 1 : 0)}
{@const lineSpacing = 16}
{@const yBase = NODE_H / 2 - ((lineCount - 1) * lineSpacing) / 2}

Each line renders at yBase, yBase + lineSpacing, yBase + 2 * lineSpacing as applicable. Nodes without a maiden name don't look like something is missing — they're simply balanced with fewer lines.

3. Truncate at 18 characters in the SVG node, full name in aria-label and side panel

{node.maidenName.length > 18 ? `${node.maidenName.slice(0, 17)}` : node.maidenName}

The aria-label on the <g> element must use the full untruncated maiden name. The side panel header always shows the complete value.

4. No maiden name in inferred relationship entries

Maiden name appears only in the selected node's side panel header — not in the inferred relationship list entries. Keeps the list clean.

## 🎨 Leonie Voss — UX/Accessibility follow-up discussion Decisions reached on the four open UX items: ### ✅ 1. Maiden name on the SVG tree node card Show `geb. Schmidt` directly on the node card in `StammbaumTree.svelte` — not side-panel only. Option B. ### ✅ 2. Uniform NODE_H=76, vertically centered text block All nodes grow to `NODE_H = 76` regardless of whether a maiden name is present. The text block (1–3 lines) is centered vertically using a dynamic `yBase`: ```svelte {@const hasDate = node.birthYear || node.deathYear} {@const hasMaiden = !!node.maidenName} {@const lineCount = 1 + (hasDate ? 1 : 0) + (hasMaiden ? 1 : 0)} {@const lineSpacing = 16} {@const yBase = NODE_H / 2 - ((lineCount - 1) * lineSpacing) / 2} ``` Each line renders at `yBase`, `yBase + lineSpacing`, `yBase + 2 * lineSpacing` as applicable. Nodes without a maiden name don't look like something is missing — they're simply balanced with fewer lines. ### ✅ 3. Truncate at 18 characters in the SVG node, full name in aria-label and side panel ```svelte {node.maidenName.length > 18 ? `${node.maidenName.slice(0, 17)}…` : node.maidenName} ``` The `aria-label` on the `<g>` element must use the **full** untruncated maiden name. The side panel header always shows the complete value. ### ✅ 4. No maiden name in inferred relationship entries Maiden name appears only in the **selected node's side panel header** — not in the inferred relationship list entries. Keeps the list clean.
Author
Owner

👨‍💻 Felix Brandt — Developer follow-up discussion

Decisions reached on three open implementation items:

1. i18n key: reuse person_born_name_prefix

The existing key already returns "geb." in de.json. The new person_maiden_name_prefix key proposed in the issue body is dropped — no duplicate needed.

2. PersonRelationshipsCard.svelte dropped from scope

Leonie's UX decision (maiden name only in the StammbaumSidePanel header) makes this component irrelevant to the issue. It is not touched in this implementation.

3. Truncation extracted to utils.ts as truncate(value, max)

The 18-char cap logic lives in a named, unit-testable utility function — not inline in the template:

// utils.ts
export function truncate(value: string, max: number): string {
    return value.length > max ? `${value.slice(0, max - 1)}…` : value;
}

Called from StammbaumTree.svelte for the maiden name SVG text element. The aria-label always uses the full untruncated value. Write unit tests for truncate() first (red/green), then wire into the template.


The implementation surface is now fully specified between this comment and Leonie's UX decisions above.

## 👨‍💻 Felix Brandt — Developer follow-up discussion Decisions reached on three open implementation items: ### ✅ 1. i18n key: reuse `person_born_name_prefix` The existing key already returns `"geb."` in de.json. The new `person_maiden_name_prefix` key proposed in the issue body is dropped — no duplicate needed. ### ✅ 2. `PersonRelationshipsCard.svelte` dropped from scope Leonie's UX decision (maiden name only in the `StammbaumSidePanel` header) makes this component irrelevant to the issue. It is not touched in this implementation. ### ✅ 3. Truncation extracted to `utils.ts` as `truncate(value, max)` The 18-char cap logic lives in a named, unit-testable utility function — not inline in the template: ```typescript // utils.ts export function truncate(value: string, max: number): string { return value.length > max ? `${value.slice(0, max - 1)}…` : value; } ``` Called from `StammbaumTree.svelte` for the maiden name SVG text element. The `aria-label` always uses the full untruncated value. Write unit tests for `truncate()` first (red/green), then wire into the template. --- The implementation surface is now fully specified between this comment and Leonie's UX decisions above.
Author
Owner

🏛️ Markus Keller — Architect follow-up discussion

Decisions reached on two structural backend items:

1. Domain boundary: alias lookup goes on PersonService

RelationshipService must not inject PersonNameAliasRepository directly — that crosses the domain boundary. The batch lookup is exposed as a method on PersonService:

// PersonService
public Map<UUID, String> findMaidenNames(Collection<UUID> personIds) {
    return aliasRepository.findByPersonIdInAndType(personIds, PersonNameAliasType.MAIDEN_NAME)
        .stream()
        .collect(Collectors.toMap(
            a -> a.getPerson().getId(),
            PersonNameAlias::getLastName,
            (a, b) -> a
        ));
}

RelationshipService calls personService.findMaidenNames(familyIds) — consistent with the established cross-domain access pattern.

2. Shared PersonNodeMapper in the relationship package

Both RelationshipService.getFamilyNetwork() and RelationshipInferenceService.findAllFor() construct PersonNodeDTO. The logic is extracted once into a shared mapper in the same package:

// relationship/PersonNodeMapper.java
class PersonNodeMapper {
    static PersonNodeDTO toNode(Person p, Map<UUID, String> maidenNames) {
        return new PersonNodeDTO(
            p.getId(), p.getDisplayName(),
            maidenNames.get(p.getId()),
            p.getBirthYear(), p.getDeathYear(), true);
    }
}

Both services call PersonNodeMapper.toNode(p, maidenNames). No duplicated construction logic.


The backend structure is clean. No domain boundary violations, no duplicated DTO construction.

## 🏛️ Markus Keller — Architect follow-up discussion Decisions reached on two structural backend items: ### ✅ 1. Domain boundary: alias lookup goes on `PersonService` `RelationshipService` must not inject `PersonNameAliasRepository` directly — that crosses the domain boundary. The batch lookup is exposed as a method on `PersonService`: ```java // PersonService public Map<UUID, String> findMaidenNames(Collection<UUID> personIds) { return aliasRepository.findByPersonIdInAndType(personIds, PersonNameAliasType.MAIDEN_NAME) .stream() .collect(Collectors.toMap( a -> a.getPerson().getId(), PersonNameAlias::getLastName, (a, b) -> a )); } ``` `RelationshipService` calls `personService.findMaidenNames(familyIds)` — consistent with the established cross-domain access pattern. ### ✅ 2. Shared `PersonNodeMapper` in the `relationship` package Both `RelationshipService.getFamilyNetwork()` and `RelationshipInferenceService.findAllFor()` construct `PersonNodeDTO`. The logic is extracted once into a shared mapper in the same package: ```java // relationship/PersonNodeMapper.java class PersonNodeMapper { static PersonNodeDTO toNode(Person p, Map<UUID, String> maidenNames) { return new PersonNodeDTO( p.getId(), p.getDisplayName(), maidenNames.get(p.getId()), p.getBirthYear(), p.getDeathYear(), true); } } ``` Both services call `PersonNodeMapper.toNode(p, maidenNames)`. No duplicated construction logic. --- The backend structure is clean. No domain boundary violations, no duplicated DTO construction.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#364