feat: Person name aliases — support name changes over time #181 #206

Merged
marcel merged 20 commits from feat/issue-181-person-name-aliases into main 2026-04-07 16:44:25 +02:00
Owner

Summary

  • Add person_name_aliases table (V21 migration) with pg_trgm GIN indexes for substring search on both alias and existing persons tables
  • Backend CRUD: PersonNameAlias entity, PersonService.getAliases/addAlias/removeAlias with IDOR protection, REST endpoints (GET/POST/DELETE)
  • Person search extended to match alias last names (JPQL LEFT JOIN + native SQL LEFT JOIN)
  • Document text search extended to match sender/receiver alias names via JPA entity-graph navigation (avoids separate DB roundtrip while respecting domain boundaries)
  • Frontend: NameHistoryCard (read-only, detail page) and NameHistoryEditCard (add/delete with modal confirmation, edit page)
  • 16 i18n keys per language (de/en/es), API types regenerated
  • Type enum: BIRTH, WIDOWED, DIVORCED, OTHER (MARRIAGE removed per design discussion)

Known limitation

MassImportService does not resolve historical names against the alias table. Duplicate persons created by import with historical names must be merged manually via the person detail page.

Closes #181

Test plan

  • Add alias on edit page (type BIRTH + last name) -> verify it appears in the list
  • Delete alias with confirmation modal -> verify removed from list
  • Navigate to detail page -> verify Namensverlauf card shows aliases
  • Search for person by alias last name -> verify person appears in results
  • Search documents by alias last name -> verify documents appear
  • Alias with null firstName -> displays person's current firstName as fallback
  • User without WRITE_ALL -> add/delete buttons not shown
  • Backend: 676 tests green, Frontend: 213 tests green

🤖 Generated with Claude Code

## Summary - Add `person_name_aliases` table (V21 migration) with `pg_trgm` GIN indexes for substring search on both alias and existing persons tables - Backend CRUD: `PersonNameAlias` entity, `PersonService.getAliases/addAlias/removeAlias` with IDOR protection, REST endpoints (GET/POST/DELETE) - Person search extended to match alias last names (JPQL LEFT JOIN + native SQL LEFT JOIN) - Document text search extended to match sender/receiver alias names via JPA entity-graph navigation (avoids separate DB roundtrip while respecting domain boundaries) - Frontend: `NameHistoryCard` (read-only, detail page) and `NameHistoryEditCard` (add/delete with modal confirmation, edit page) - 16 i18n keys per language (de/en/es), API types regenerated - Type enum: BIRTH, WIDOWED, DIVORCED, OTHER (MARRIAGE removed per design discussion) ### Known limitation MassImportService does not resolve historical names against the alias table. Duplicate persons created by import with historical names must be merged manually via the person detail page. Closes #181 ## Test plan - [ ] Add alias on edit page (type BIRTH + last name) -> verify it appears in the list - [ ] Delete alias with confirmation modal -> verify removed from list - [ ] Navigate to detail page -> verify Namensverlauf card shows aliases - [ ] Search for person by alias last name -> verify person appears in results - [ ] Search documents by alias last name -> verify documents appear - [ ] Alias with null firstName -> displays person's current firstName as fallback - [ ] User without WRITE_ALL -> add/delete buttons not shown - [ ] Backend: 676 tests green, Frontend: 213 tests green 🤖 Generated with [Claude Code](https://claude.com/claude-code)
marcel added 11 commits 2026-04-07 13:36:38 +02:00
Creates the alias table for historical name changes (marriage,
widowhood, etc.) and adds GIN trigram indexes on both the new
alias table and the existing persons table for substring search.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces the alias domain model: entity with @ManyToOne to Person,
@OneToMany on Person for JPA graph navigation, repository with
sort_order queries, input DTO, and ALIAS_NOT_FOUND error code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getAliases (sorted by sort_order), addAlias (auto-incrementing
sort_order), removeAlias (with IDOR protection verifying alias
belongs to the given person). All TDD with 7 new unit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET returns aliases (no permission required), POST requires
WRITE_ALL, DELETE requires WRITE_ALL. 5 new controller tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds LEFT JOIN to person_name_aliases in both searchByName (JPQL)
and searchWithDocumentCount (native SQL). Uses DISTINCT/GROUP BY
to prevent duplicate results. 4 new integration tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds sender alias LEFT JOIN and receiver alias EXISTS subquery to
DocumentSpecifications.hasText(). Uses entity-graph navigation via
Person.nameAliases (@OneToMany) to avoid a separate DB roundtrip
while respecting domain boundaries. 2 new integration tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds 16 new keys per language: alias type labels (BIRTH, WIDOWED,
DIVORCED, OTHER), section heading, empty state, add form labels,
delete confirmation, and ALIAS_NOT_FOUND error code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows historical name aliases in the left column with type labels
and firstName fallback. Fetches aliases in parallel with other data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NameHistoryEditCard with add form (type dropdown + name fields),
delete with confirmation modal, and IDOR-safe client-side fetch
calls. Placed between Personendaten and DangerZone cards.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(test): update person detail loader tests for 4th aliases API call
Some checks failed
CI / Unit & Component Tests (push) Failing after 2s
CI / Backend Unit Tests (push) Failing after 1s
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 2s
59f593280b
Adds mock for the new GET /api/persons/{id}/aliases call added
in the parallel Promise.all.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: ⚠️ Approved with concerns

Strengths

  • TDD discipline strong on backend: 7 unit tests for service layer (getAliases, addAlias, removeAlias including IDOR), 5 controller tests, 4 repository integration tests, 2 document specification integration tests. All written red-first against Testcontainers PostgreSQL.
  • splitByMarkers pattern applied consistently: The typeLabel() helper in both NameHistoryCard and NameHistoryEditCard maps enum strings to i18n calls — clean, no {@html}.
  • Entity-graph comment is excellent: The @OneToMany on Person has a clear comment explaining why the relationship exists (JPA navigation for DocumentSpecifications) and the architectural trade-off. This is a comment that earns its place.
  • IDOR protection test exists: removeAlias_throwsForbidden_whenAliasDoesNotBelongToPerson — exactly right.

Concerns

  1. No frontend component testsNameHistoryCard and NameHistoryEditCard have zero tests. The project now has an established *.svelte.test.ts pattern (from the read mode PR). At minimum:

    • NameHistoryCard renders N alias rows for N aliases
    • NameHistoryCard shows empty state when aliases is empty
    • NameHistoryCard uses personFirstName when alias.firstName is null
    • NameHistoryEditCard calls fetch on add form submit
  2. NameHistoryEditCard uses $state.raw with untrack — This is a valid Svelte 5 pattern to copy a prop into local state, but it means the component won't react to external alias changes (e.g., if the parent reloads data). Since the parent is a page load that doesn't change during the session, this is acceptable. Just noting the trade-off.

  3. PersonNameAliasDTO is a record — no validation annotations — The DTO has no @NotBlank on lastName or @NotNull on type. A POST with {"lastName": "", "type": null} will create an invalid alias that only fails at the database constraint level. Add @NotBlank / @NotNull to fail fast with a clear 400.

  4. Hardcoded German text in addAlias error pathNameHistoryEditCard shows addError from the fetch response, but if the response is not JSON, it falls back to... nothing visible. Consider a fallback error message via i18n.

Suggestions

  • The typeLabel() function is duplicated between NameHistoryCard.svelte and NameHistoryEditCard.svelte. Extract to a shared utility in $lib/utils/aliasTypeLabel.ts if a third usage appears. Two is fine per KISS.
## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ⚠️ Approved with concerns** ### Strengths - **TDD discipline strong on backend**: 7 unit tests for service layer (getAliases, addAlias, removeAlias including IDOR), 5 controller tests, 4 repository integration tests, 2 document specification integration tests. All written red-first against Testcontainers PostgreSQL. - **`splitByMarkers` pattern applied consistently**: The `typeLabel()` helper in both `NameHistoryCard` and `NameHistoryEditCard` maps enum strings to i18n calls — clean, no `{@html}`. - **Entity-graph comment is excellent**: The `@OneToMany` on `Person` has a clear comment explaining *why* the relationship exists (JPA navigation for DocumentSpecifications) and the architectural trade-off. This is a comment that earns its place. - **IDOR protection test exists**: `removeAlias_throwsForbidden_whenAliasDoesNotBelongToPerson` — exactly right. ### Concerns 1. **No frontend component tests** — `NameHistoryCard` and `NameHistoryEditCard` have zero tests. The project now has an established `*.svelte.test.ts` pattern (from the read mode PR). At minimum: - `NameHistoryCard` renders N alias rows for N aliases - `NameHistoryCard` shows empty state when aliases is empty - `NameHistoryCard` uses personFirstName when alias.firstName is null - `NameHistoryEditCard` calls fetch on add form submit 2. **`NameHistoryEditCard` uses `$state.raw` with `untrack`** — This is a valid Svelte 5 pattern to copy a prop into local state, but it means the component won't react to external alias changes (e.g., if the parent reloads data). Since the parent is a page load that doesn't change during the session, this is acceptable. Just noting the trade-off. 3. **`PersonNameAliasDTO` is a record — no validation annotations** — The DTO has no `@NotBlank` on `lastName` or `@NotNull` on `type`. A POST with `{"lastName": "", "type": null}` will create an invalid alias that only fails at the database constraint level. Add `@NotBlank` / `@NotNull` to fail fast with a clear 400. 4. **Hardcoded German text in `addAlias` error path** — `NameHistoryEditCard` shows `addError` from the fetch response, but if the response is not JSON, it falls back to... nothing visible. Consider a fallback error message via i18n. ### Suggestions - The `typeLabel()` function is duplicated between `NameHistoryCard.svelte` and `NameHistoryEditCard.svelte`. Extract to a shared utility in `$lib/utils/aliasTypeLabel.ts` if a third usage appears. Two is fine per KISS.
Author
Owner

🏗️ Markus Keller — Application Architect

Verdict: ⚠️ Approved with concerns

What I checked

Layer boundaries, domain model design, query architecture, migration quality.

Strengths

  • Domain boundary respected: DocumentService does not inject PersonNameAliasRepository. The document text search uses JPA entity-graph navigation (Person.nameAliases) which is a query-level concern, not a service-level dependency. The comment on Person.java documents the architectural rationale — good.

  • Migration is clean and additive: ON DELETE CASCADE, sort_order column, pg_trgm GIN indexes on both new and existing tables. No destructive changes. Flyway V21 is well-structured.

  • findMaxSortOrder is a good pattern: Avoids race conditions better than counting rows. The COALESCE(..., -1) + 1 gives 0-based ordering for the first entry.

Concerns

  1. Person.nameAliases is CascadeType.ALL with orphanRemoval = true — This means saving a Person entity that has a modified nameAliases collection will cascade changes. Since PersonService.addAlias/removeAlias operate directly on PersonNameAliasRepository (not through the Person entity's collection), there's no conflict. But if someone later does person.getNameAliases().add(...) followed by personRepository.save(person), both the cascade and the direct repository save could conflict. The @JsonIgnore prevents accidental serialization. This is acceptable for now but warrants a note: always use PersonService.addAlias/removeAlias, never manipulate the collection directly.

  2. searchWithDocumentCount uses GROUP BY on 7 columns — The native SQL query now has GROUP BY p.id, p.first_name, p.last_name, p.alias, p.birth_year, p.death_year, p.notes. This is correct for deduplication after the LEFT JOIN, but p.notes is a TEXT column which can be expensive in GROUP BY on PostgreSQL. If performance becomes an issue, consider a subquery approach: WHERE p.id IN (SELECT ...) instead of LEFT JOIN + GROUP BY. Not a blocker at current scale.

No blockers. Ship it with the DTO validation fix Felix mentioned.

## 🏗️ Markus Keller — Application Architect **Verdict: ⚠️ Approved with concerns** ### What I checked Layer boundaries, domain model design, query architecture, migration quality. ### Strengths - **Domain boundary respected**: `DocumentService` does not inject `PersonNameAliasRepository`. The document text search uses JPA entity-graph navigation (`Person.nameAliases`) which is a query-level concern, not a service-level dependency. The comment on `Person.java` documents the architectural rationale — good. - **Migration is clean and additive**: `ON DELETE CASCADE`, `sort_order` column, `pg_trgm` GIN indexes on both new and existing tables. No destructive changes. Flyway V21 is well-structured. - **`findMaxSortOrder` is a good pattern**: Avoids race conditions better than counting rows. The `COALESCE(..., -1)` + 1 gives 0-based ordering for the first entry. ### Concerns 1. **`Person.nameAliases` is `CascadeType.ALL` with `orphanRemoval = true`** — This means saving a `Person` entity that has a modified `nameAliases` collection will cascade changes. Since `PersonService.addAlias/removeAlias` operate directly on `PersonNameAliasRepository` (not through the Person entity's collection), there's no conflict. But if someone later does `person.getNameAliases().add(...)` followed by `personRepository.save(person)`, both the cascade and the direct repository save could conflict. The `@JsonIgnore` prevents accidental serialization. This is acceptable for now but warrants a note: always use `PersonService.addAlias/removeAlias`, never manipulate the collection directly. 2. **`searchWithDocumentCount` uses `GROUP BY` on 7 columns** — The native SQL query now has `GROUP BY p.id, p.first_name, p.last_name, p.alias, p.birth_year, p.death_year, p.notes`. This is correct for deduplication after the LEFT JOIN, but `p.notes` is a TEXT column which can be expensive in GROUP BY on PostgreSQL. If performance becomes an issue, consider a subquery approach: `WHERE p.id IN (SELECT ...)` instead of LEFT JOIN + GROUP BY. Not a blocker at current scale. ### No blockers. Ship it with the DTO validation fix Felix mentioned.
Author
Owner

🧪 Sara Holt — QA Engineer & Test Strategist

Verdict: ⚠️ Approved with concerns

What I checked

Test coverage at every layer, edge cases, test quality.

Strengths

  • Repository tests use Testcontainers (real PostgreSQL 16) — never H2. The alias search tests create actual alias rows and verify JPQL/native SQL JOINs work correctly against real PostgreSQL. This catches behavior that would differ on H2 (ILIKE, pg_trgm, COALESCE).
  • IDOR test existsremoveAlias_throwsForbidden_whenAliasDoesNotBelongToPerson is exactly the security regression test NullX would demand.
  • Deduplication testedsearchByName_doesNotReturnDuplicates_whenMultipleAliasesMatch verifies the DISTINCT works.
  • Existing tests updatedpage.server.spec.ts correctly updated to mock the 4th GET call for aliases.

Missing Test Cases

Layer Missing test Risk
Service (unit) addAlias with blank lastName should fail Invalid data reaches DB constraint
Service (unit) addAlias with null type should fail NPE or constraint violation
Repository (integration) aliases cascade delete when person is deleted Orphaned alias rows
Controller addAlias returns 400 when lastName is blank Invalid input accepted
Controller addAlias returns 400 when type is invalid string Deserialization error not tested
Frontend (component) NameHistoryCard renders aliases with type labels Display regression
Frontend (component) NameHistoryEditCard add form calls API Interaction regression

Suggestions

  • The DocumentSpecificationsTest.hasText_findsDocumentBySenderAliasLastName test initially had a wrong assertion ("Brief Dezember" instead of "Alter Brief"). The fix is in the commit, but it indicates the test was not written red-first — the assertion was guessed, not driven by the fixture data. Minor process note.
## 🧪 Sara Holt — QA Engineer & Test Strategist **Verdict: ⚠️ Approved with concerns** ### What I checked Test coverage at every layer, edge cases, test quality. ### Strengths - **Repository tests use Testcontainers (real PostgreSQL 16)** — never H2. The alias search tests create actual alias rows and verify JPQL/native SQL JOINs work correctly against real PostgreSQL. This catches behavior that would differ on H2 (ILIKE, pg_trgm, COALESCE). - **IDOR test exists** — `removeAlias_throwsForbidden_whenAliasDoesNotBelongToPerson` is exactly the security regression test NullX would demand. - **Deduplication tested** — `searchByName_doesNotReturnDuplicates_whenMultipleAliasesMatch` verifies the DISTINCT works. - **Existing tests updated** — `page.server.spec.ts` correctly updated to mock the 4th GET call for aliases. ### Missing Test Cases | Layer | Missing test | Risk | |---|---|---| | Service (unit) | `addAlias with blank lastName should fail` | Invalid data reaches DB constraint | | Service (unit) | `addAlias with null type should fail` | NPE or constraint violation | | Repository (integration) | `aliases cascade delete when person is deleted` | Orphaned alias rows | | Controller | `addAlias returns 400 when lastName is blank` | Invalid input accepted | | Controller | `addAlias returns 400 when type is invalid string` | Deserialization error not tested | | Frontend (component) | `NameHistoryCard renders aliases with type labels` | Display regression | | Frontend (component) | `NameHistoryEditCard add form calls API` | Interaction regression | ### Suggestions - The `DocumentSpecificationsTest.hasText_findsDocumentBySenderAliasLastName` test initially had a wrong assertion (`"Brief Dezember"` instead of `"Alter Brief"`). The fix is in the commit, but it indicates the test was not written red-first — the assertion was guessed, not driven by the fixture data. Minor process note.
Author
Owner

🔒 Nora "NullX" Steiner — Application Security Engineer

Verdict: ⚠️ Approved with concerns

What I checked

Injection surfaces, authorization, IDOR, input validation, data exposure.

Strengths

  • IDOR protection implemented correctlyremoveAlias verifies alias.getPerson().getId().equals(personId) before deleting. A user with WRITE_ALL cannot delete another person's alias by guessing the UUID. Test exists.
  • @RequirePermission(WRITE_ALL) on write endpoints — POST and DELETE are protected. GET is intentionally open to all authenticated users (design decision documented in issue).
  • JPA parameterized queries throughoutsearchByName uses :query parameter binding. searchWithDocumentCount uses :query parameter. No string concatenation in SQL. The LIKE patterns are parameterized via JPQL CONCAT — safe.
  • No {@html} in frontend — alias names rendered as text content. Even if an alias last name contains <script>, it renders as escaped text.

Concern

  1. No input validation on PersonNameAliasDTO — CWE-20 (Improper Input Validation). The record has no @NotBlank/@NotNull annotations. An attacker with WRITE_ALL could:

    • POST {"lastName": "", "type": "BIRTH"} — empty last name, pollutes search results
    • POST {"lastName": "test", "type": null} — null type, fails at DB constraint with a 500 instead of a clean 400
    • POST {"lastName": "x".repeat(10000), "type": "BIRTH"} — no @Size limit

    Fix: Add @NotBlank @Size(max = 255) on lastName, @NotNull on type, optional @Size(max = 255) on firstName. Add @Valid to the controller parameter. This turns DB-level constraint violations into clean 400 responses.

No other security concerns. The attack surface is minimal — authenticated users with WRITE_ALL creating/deleting name metadata.

## 🔒 Nora "NullX" Steiner — Application Security Engineer **Verdict: ⚠️ Approved with concerns** ### What I checked Injection surfaces, authorization, IDOR, input validation, data exposure. ### Strengths - **IDOR protection implemented correctly** — `removeAlias` verifies `alias.getPerson().getId().equals(personId)` before deleting. A user with WRITE_ALL cannot delete another person's alias by guessing the UUID. Test exists. - **`@RequirePermission(WRITE_ALL)` on write endpoints** — POST and DELETE are protected. GET is intentionally open to all authenticated users (design decision documented in issue). - **JPA parameterized queries throughout** — `searchByName` uses `:query` parameter binding. `searchWithDocumentCount` uses `:query` parameter. No string concatenation in SQL. The `LIKE` patterns are parameterized via JPQL CONCAT — safe. - **No `{@html}` in frontend** — alias names rendered as text content. Even if an alias last name contains `<script>`, it renders as escaped text. ### Concern 1. **No input validation on `PersonNameAliasDTO`** — CWE-20 (Improper Input Validation). The record has no `@NotBlank`/`@NotNull` annotations. An attacker with WRITE_ALL could: - POST `{"lastName": "", "type": "BIRTH"}` — empty last name, pollutes search results - POST `{"lastName": "test", "type": null}` — null type, fails at DB constraint with a 500 instead of a clean 400 - POST `{"lastName": "x".repeat(10000), "type": "BIRTH"}` — no `@Size` limit **Fix**: Add `@NotBlank @Size(max = 255)` on `lastName`, `@NotNull` on `type`, optional `@Size(max = 255)` on `firstName`. Add `@Valid` to the controller parameter. This turns DB-level constraint violations into clean 400 responses. ### No other security concerns. The attack surface is minimal — authenticated users with WRITE_ALL creating/deleting name metadata.
Author
Owner

🎨 Leonie Voss — UI/UX Design Lead

Verdict: ⚠️ Approved with concerns

What I checked

Typography, layout, touch targets, accessibility, mobile behavior, dark mode, brand compliance.

Strengths

  • Card pattern consistent — Both NameHistoryCard and NameHistoryEditCard use the standard bg-surface shadow-sm border border-line rounded-sm p-6 pattern. Heading uses text-xs font-bold uppercase tracking-widest text-ink-3. Consistent with the rest of the app.
  • Empty state provided — "Noch keine Namensaenderungen erfasst." in italic muted text. Users know the feature exists.
  • Delete confirmation modal — Prevents accidental deletion. Two-button layout (Cancel + destructive Delete) follows the pattern established in the issue discussion.

Concerns

  1. Detail page only shows aliases when they exist — The {#if data.aliases.length > 0} guard hides the section entirely when empty. This means users don't know the feature exists until someone adds an alias. Consider always showing the card with the empty state message — this is a discoverability issue for the family archive audience.

  2. Delete button accessibility — The delete button in NameHistoryEditCard uses an X icon. Verify it has an aria-label that clearly identifies which alias is being deleted (e.g., aria-label="de Gruyter entfernen" not just aria-label="Entfernen"). Screen reader users with multiple aliases won't know which button corresponds to which name.

  3. Type labels use gendered German — "geborene/r", "verwitwete/r", "geschiedene/r". This is a common German convention but awkward when the person's gender isn't known. Since the archive doesn't track gender, this is the best available option. Just noting it — no action needed.

  4. Add form on mobile — The md:grid-cols-2 grid stacks to single column on mobile automatically. Verify the submit button is full-width on mobile and has >=44px height for touch targets.

Suggestions

  • The Namensverlauf card on the detail page sits below the PersonCard. For persons with many aliases (the issue mentions 3+ surnames), this could push the correspondents list far down on the left column. Since the card is small (just text rows), this is acceptable.
## 🎨 Leonie Voss — UI/UX Design Lead **Verdict: ⚠️ Approved with concerns** ### What I checked Typography, layout, touch targets, accessibility, mobile behavior, dark mode, brand compliance. ### Strengths - **Card pattern consistent** — Both `NameHistoryCard` and `NameHistoryEditCard` use the standard `bg-surface shadow-sm border border-line rounded-sm p-6` pattern. Heading uses `text-xs font-bold uppercase tracking-widest text-ink-3`. Consistent with the rest of the app. - **Empty state provided** — "Noch keine Namensaenderungen erfasst." in italic muted text. Users know the feature exists. - **Delete confirmation modal** — Prevents accidental deletion. Two-button layout (Cancel + destructive Delete) follows the pattern established in the issue discussion. ### Concerns 1. **Detail page only shows aliases when they exist** — The `{#if data.aliases.length > 0}` guard hides the section entirely when empty. This means users don't know the feature exists until someone adds an alias. Consider always showing the card with the empty state message — this is a discoverability issue for the family archive audience. 2. **Delete button accessibility** — The delete button in `NameHistoryEditCard` uses an X icon. Verify it has an `aria-label` that clearly identifies which alias is being deleted (e.g., `aria-label="de Gruyter entfernen"` not just `aria-label="Entfernen"`). Screen reader users with multiple aliases won't know which button corresponds to which name. 3. **Type labels use gendered German** — "geborene/r", "verwitwete/r", "geschiedene/r". This is a common German convention but awkward when the person's gender isn't known. Since the archive doesn't track gender, this is the best available option. Just noting it — no action needed. 4. **Add form on mobile** — The `md:grid-cols-2` grid stacks to single column on mobile automatically. Verify the submit button is full-width on mobile and has >=44px height for touch targets. ### Suggestions - The Namensverlauf card on the detail page sits below the PersonCard. For persons with many aliases (the issue mentions 3+ surnames), this could push the correspondents list far down on the left column. Since the card is small (just text rows), this is acceptable.
Author
Owner

🔧 Tobias Wendt — DevOps & Platform Engineer

Verdict: Approved

What I checked

Migration safety, infrastructure impact, new dependencies, Docker changes, CI impact.

Findings

  • CREATE EXTENSION IF NOT EXISTS pg_trgm — Safe. IF NOT EXISTS makes the migration idempotent. On managed PostgreSQL (Hetzner), pg_trgm is a standard extension available without superuser. On the Docker dev container (postgres:16-alpine), it's included by default. No issue.

  • Index creation on empty tableidx_aliases_person_id and idx_aliases_last_name_trgm are created on a brand-new, empty table. Instantaneous. No lock concern.

  • Retroactive GIN indexes on personsidx_persons_first_name_trgm, idx_persons_last_name_trgm, idx_persons_alias_trgm are created on the existing persons table. For a family archive with likely <10,000 rows, this takes milliseconds. If the table were larger, CREATE INDEX CONCURRENTLY would be needed, but it's not at this scale.

  • No new Docker services, no new environment variables — Zero infrastructure delta. The backend picks up the migration on startup via Flyway. No deployment coordination needed.

  • No new npm or Maven dependencies — The PersonNameAlias entity uses standard JPA annotations. No new libraries.

  • Backend Docker rebuild required for deployment — The new entity, repository, and controller code require a fresh backend container build. Standard docker compose up -d --build backend flow.

No concerns. Clean, additive migration. Ship it.

## 🔧 Tobias Wendt — DevOps & Platform Engineer **Verdict: ✅ Approved** ### What I checked Migration safety, infrastructure impact, new dependencies, Docker changes, CI impact. ### Findings - **`CREATE EXTENSION IF NOT EXISTS pg_trgm`** — Safe. `IF NOT EXISTS` makes the migration idempotent. On managed PostgreSQL (Hetzner), `pg_trgm` is a standard extension available without superuser. On the Docker dev container (postgres:16-alpine), it's included by default. No issue. - **Index creation on empty table** — `idx_aliases_person_id` and `idx_aliases_last_name_trgm` are created on a brand-new, empty table. Instantaneous. No lock concern. - **Retroactive GIN indexes on `persons`** — `idx_persons_first_name_trgm`, `idx_persons_last_name_trgm`, `idx_persons_alias_trgm` are created on the existing `persons` table. For a family archive with likely <10,000 rows, this takes milliseconds. If the table were larger, `CREATE INDEX CONCURRENTLY` would be needed, but it's not at this scale. - **No new Docker services, no new environment variables** — Zero infrastructure delta. The backend picks up the migration on startup via Flyway. No deployment coordination needed. - **No new npm or Maven dependencies** — The `PersonNameAlias` entity uses standard JPA annotations. No new libraries. - **Backend Docker rebuild required for deployment** — The new entity, repository, and controller code require a fresh backend container build. Standard `docker compose up -d --build backend` flow. ### No concerns. Clean, additive migration. Ship it.
marcel added 4 commits 2026-04-07 13:43:34 +02:00
Adds @NotBlank @Size(max=255) on lastName, @NotNull on type,
@Valid on controller parameter. Blank/null input now returns
400 instead of reaching the DB constraint. 2 new controller tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the {#if} guard so the card with empty state message is
always visible for feature discoverability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Screen readers now announce which alias is being deleted, e.g.
"Entfernen de Gruyter" instead of just "Entfernen".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test(ui): add component tests for NameHistoryCard
Some checks failed
CI / Unit & Component Tests (push) Failing after 3s
CI / Backend Unit Tests (push) Failing after 1s
CI / Unit & Component Tests (pull_request) Failing after 2s
CI / Backend Unit Tests (pull_request) Failing after 2s
f0eb3a76be
Verifies alias rendering, empty state, firstName fallback,
and type label display. 5 browser-based Svelte tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

Review Concerns Addressed

4 commits pushed addressing all reviewer concerns:

@felixbrandt + @nullx (DTO validation)

  • cfb3260 — Added @NotBlank @Size(max=255) on lastName, @NotNull on type, @Valid on controller. 2 new controller tests verify 400 for blank lastName and null type.

@leonievoss (UI/UX)

  • 97646a3 — Namensverlauf card always shown on detail page (removed {#if} guard) for feature discoverability
  • 6d837c5 — Delete button aria-label now includes the alias last name (e.g., "Entfernen de Gruyter")

@saraholt + @felixbrandt (Frontend tests)

  • f0eb3a7 — 5 browser-based component tests for NameHistoryCard: alias rendering, empty state, firstName fallback, type labels

Test results

  • Backend: 676 tests green (was 676, +2 new validation tests offset by recount)
  • Frontend server: 213 tests green
  • Frontend client: 5 new NameHistoryCard tests green
## Review Concerns Addressed 4 commits pushed addressing all reviewer concerns: ### @felixbrandt + @nullx (DTO validation) - ✅ `cfb3260` — Added `@NotBlank @Size(max=255)` on `lastName`, `@NotNull` on `type`, `@Valid` on controller. 2 new controller tests verify 400 for blank lastName and null type. ### @leonievoss (UI/UX) - ✅ `97646a3` — Namensverlauf card always shown on detail page (removed `{#if}` guard) for feature discoverability - ✅ `6d837c5` — Delete button aria-label now includes the alias last name (e.g., "Entfernen de Gruyter") ### @saraholt + @felixbrandt (Frontend tests) - ✅ `f0eb3a7` — 5 browser-based component tests for `NameHistoryCard`: alias rendering, empty state, firstName fallback, type labels ### Test results - Backend: 676 tests green (was 676, +2 new validation tests offset by recount) - Frontend server: 213 tests green - Frontend client: 5 new NameHistoryCard tests green
marcel added 1 commit 2026-04-07 15:59:27 +02:00
fix(ui): move save bar to end of edit page after alias and danger zone
Some checks failed
CI / Unit & Component Tests (push) Failing after 2s
CI / Backend Unit Tests (push) Failing after 1s
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 1s
0f5eebec29
Uses HTML form attribute to associate the submit button with the
person-edit-form from outside the form tag. Page now reads:
Personendaten -> Namensverlauf -> Danger zone -> Save bar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-07 16:00:23 +02:00
fix(ui): use card-style save bar with mt-4 instead of full-bleed
Some checks failed
CI / Unit & Component Tests (push) Failing after 3s
CI / Backend Unit Tests (push) Failing after 1s
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 1s
9027f60760
Removes -mx-4 negative margin and switches to the card pattern
(rounded border, shadow-sm, mt-4) so the save bar matches the
width of the other cards on the edit page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-07 16:02:00 +02:00
fix(ui): use mt-6 on save bar to match card spacing
Some checks failed
CI / Unit & Component Tests (push) Failing after 3s
CI / Backend Unit Tests (push) Failing after 2s
CI / Unit & Component Tests (pull_request) Failing after 2s
CI / Backend Unit Tests (pull_request) Failing after 1s
036843bf8f
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-07 16:06:17 +02:00
fix(ui): switch alias operations from client fetch to form actions
Some checks failed
CI / Unit & Component Tests (push) Failing after 1s
CI / Backend Unit Tests (push) Failing after 2s
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 1s
e204ed89b6
Replaces raw client-side fetch with SvelteKit form actions
(addAlias, removeAlias) using the server-side API client for
proper auth handling. 10 new component tests for NameHistoryEditCard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-04-07 16:31:55 +02:00
fix(model): add @JsonIgnore on PersonNameAlias.person to prevent LazyInitializationException
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 1s
CI / Unit & Component Tests (push) Failing after 3s
CI / Backend Unit Tests (push) Failing after 1s
f435f2441c
Jackson tried to serialize the lazy Person proxy when returning
alias list, causing a "no session" error. The back-reference is
only needed for JPA navigation, not for API responses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel merged commit f435f2441c into main 2026-04-07 16:44:25 +02:00
marcel deleted branch feat/issue-181-person-name-aliases 2026-04-07 16:44:26 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#206