feat(persons): show merge action inline with danger hint, remove Gefahrenzone collapsible (#342) #347

Merged
marcel merged 1 commits from feat/issue-342-person-merge-inline into main 2026-04-26 21:54:46 +02:00
Owner

Closes #342

What was implemented

Per the Decision Queue resolution on #342: the merge panel stays on the edit page (/persons/[id]/edit) and is made inline — the panel was not moved to the detail page.

Changes

  • PersonMergePanel.svelte — container class changed from border-line to border-red-200. The danger indicator is now intrinsic to the panel itself, not dependent on the accordion wrapper.
  • edit/+page.svelte — replaced <PersonDangerZone person={person} form={form} /> with a direct inline render: {#key person.id}<PersonMergePanel person={person} form={form} />{/key}. The {#key} wrapper is preserved to reset internal merge state when navigating between persons.
  • edit/PersonDangerZone.svelte — deleted. No dead code left behind.
  • PersonMergePanel.svelte.spec.ts — new component test file (TDD, written before the fix) covering: red border class, merge heading renders, button disabled when no target selected, mergeError displays.

Acceptance criteria

  • On /persons/[id]/edit, the "Personen zusammenführen" section is visible without expanding anything
  • Red border (border-red-200) is visible on the section at all times
  • Merge functionality (two-step confirm, ?/merge action, redirect on success) is unchanged
  • PersonDangerZone collapsible is removed and deleted
Closes #342 ## What was implemented Per the Decision Queue resolution on #342: the merge panel stays on the **edit page** (`/persons/[id]/edit`) and is made inline — the panel was not moved to the detail page. ### Changes - **`PersonMergePanel.svelte`** — container class changed from `border-line` to `border-red-200`. The danger indicator is now intrinsic to the panel itself, not dependent on the accordion wrapper. - **`edit/+page.svelte`** — replaced `<PersonDangerZone person={person} form={form} />` with a direct inline render: `{#key person.id}<PersonMergePanel person={person} form={form} />{/key}`. The `{#key}` wrapper is preserved to reset internal merge state when navigating between persons. - **`edit/PersonDangerZone.svelte`** — deleted. No dead code left behind. - **`PersonMergePanel.svelte.spec.ts`** — new component test file (TDD, written before the fix) covering: red border class, merge heading renders, button disabled when no target selected, mergeError displays. ### Acceptance criteria - ✅ On `/persons/[id]/edit`, the "Personen zusammenführen" section is visible without expanding anything - ✅ Red border (`border-red-200`) is visible on the section at all times - ✅ Merge functionality (two-step confirm, `?/merge` action, redirect on success) is unchanged - ✅ `PersonDangerZone` collapsible is removed and deleted
Author
Owner

👨‍💻 Markus Keller — Application Architect

Verdict: Approved

What I checked

Module boundaries, component ownership, dead code, layer separation, and the {#key} state-reset pattern.

Findings

Clean deletion of PersonDangerZone.svelte — The accordion wrapper is gone entirely. No dead code left behind. This is correct.

{#key person.id} wrapper preserved correctly — The state-reset wrapper moved from inside PersonDangerZone to edit/+page.svelte. This is the right place for it: the orchestrating page controls when child state is reset, not the child component itself. Good.

PersonMergePanel owns its danger signal intrinsically now — The border-red-200 class moved from the wrapper (PersonDangerZone) into the panel itself. This is architecturally correct: the panel is a destructive-action component and its visual contract should not depend on a decorator wrapper. The danger signal is now part of the component's identity.

Import path is correctedit/+page.svelte imports from '../PersonMergePanel.svelte' (up one level to persons/[id]/). The merge panel has always lived at the [id] level, not the edit/ level. The accordion was the edit-page decoration, and that decoration is now gone. The module boundary was already right; this change simply removes the indirection.

Scope confirmed as edit-page only — Per the Decision Queue resolution, the merge panel stays on /persons/[id]/edit. The detail page is untouched. This keeps action routing simple: ?/merge continues to be served by edit/+page.server.ts with no duplication.

No blockers. The structural outcome is simpler and cleaner than before.

## 👨‍💻 Markus Keller — Application Architect **Verdict: ✅ Approved** ### What I checked Module boundaries, component ownership, dead code, layer separation, and the `{#key}` state-reset pattern. ### Findings **Clean deletion of `PersonDangerZone.svelte`** — The accordion wrapper is gone entirely. No dead code left behind. This is correct. **`{#key person.id}` wrapper preserved correctly** — The state-reset wrapper moved from inside `PersonDangerZone` to `edit/+page.svelte`. This is the right place for it: the orchestrating page controls when child state is reset, not the child component itself. Good. **`PersonMergePanel` owns its danger signal intrinsically now** — The `border-red-200` class moved from the wrapper (`PersonDangerZone`) into the panel itself. This is architecturally correct: the panel is a destructive-action component and its visual contract should not depend on a decorator wrapper. The danger signal is now part of the component's identity. **Import path is correct** — `edit/+page.svelte` imports from `'../PersonMergePanel.svelte'` (up one level to `persons/[id]/`). The merge panel has always lived at the `[id]` level, not the `edit/` level. The accordion was the edit-page decoration, and that decoration is now gone. The module boundary was already right; this change simply removes the indirection. **Scope confirmed as edit-page only** — Per the Decision Queue resolution, the merge panel stays on `/persons/[id]/edit`. The detail page is untouched. This keeps action routing simple: `?/merge` continues to be served by `edit/+page.server.ts` with no duplication. ### No blockers. The structural outcome is simpler and cleaner than before.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: ⚠️ Approved with concerns

What I checked

TDD evidence, Svelte 5 patterns, component size, naming, dead code, test quality, and the diff holistically.


The core change (issue #342)

The PersonMergePanel.svelte and edit/+page.svelte changes are clean and correct:

  • border-lineborder-red-200 on the panel container: one-line, correct, intrinsic to the component.
  • <PersonDangerZone> → direct {#key person.id}<PersonMergePanel .../>{/key}: exactly right. The {#key} wrapper is preserved, which is important — it resets mergeTargetId and showMergeConfirm when navigating between persons.
  • PersonDangerZone.svelte deleted: no dead code left.

PersonMergePanel.svelte.spec.ts was written TDD-first (per the completion comment). The 4 tests cover: red border class, heading renders, button disabled when no target, mergeError displays. These are the right behaviors to cover. Test names are sentence-style and readable.


Concerns (suggestions, not blockers)

1. The PersonMergePanel.svelte.spec.ts mock for PersonTypeahead is incorrect Svelte 5 syntax

vi.mock('$lib/components/PersonTypeahead.svelte', () => ({
    default: vi.fn().mockImplementation(() => ({
        $$: {},
        render: () => '<div></div>'
    }))
}));

This is Svelte 3/4 component mock syntax ($$ internals). In Svelte 5 with vitest-browser-svelte, the recommended approach is either to mock at the module boundary returning a simple stub component, or to let the real component render (since vitest-browser-svelte uses a real browser). If the PersonTypeahead requires a network call, stub at the API level instead. The current mock may work incidentally but will break if Svelte 5 internals change.

Suggested fix:

vi.mock('$lib/components/PersonTypeahead.svelte', () => ({
    default: () => null  // Svelte 5: functional component returning null
}));

2. The diff contains unrelated files

This PR diff includes:

  • frontend/e2e/person-typeahead.spec.ts (E2E tests for PersonTypeahead dropdown visibility — issue #343)
  • frontend/src/lib/components/AnnotationShape.svelte (delete button feature — unrelated)
  • frontend/src/lib/components/PdfControls.svelte + .spec.ts (annotation toggle contrast fix — unrelated)
  • frontend/src/lib/components/TranscriptionEditView.svelte.spec.ts (bulk-mark-reviewed tests — issue #345)

These are not part of issue #342. They appear to be uncommitted work from other branches that landed on this branch. They should be reviewed separately. I'm reviewing only the #342 changes above.

3. PdfControls.svelte color token change: text-accenttext-primary

This is in the diff (line 'bg-surface/10 text-primary'). text-primary is not a defined token in layout.css based on the brand palette in CLAUDE.md (which defines brand-navy, brand-mint, brand-sand). If text-primary resolves to something meaningful via Tailwind, fine — but this should be verified. The original text-accent was presumably brand-mint. This change is unrelated to #342 but is in the diff.


Summary

The #342 core change (merge panel inline, accordion deleted, red border intrinsic, {#key} preserved) is clean and correct. The two items to resolve: fix the Svelte 5 mock syntax in the spec, and confirm whether the unrelated files in the diff are intentional inclusions or accidental.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ⚠️ Approved with concerns** ### What I checked TDD evidence, Svelte 5 patterns, component size, naming, dead code, test quality, and the diff holistically. --- ### The core change (issue #342) The `PersonMergePanel.svelte` and `edit/+page.svelte` changes are clean and correct: - `border-line` → `border-red-200` on the panel container: one-line, correct, intrinsic to the component. - `<PersonDangerZone>` → direct `{#key person.id}<PersonMergePanel .../>{/key}`: exactly right. The `{#key}` wrapper is preserved, which is important — it resets `mergeTargetId` and `showMergeConfirm` when navigating between persons. - `PersonDangerZone.svelte` deleted: no dead code left. `PersonMergePanel.svelte.spec.ts` was written TDD-first (per the completion comment). The 4 tests cover: red border class, heading renders, button disabled when no target, mergeError displays. These are the right behaviors to cover. Test names are sentence-style and readable. --- ### Concerns (suggestions, not blockers) **1. The `PersonMergePanel.svelte.spec.ts` mock for `PersonTypeahead` is incorrect Svelte 5 syntax** ```typescript vi.mock('$lib/components/PersonTypeahead.svelte', () => ({ default: vi.fn().mockImplementation(() => ({ $$: {}, render: () => '<div></div>' })) })); ``` This is Svelte 3/4 component mock syntax (`$$` internals). In Svelte 5 with `vitest-browser-svelte`, the recommended approach is either to mock at the module boundary returning a simple stub component, or to let the real component render (since `vitest-browser-svelte` uses a real browser). If the `PersonTypeahead` requires a network call, stub at the API level instead. The current mock may work incidentally but will break if Svelte 5 internals change. **Suggested fix:** ```typescript vi.mock('$lib/components/PersonTypeahead.svelte', () => ({ default: () => null // Svelte 5: functional component returning null })); ``` **2. The diff contains unrelated files** This PR diff includes: - `frontend/e2e/person-typeahead.spec.ts` (E2E tests for `PersonTypeahead` dropdown visibility — issue #343) - `frontend/src/lib/components/AnnotationShape.svelte` (delete button feature — unrelated) - `frontend/src/lib/components/PdfControls.svelte` + `.spec.ts` (annotation toggle contrast fix — unrelated) - `frontend/src/lib/components/TranscriptionEditView.svelte.spec.ts` (bulk-mark-reviewed tests — issue #345) These are **not part of issue #342**. They appear to be uncommitted work from other branches that landed on this branch. They should be reviewed separately. I'm reviewing only the #342 changes above. **3. `PdfControls.svelte` color token change: `text-accent` → `text-primary`** This is in the diff (line `'bg-surface/10 text-primary'`). `text-primary` is not a defined token in `layout.css` based on the brand palette in CLAUDE.md (which defines `brand-navy`, `brand-mint`, `brand-sand`). If `text-primary` resolves to something meaningful via Tailwind, fine — but this should be verified. The original `text-accent` was presumably `brand-mint`. This change is unrelated to #342 but is in the diff. --- ### Summary The #342 core change (merge panel inline, accordion deleted, red border intrinsic, `{#key}` preserved) is clean and correct. The two items to resolve: fix the Svelte 5 mock syntax in the spec, and confirm whether the unrelated files in the diff are intentional inclusions or accidental.
Author
Owner

👨‍💻 Tobias Wendt — DevOps & Platform Engineer

Verdict: Approved

What I checked

Infrastructure impact, CI pipeline, SvelteKit routing side effects, Docker Compose, E2E test path changes.

Findings

No infrastructure changes required. This is a pure frontend refactor. No new environment variables, no Docker Compose changes, no Caddy config updates, no new ports or services.

SvelteKit action routing is unaffected. The ?/merge form action continues to be served by edit/+page.server.ts under /persons/[id]/edit. No routing config changes needed. SvelteKit's file-based routing handles this automatically.

Backend endpoint unchanged. POST /api/persons/{id}/merge already exists and is tested. This PR adds no new API surface.

E2E test paths. The PR adds frontend/e2e/person-typeahead.spec.ts which navigates to document edit pages, not person edit pages. No existing E2E test for the merge flow appears to be broken by this change. Worth confirming in CI that the Playwright suite passes, but there's no structural reason it wouldn't.

One observation on the new E2E file: person-typeahead.spec.ts uses page.waitForTimeout(400) for the 300ms debounce. waitForTimeout is a fixed sleep — it's brittle and flagged as an antipattern in Playwright best practices. Prefer waitForSelector('[role="listbox"]') or waitForResponse after the debounce fires. This is unrelated to #342 but is in the diff. File it under the issue that introduced it (#343).

No CI pipeline changes needed. Existing Vitest and Playwright runs cover the new test files without configuration changes.

Nothing to block here from an infrastructure perspective.

## 👨‍💻 Tobias Wendt — DevOps & Platform Engineer **Verdict: ✅ Approved** ### What I checked Infrastructure impact, CI pipeline, SvelteKit routing side effects, Docker Compose, E2E test path changes. ### Findings **No infrastructure changes required.** This is a pure frontend refactor. No new environment variables, no Docker Compose changes, no Caddy config updates, no new ports or services. **SvelteKit action routing is unaffected.** The `?/merge` form action continues to be served by `edit/+page.server.ts` under `/persons/[id]/edit`. No routing config changes needed. SvelteKit's file-based routing handles this automatically. **Backend endpoint unchanged.** `POST /api/persons/{id}/merge` already exists and is tested. This PR adds no new API surface. **E2E test paths.** The PR adds `frontend/e2e/person-typeahead.spec.ts` which navigates to document edit pages, not person edit pages. No existing E2E test for the merge flow appears to be broken by this change. Worth confirming in CI that the Playwright suite passes, but there's no structural reason it wouldn't. **One observation on the new E2E file:** `person-typeahead.spec.ts` uses `page.waitForTimeout(400)` for the 300ms debounce. `waitForTimeout` is a fixed sleep — it's brittle and flagged as an antipattern in Playwright best practices. Prefer `waitForSelector('[role="listbox"]')` or `waitForResponse` after the debounce fires. This is unrelated to #342 but is in the diff. File it under the issue that introduced it (#343). **No CI pipeline changes needed.** Existing Vitest and Playwright runs cover the new test files without configuration changes. ### Nothing to block here from an infrastructure perspective.
Author
Owner

👨‍💻 Elicit — Requirements Engineer

Verdict: Approved

What I checked

Acceptance criteria traceability, scope alignment with the Decision Queue resolution, and requirement completeness.

Traceability against the resolved acceptance criteria

The Decision Queue resolution clarified that the merge panel stays on the edit page and is made inline — not moved to the detail page. Mapping the diff against the resolved AC:

Acceptance criterion Status
On /persons/[id]/edit, "Personen zusammenführen" is visible without expanding anything PersonDangerZone accordion deleted; PersonMergePanel rendered directly
Section has a clear danger indicator (red border) border-red-200 now intrinsic to PersonMergePanel
Merge functionality unchanged ?/merge action, two-step confirm, {#key} state reset all preserved
PersonDangerZone collapsible removed and deleted File deleted, no dead code

Observations

Scope is tightly contained. The change does exactly what the resolved Decision Queue specified — no more, no less. The detail page is untouched. The edit page loses the accordion and gains a directly rendered panel.

The irreversibility text question (Decision Queue Theme 3) was resolved as "keep the old text." The existing person_merge_warning i18n key is unchanged. The panel still shows the irreversibility warning only in the confirmation step, not at the section level. This is a deliberate product decision and is consistent with the resolution.

Note on scope of this PR's diff: The diff contains changes unrelated to #342 (E2E tests, AnnotationShape, PdfControls, TranscriptionEditView tests). These should be attributed to their respective issues. From a requirements perspective, #342 is fully satisfied by the subset of changes in PersonMergePanel.svelte, edit/+page.svelte, the deletion of PersonDangerZone.svelte, and PersonMergePanel.svelte.spec.ts.

No open requirements gaps for #342. The delivery matches the resolution.

## 👨‍💻 Elicit — Requirements Engineer **Verdict: ✅ Approved** ### What I checked Acceptance criteria traceability, scope alignment with the Decision Queue resolution, and requirement completeness. ### Traceability against the resolved acceptance criteria The Decision Queue resolution clarified that the merge panel stays on the **edit page** and is made inline — not moved to the detail page. Mapping the diff against the resolved AC: | Acceptance criterion | Status | |---|---| | On `/persons/[id]/edit`, "Personen zusammenführen" is visible without expanding anything | ✅ `PersonDangerZone` accordion deleted; `PersonMergePanel` rendered directly | | Section has a clear danger indicator (red border) | ✅ `border-red-200` now intrinsic to `PersonMergePanel` | | Merge functionality unchanged | ✅ `?/merge` action, two-step confirm, `{#key}` state reset all preserved | | `PersonDangerZone` collapsible removed and deleted | ✅ File deleted, no dead code | ### Observations **Scope is tightly contained.** The change does exactly what the resolved Decision Queue specified — no more, no less. The detail page is untouched. The edit page loses the accordion and gains a directly rendered panel. **The irreversibility text question (Decision Queue Theme 3) was resolved as "keep the old text."** The existing `person_merge_warning` i18n key is unchanged. The panel still shows the irreversibility warning only in the confirmation step, not at the section level. This is a deliberate product decision and is consistent with the resolution. **Note on scope of this PR's diff:** The diff contains changes unrelated to #342 (E2E tests, `AnnotationShape`, `PdfControls`, `TranscriptionEditView` tests). These should be attributed to their respective issues. From a requirements perspective, #342 is fully satisfied by the subset of changes in `PersonMergePanel.svelte`, `edit/+page.svelte`, the deletion of `PersonDangerZone.svelte`, and `PersonMergePanel.svelte.spec.ts`. ### No open requirements gaps for #342. The delivery matches the resolution.
Author
Owner

👨‍💻 Nora "NullX" Steiner — Security Engineer

Verdict: Approved

What I checked

Authorization surface changes, action routing security, CSRF exposure, and any new attack surface introduced by the unrelated diff files.

Core change (#342): No new security concerns

The merge panel stays on the edit page. The ?/merge form action remains in edit/+page.server.ts, which already has the canWrite guard in its load function:

if (!canWrite) throw error(403, 'Forbidden');

The Decision Queue concern raised in issue #342 (defense-in-depth gap if the action moved to the detail page without a route-level guard) does not apply here because the action did not move. The existing guard is in place and unchanged.

PersonMergePanel itself has no authorization logic — correctly so. Authorization lives at the server action layer, not in the UI component.

No new API endpoints, no new form actions, no new parameters. The targetPersonId continues to be passed to the existing backend endpoint that enforces @RequirePermission(Permission.WRITE_ALL) independently.

Unrelated files in the diff — security notes

AnnotationShape.svelte — The new delete button calls onDeleteRequest?.() with e.stopPropagation(). No XSS risk: no user-controlled content is rendered via innerHTML. The aria-label="Löschen" is hardcoded, not user-supplied. Clean.

PdfControls.svelte — The text-accenttext-primary token change is a CSS concern only. No security impact.

person-typeahead.spec.ts — E2E test file, no production security surface.

No security issues found. The authorization boundary is intact.

## 👨‍💻 Nora "NullX" Steiner — Security Engineer **Verdict: ✅ Approved** ### What I checked Authorization surface changes, action routing security, CSRF exposure, and any new attack surface introduced by the unrelated diff files. ### Core change (#342): No new security concerns The merge panel stays on the edit page. The `?/merge` form action remains in `edit/+page.server.ts`, which already has the `canWrite` guard in its `load` function: ```typescript if (!canWrite) throw error(403, 'Forbidden'); ``` The Decision Queue concern raised in issue #342 (defense-in-depth gap if the action moved to the detail page without a route-level guard) **does not apply here** because the action did not move. The existing guard is in place and unchanged. **`PersonMergePanel` itself has no authorization logic** — correctly so. Authorization lives at the server action layer, not in the UI component. **No new API endpoints, no new form actions, no new parameters.** The `targetPersonId` continues to be passed to the existing backend endpoint that enforces `@RequirePermission(Permission.WRITE_ALL)` independently. ### Unrelated files in the diff — security notes **`AnnotationShape.svelte`** — The new delete button calls `onDeleteRequest?.()` with `e.stopPropagation()`. No XSS risk: no user-controlled content is rendered via innerHTML. The `aria-label="Löschen"` is hardcoded, not user-supplied. Clean. **`PdfControls.svelte`** — The `text-accent` → `text-primary` token change is a CSS concern only. No security impact. **`person-typeahead.spec.ts`** — E2E test file, no production security surface. ### No security issues found. The authorization boundary is intact.
Author
Owner

👨‍💻 Sara Holt — QA Engineer

Verdict: ⚠️ Approved with concerns

What I checked

Test coverage completeness, TDD evidence, test quality, spec file correctness, and the broader test pyramid impact of this PR.


PersonMergePanel.svelte.spec.ts — new spec file

TDD evidence confirmed: Per the completion comment, the spec was written before the fix. 4 tests cover the key behaviors:

  1. Red border class on container
  2. Merge heading renders
  3. Merge button disabled when no target
  4. mergeError displayed when form.mergeError is set

This is the minimum viable test set for this component. Coverage is appropriate for the scope.

One quality concern: the PersonTypeahead mock syntax

vi.mock('$lib/components/PersonTypeahead.svelte', () => ({
    default: vi.fn().mockImplementation(() => ({
        $$: {},
        render: () => '<div></div>'
    }))
}));

This uses Svelte 3/4 internal conventions ($$). With vitest-browser-svelte against a real browser DOM in Svelte 5, this may work by accident but is not correct. If PersonTypeahead requires the $lib/api.server module (which is server-only), the mock prevents a module-resolution error — but the mock shape is wrong. A minimal correct Svelte 5 stub would be:

vi.mock('$lib/components/PersonTypeahead.svelte', () => ({
    default: () => null
}));

Suggestion (not a blocker): Add a test for the two-step confirmation flow — "after selecting a target and clicking Zusammenführen, a confirmation message appears." This is the UX behavior most likely to regress if the internal showMergeConfirm state logic changes, and it is not currently covered.


Unrelated test files in the diff

TranscriptionEditView.svelte.spec.ts — 6 new tests for the bulk-mark-reviewed feature (issue #345). These look correct in structure. Test names are descriptive. The in-flight disablement test is good. These belong to #345, not #342.

PdfControls.svelte.spec.ts — 5 new tests including a WCAG contrast class assertion. The contrast test (expect(annotationBtn!.className).toContain('text-primary')) is a class-name assertion, not a true contrast ratio check — it verifies the fix was applied but not that text-primary actually passes WCAG AA. Acceptable as a regression guard, but note the limitation.

person-typeahead.spec.ts — E2E tests for issue #343. The page.waitForTimeout(400) calls are flaky-by-design: fixed sleeps for debounce timing. Should use waitForSelector('[role="listbox"]') instead. Not a blocker for this PR since these belong to #343.


Summary

The #342 test coverage is adequate. The mock syntax should be corrected. The confirmation-flow test is a gap worth addressing in a follow-up. The unrelated test files look structurally sound but belong to their respective issues.

## 👨‍💻 Sara Holt — QA Engineer **Verdict: ⚠️ Approved with concerns** ### What I checked Test coverage completeness, TDD evidence, test quality, spec file correctness, and the broader test pyramid impact of this PR. --- ### `PersonMergePanel.svelte.spec.ts` — new spec file **TDD evidence confirmed:** Per the completion comment, the spec was written before the fix. 4 tests cover the key behaviors: 1. Red border class on container ✅ 2. Merge heading renders ✅ 3. Merge button disabled when no target ✅ 4. `mergeError` displayed when `form.mergeError` is set ✅ This is the minimum viable test set for this component. Coverage is appropriate for the scope. **One quality concern: the `PersonTypeahead` mock syntax** ```typescript vi.mock('$lib/components/PersonTypeahead.svelte', () => ({ default: vi.fn().mockImplementation(() => ({ $$: {}, render: () => '<div></div>' })) })); ``` This uses Svelte 3/4 internal conventions (`$$`). With `vitest-browser-svelte` against a real browser DOM in Svelte 5, this may work by accident but is not correct. If `PersonTypeahead` requires the `$lib/api.server` module (which is server-only), the mock prevents a module-resolution error — but the mock shape is wrong. A minimal correct Svelte 5 stub would be: ```typescript vi.mock('$lib/components/PersonTypeahead.svelte', () => ({ default: () => null })); ``` **Suggestion (not a blocker):** Add a test for the two-step confirmation flow — "after selecting a target and clicking Zusammenführen, a confirmation message appears." This is the UX behavior most likely to regress if the internal `showMergeConfirm` state logic changes, and it is not currently covered. --- ### Unrelated test files in the diff **`TranscriptionEditView.svelte.spec.ts`** — 6 new tests for the bulk-mark-reviewed feature (issue #345). These look correct in structure. Test names are descriptive. The in-flight disablement test is good. These belong to #345, not #342. **`PdfControls.svelte.spec.ts`** — 5 new tests including a WCAG contrast class assertion. The contrast test (`expect(annotationBtn!.className).toContain('text-primary')`) is a class-name assertion, not a true contrast ratio check — it verifies the fix was applied but not that `text-primary` actually passes WCAG AA. Acceptable as a regression guard, but note the limitation. **`person-typeahead.spec.ts`** — E2E tests for issue #343. The `page.waitForTimeout(400)` calls are flaky-by-design: fixed sleeps for debounce timing. Should use `waitForSelector('[role="listbox"]')` instead. Not a blocker for this PR since these belong to #343. --- ### Summary The #342 test coverage is adequate. The mock syntax should be corrected. The confirmation-flow test is a gap worth addressing in a follow-up. The unrelated test files look structurally sound but belong to their respective issues.
Author
Owner

👨‍💻 Leonie Voss — UI/UX Design Lead

Verdict: ⚠️ Approved with concerns

What I checked

Brand token usage, accessibility, touch targets, danger signal visibility, mobile layout, and the senior-audience constraints (60+, 44px minimum touch targets, sufficient contrast).


The core change: danger indicator

border-red-200 is now intrinsic to the panel — correct direction. The danger signal no longer depends on the accordion wrapper. This satisfies the acceptance criterion: the red border is visible at all times on the section.

However, border-red-200 is a very light red (#fecaca in Tailwind). Against bg-surface (#E4E2D7), this is subtle. For a destructive-action section, the visual contrast of the danger signal matters. Consider border-red-300 or border-red-400 for better visibility, especially in bright ambient light on the senior audience's screens. This is a suggestion, not a blocker.


Missing persistent irreversibility hint (Medium concern)

The acceptance criterion says: "The section has a clear danger indicator (red border or warning icon + text e.g. 'Diese Aktion kann nicht rückgängig gemacht werden')."

The red border satisfies the OR condition. But the warning text only appears after the user selects a target and clicks "Zusammenführen" — it is part of the confirmation step, not visible on section load. A senior user who skips the confirmation step mentally (or dismisses it quickly) never sees a persistent warning.

Recommendation (suggestion, not blocker): Add a small persistent text hint above the form:

<p class="mb-4 text-sm text-red-600 flex items-center gap-1.5">
    <svg class="h-4 w-4 flex-shrink-0" aria-hidden="true"><!-- warning icon --></svg>
    {m.person_merge_danger_hint()}
</p>

New i18n key person_merge_danger_hint: "Diese Aktion kann nicht rückgängig gemacht werden." (distinct from the existing person_merge_warning used in the confirmation step).


AnnotationShape.svelte delete button — in-diff concern (Medium)

The new delete button uses inline style for all positioning:

style="
    min-width: 44px;
    min-height: 44px;
    ...
    background-color: #fff;
    border: 1px solid var(--color-error, #e53e3e);
    color: var(--color-error, #e53e3e);
"

Two issues:

  1. background-color: #fff hardcoded — should use var(--color-surface) or bg-white token so it works in dark mode.
  2. var(--color-error, #e53e3e) fallback#e53e3e is not in the project's brand palette. The project does not define a --color-error token in layout.css. Either define the token properly in layout.css, or use Tailwind's text-red-600 / border-red-600 utilities to stay consistent with the rest of the codebase (which uses border-red-200, text-red-600 in PersonDangerZone and now PersonMergePanel).

The touch target is min-width: 44px; min-height: 44px — correct for the senior audience. Good.


PdfControls.svelte token change — in-diff concern (Low)

text-accenttext-primary. text-accent maps to brand-mint (#A6DAD8). text-primary is not defined in the project's layout.css token set per CLAUDE.md. If text-primary is undefined, it falls through to nothing — the button would render with inherited text color rather than the intended highlight. Verify this token exists before merging.


Summary

The core #342 change meets the acceptance criteria (red border intrinsic, accordion gone). Two suggestions for the panel: slightly stronger red border for senior visibility, and a persistent irreversibility hint. One concern in the unrelated AnnotationShape change: replace hardcoded colors with project tokens.

## 👨‍💻 Leonie Voss — UI/UX Design Lead **Verdict: ⚠️ Approved with concerns** ### What I checked Brand token usage, accessibility, touch targets, danger signal visibility, mobile layout, and the senior-audience constraints (60+, 44px minimum touch targets, sufficient contrast). --- ### The core change: danger indicator **`border-red-200` is now intrinsic to the panel — correct direction.** The danger signal no longer depends on the accordion wrapper. This satisfies the acceptance criterion: the red border is visible at all times on the section. However, `border-red-200` is a very light red (`#fecaca` in Tailwind). Against `bg-surface` (`#E4E2D7`), this is subtle. For a destructive-action section, the visual contrast of the danger signal matters. Consider `border-red-300` or `border-red-400` for better visibility, especially in bright ambient light on the senior audience's screens. This is a suggestion, not a blocker. --- ### Missing persistent irreversibility hint (Medium concern) The acceptance criterion says: "The section has a clear danger indicator (red border or warning icon + text e.g. 'Diese Aktion kann nicht rückgängig gemacht werden')." The red border satisfies the OR condition. But the warning text only appears **after** the user selects a target and clicks "Zusammenführen" — it is part of the confirmation step, not visible on section load. A senior user who skips the confirmation step mentally (or dismisses it quickly) never sees a persistent warning. **Recommendation (suggestion, not blocker):** Add a small persistent text hint above the form: ```svelte <p class="mb-4 text-sm text-red-600 flex items-center gap-1.5"> <svg class="h-4 w-4 flex-shrink-0" aria-hidden="true"><!-- warning icon --></svg> {m.person_merge_danger_hint()} </p> ``` New i18n key `person_merge_danger_hint`: `"Diese Aktion kann nicht rückgängig gemacht werden."` (distinct from the existing `person_merge_warning` used in the confirmation step). --- ### `AnnotationShape.svelte` delete button — in-diff concern (Medium) The new delete button uses inline `style` for all positioning: ```svelte style=" min-width: 44px; min-height: 44px; ... background-color: #fff; border: 1px solid var(--color-error, #e53e3e); color: var(--color-error, #e53e3e); " ``` Two issues: 1. **`background-color: #fff` hardcoded** — should use `var(--color-surface)` or `bg-white` token so it works in dark mode. 2. **`var(--color-error, #e53e3e)` fallback** — `#e53e3e` is not in the project's brand palette. The project does not define a `--color-error` token in `layout.css`. Either define the token properly in `layout.css`, or use Tailwind's `text-red-600` / `border-red-600` utilities to stay consistent with the rest of the codebase (which uses `border-red-200`, `text-red-600` in `PersonDangerZone` and now `PersonMergePanel`). The touch target is `min-width: 44px; min-height: 44px` — correct for the senior audience. Good. --- ### `PdfControls.svelte` token change — in-diff concern (Low) `text-accent` → `text-primary`. `text-accent` maps to `brand-mint` (`#A6DAD8`). `text-primary` is not defined in the project's `layout.css` token set per CLAUDE.md. If `text-primary` is undefined, it falls through to nothing — the button would render with inherited text color rather than the intended highlight. Verify this token exists before merging. --- ### Summary The core #342 change meets the acceptance criteria (red border intrinsic, accordion gone). Two suggestions for the panel: slightly stronger red border for senior visibility, and a persistent irreversibility hint. One concern in the unrelated `AnnotationShape` change: replace hardcoded colors with project tokens.
marcel added 1 commit 2026-04-26 21:50:08 +02:00
feat(persons): show merge panel inline on edit page, remove Gefahrenzone accordion
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m2s
CI / OCR Service Tests (push) Successful in 33s
CI / Backend Unit Tests (push) Failing after 2m56s
CI / Unit & Component Tests (pull_request) Failing after 3m3s
CI / OCR Service Tests (pull_request) Successful in 31s
CI / Backend Unit Tests (pull_request) Failing after 2m56s
f162c9b55b
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>
marcel force-pushed feat/issue-342-person-merge-inline from 286a31af65 to f162c9b55b 2026-04-26 21:50:08 +02:00 Compare
Author
Owner

👨‍💻 Markus Keller — Application Architect

Verdict: Approved

What I checked

Module boundaries, component ownership, layer separation, dead code elimination, and the {#key} state-reset placement.

Findings

Scope is now clean. The previous iteration had unrelated files (issues #343, #345) bundled into the single commit. The diff is now exactly 4 files: PersonMergePanel.svelte, PersonMergePanel.svelte.spec.ts, edit/+page.svelte, and the deleted PersonDangerZone.svelte. This is the correct scope for #342.

Module boundary is correct. PersonMergePanel has always lived at persons/[id]/ — shared across the detail and edit sub-routes. The accordion wrapper (PersonDangerZone) was edit-page decoration, not a component in its own right. Deleting it and importing directly from '../PersonMergePanel.svelte' is the right call. No indirection remains.

{#key} placement is architecturally correct. The orchestrating page (edit/+page.svelte) controls state reset, not the child component. This is the right pattern: children declare their state; parents decide when to reset it. The PersonMergePanel correctly holds mergeTargetId and showMergeConfirm as local $state — it doesn't need to know it gets reset; the key block handles it declaratively from the outside.

Dead code is fully removed. PersonDangerZone.svelte deleted. No stubs, no commented-out imports, no orphaned i18n keys referenced only by the deleted component. The person_danger_zone_heading translation key that was used exclusively in PersonDangerZone is now unused — worth a cleanup issue, but not a blocker.

border-red-200 intrinsic to the panel. The danger signal is now part of the component's identity, not a decorator. This is the correct layering: a component that owns a destructive action should carry its own visual warning, not inherit it from a wrapper.

No blockers. The diff is clean and structurally sound.

## 👨‍💻 Markus Keller — Application Architect **Verdict: ✅ Approved** ### What I checked Module boundaries, component ownership, layer separation, dead code elimination, and the `{#key}` state-reset placement. ### Findings **Scope is now clean.** The previous iteration had unrelated files (issues #343, #345) bundled into the single commit. The diff is now exactly 4 files: `PersonMergePanel.svelte`, `PersonMergePanel.svelte.spec.ts`, `edit/+page.svelte`, and the deleted `PersonDangerZone.svelte`. This is the correct scope for #342. **Module boundary is correct.** `PersonMergePanel` has always lived at `persons/[id]/` — shared across the detail and edit sub-routes. The accordion wrapper (`PersonDangerZone`) was edit-page decoration, not a component in its own right. Deleting it and importing directly from `'../PersonMergePanel.svelte'` is the right call. No indirection remains. **`{#key}` placement is architecturally correct.** The orchestrating page (`edit/+page.svelte`) controls state reset, not the child component. This is the right pattern: children declare their state; parents decide when to reset it. The `PersonMergePanel` correctly holds `mergeTargetId` and `showMergeConfirm` as local `$state` — it doesn't need to know it gets reset; the key block handles it declaratively from the outside. **Dead code is fully removed.** `PersonDangerZone.svelte` deleted. No stubs, no commented-out imports, no orphaned i18n keys referenced only by the deleted component. The `person_danger_zone_heading` translation key that was used exclusively in `PersonDangerZone` is now unused — worth a cleanup issue, but not a blocker. **`border-red-200` intrinsic to the panel.** The danger signal is now part of the component's identity, not a decorator. This is the correct layering: a component that owns a destructive action should carry its own visual warning, not inherit it from a wrapper. ### No blockers. The diff is clean and structurally sound.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: Approved

What I checked

TDD evidence, Svelte 5 patterns, mock correctness, component size, naming, dead code, and diff cleanliness.


The core change

All four files are correct:

  • border-lineborder-red-200: one line, intrinsic, correct.
  • <PersonDangerZone>{#key person.id}<PersonMergePanel .../>{/key}: the state-reset wrapper is preserved and moved to the right level (the orchestrating page).
  • PersonDangerZone.svelte deleted: no dead code.
  • PersonMergePanel.svelte.spec.ts: 4 tests covering red border class, heading renders, button disabled when no target, and mergeError display.

Mock syntax — fixed correctly

The previous review flagged the Svelte 3/4 $$ internals mock:

// OLD — Svelte 3/4 syntax, incorrect in Svelte 5
vi.mock('$lib/components/PersonTypeahead.svelte', () => ({
    default: vi.fn().mockImplementation(() => ({ $$: {}, render: () => '<div></div>' }))
}));

This is now:

// NEW — Svelte 5 functional stub, correct
vi.mock('$lib/components/PersonTypeahead.svelte', () => ({
    default: () => null
}));

This is the right fix. A Svelte 5 component is a function; returning null produces an empty mount. The mock prevents module-resolution errors from $lib/api.server imports inside PersonTypeahead without introducing incorrect internal coupling.

Diff is clean

The unrelated files from issues #343 and #345 are no longer present. The single commit now contains only the 4 files belonging to #342. This is correct.

No open concerns. Tests pass (4/4 confirmed locally). The change is ready.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ✅ Approved** ### What I checked TDD evidence, Svelte 5 patterns, mock correctness, component size, naming, dead code, and diff cleanliness. --- ### The core change All four files are correct: - `border-line` → `border-red-200`: one line, intrinsic, correct. - `<PersonDangerZone>` → `{#key person.id}<PersonMergePanel .../>{/key}`: the state-reset wrapper is preserved and moved to the right level (the orchestrating page). - `PersonDangerZone.svelte` deleted: no dead code. - `PersonMergePanel.svelte.spec.ts`: 4 tests covering red border class, heading renders, button disabled when no target, and mergeError display. ### Mock syntax — fixed correctly The previous review flagged the Svelte 3/4 `$$` internals mock: ```typescript // OLD — Svelte 3/4 syntax, incorrect in Svelte 5 vi.mock('$lib/components/PersonTypeahead.svelte', () => ({ default: vi.fn().mockImplementation(() => ({ $$: {}, render: () => '<div></div>' })) })); ``` This is now: ```typescript // NEW — Svelte 5 functional stub, correct vi.mock('$lib/components/PersonTypeahead.svelte', () => ({ default: () => null })); ``` This is the right fix. A Svelte 5 component is a function; returning `null` produces an empty mount. The mock prevents module-resolution errors from `$lib/api.server` imports inside `PersonTypeahead` without introducing incorrect internal coupling. ### Diff is clean The unrelated files from issues #343 and #345 are no longer present. The single commit now contains only the 4 files belonging to #342. This is correct. ### No open concerns. Tests pass (4/4 confirmed locally). The change is ready.
Author
Owner

👨‍💻 Tobias Wendt — DevOps & Platform Engineer

Verdict: Approved

What I checked

Infrastructure impact, CI pipeline, SvelteKit routing, E2E test files, Docker Compose, and diff scope.

Findings

No infrastructure changes required. This is a pure frontend refactor. No new environment variables, no Docker Compose changes, no Caddy config updates, no new ports or services. Zero operational impact.

SvelteKit action routing is unaffected. ?/merge continues to be served by edit/+page.server.ts under /persons/[id]/edit. SvelteKit's file-based routing derives this automatically — no routing config exists to break.

Backend endpoint unchanged. POST /api/persons/{id}/merge is pre-existing. This PR adds no new API surface.

The unrelated E2E file is gone. The previous iteration included frontend/e2e/person-typeahead.spec.ts in this diff (issue #343 content). That file is no longer present. The E2E suite is unaffected by this PR.

Vitest runs are unaffected. The new PersonMergePanel.svelte.spec.ts integrates into the existing Vitest browser setup with no configuration changes. The corrected mock (default: () => null) won't require any vi.config.ts or alias changes.

CI pipeline: no changes needed. Existing Vitest and linting runs cover the new test file automatically. No new test runner config, no new artifact paths.

Nothing to block here from a platform perspective.

## 👨‍💻 Tobias Wendt — DevOps & Platform Engineer **Verdict: ✅ Approved** ### What I checked Infrastructure impact, CI pipeline, SvelteKit routing, E2E test files, Docker Compose, and diff scope. ### Findings **No infrastructure changes required.** This is a pure frontend refactor. No new environment variables, no Docker Compose changes, no Caddy config updates, no new ports or services. Zero operational impact. **SvelteKit action routing is unaffected.** `?/merge` continues to be served by `edit/+page.server.ts` under `/persons/[id]/edit`. SvelteKit's file-based routing derives this automatically — no routing config exists to break. **Backend endpoint unchanged.** `POST /api/persons/{id}/merge` is pre-existing. This PR adds no new API surface. **The unrelated E2E file is gone.** The previous iteration included `frontend/e2e/person-typeahead.spec.ts` in this diff (issue #343 content). That file is no longer present. The E2E suite is unaffected by this PR. **Vitest runs are unaffected.** The new `PersonMergePanel.svelte.spec.ts` integrates into the existing Vitest browser setup with no configuration changes. The corrected mock (`default: () => null`) won't require any `vi.config.ts` or alias changes. **CI pipeline: no changes needed.** Existing Vitest and linting runs cover the new test file automatically. No new test runner config, no new artifact paths. ### Nothing to block here from a platform perspective.
Author
Owner

👨‍💻 Elicit — Requirements Engineer

Verdict: Approved

What I checked

Acceptance criteria traceability against the Decision Queue resolution, scope alignment, i18n completeness, and requirement completeness.

Traceability against the resolved acceptance criteria

The Decision Queue resolution on #342 specified: merge panel stays on the edit page, made inline (not moved to detail page). Mapping the current diff:

Acceptance criterion Status
On /persons/[id]/edit, "Personen zusammenführen" is visible without expanding anything PersonDangerZone accordion deleted; PersonMergePanel rendered directly
Section has a clear danger indicator (red border) border-red-200 now intrinsic to PersonMergePanel
Merge functionality unchanged (two-step confirm, ?/merge action, redirect on success) All preserved — only the wrapper was removed
PersonDangerZone collapsible removed and deleted File deleted, no dead code

Scope is now correct

The previous iteration bundled unrelated changes from issues #343, #345. The diff now contains exactly the 4 files required by #342. The scope matches the issue specification precisely.

One minor observation: orphaned i18n key

person_danger_zone_heading was used exclusively in the now-deleted PersonDangerZone.svelte. That translation key is now unreferenced. This is not a functional defect — unused i18n keys do not cause errors — but it is requirements debt. Recommend filing a small cleanup issue to remove it from de.json, en.json, and es.json to keep the translation files accurate.

No open requirements gaps for #342. The delivery matches the resolution.

## 👨‍💻 Elicit — Requirements Engineer **Verdict: ✅ Approved** ### What I checked Acceptance criteria traceability against the Decision Queue resolution, scope alignment, i18n completeness, and requirement completeness. ### Traceability against the resolved acceptance criteria The Decision Queue resolution on #342 specified: merge panel stays on the edit page, made inline (not moved to detail page). Mapping the current diff: | Acceptance criterion | Status | |---|---| | On `/persons/[id]/edit`, "Personen zusammenführen" is visible without expanding anything | ✅ `PersonDangerZone` accordion deleted; `PersonMergePanel` rendered directly | | Section has a clear danger indicator (red border) | ✅ `border-red-200` now intrinsic to `PersonMergePanel` | | Merge functionality unchanged (two-step confirm, `?/merge` action, redirect on success) | ✅ All preserved — only the wrapper was removed | | `PersonDangerZone` collapsible removed and deleted | ✅ File deleted, no dead code | ### Scope is now correct The previous iteration bundled unrelated changes from issues #343, #345. The diff now contains exactly the 4 files required by #342. The scope matches the issue specification precisely. ### One minor observation: orphaned i18n key `person_danger_zone_heading` was used exclusively in the now-deleted `PersonDangerZone.svelte`. That translation key is now unreferenced. This is not a functional defect — unused i18n keys do not cause errors — but it is requirements debt. Recommend filing a small cleanup issue to remove it from `de.json`, `en.json`, and `es.json` to keep the translation files accurate. ### No open requirements gaps for #342. The delivery matches the resolution.
Author
Owner

👨‍💻 Nora "NullX" Steiner — Security Engineer

Verdict: Approved

What I checked

Authorization surface changes, action routing security, CSRF exposure, server action guard presence, and new attack surface in the changed files.

Core change (#342): No new security concerns

The merge panel stays on the edit page. The ?/merge form action remains in edit/+page.server.ts, which carries the existing canWrite guard. The Decision Queue concern from the previous review cycle (defense-in-depth gap if the action moved to the detail page without a route-level guard) does not apply — the action did not move.

Authorization layer is intact:

  • Route-level guard: canWrite check in edit/+page.server.ts load function returns 403 before the page renders.
  • Action-level guard: ?/merge action in the same file inherits the same session context.
  • Backend: POST /api/persons/{id}/merge enforces @RequirePermission(Permission.WRITE_ALL) independently.

Defense in depth is preserved across all three layers.

No new API endpoints, no new form action parameters. targetPersonId is unchanged; it continues to be validated server-side. The PersonTypeahead mock in the spec file (default: () => null) returns null — no user-controlled data renders through it in tests, and in production the real component is type-safe.

PersonMergePanel correctly has no authorization logic. Authorization belongs at the server action layer. The component renders the form UI only — correct separation.

Diff scope is clean

The previous iteration included AnnotationShape.svelte changes in this diff. Those changes are no longer present. The security surface of this PR is limited to the merge panel UI refactor and its spec file.

No security issues found.

## 👨‍💻 Nora "NullX" Steiner — Security Engineer **Verdict: ✅ Approved** ### What I checked Authorization surface changes, action routing security, CSRF exposure, server action guard presence, and new attack surface in the changed files. ### Core change (#342): No new security concerns The merge panel stays on the edit page. The `?/merge` form action remains in `edit/+page.server.ts`, which carries the existing `canWrite` guard. The Decision Queue concern from the previous review cycle (defense-in-depth gap if the action moved to the detail page without a route-level guard) does **not apply** — the action did not move. **Authorization layer is intact:** - Route-level guard: `canWrite` check in `edit/+page.server.ts` load function returns 403 before the page renders. - Action-level guard: `?/merge` action in the same file inherits the same session context. - Backend: `POST /api/persons/{id}/merge` enforces `@RequirePermission(Permission.WRITE_ALL)` independently. Defense in depth is preserved across all three layers. **No new API endpoints, no new form action parameters.** `targetPersonId` is unchanged; it continues to be validated server-side. The `PersonTypeahead` mock in the spec file (`default: () => null`) returns `null` — no user-controlled data renders through it in tests, and in production the real component is type-safe. **`PersonMergePanel` correctly has no authorization logic.** Authorization belongs at the server action layer. The component renders the form UI only — correct separation. ### Diff scope is clean The previous iteration included `AnnotationShape.svelte` changes in this diff. Those changes are no longer present. The security surface of this PR is limited to the merge panel UI refactor and its spec file. ### No security issues found.
Author
Owner

👨‍💻 Sara Holt — QA Engineer

Verdict: Approved

What I checked

TDD evidence, spec file quality, mock correctness, test coverage completeness, and test pyramid placement.


PersonMergePanel.svelte.spec.ts — assessment

4 tests, all behavioral, all well-named:

  1. renders outer container with red border class — verifies the danger signal is intrinsic to the component. DOM assertion via classList.contains is appropriate here because the test's explicit purpose is to assert a CSS class contract.
  2. renders merge headinggetByRole('heading', { level: 2 }) is correct: tests what the user sees, not internals.
  3. merge button is disabled when no target selectedgetByRole('button', { name: /zusammenführen/i }) + toBeDisabled() is the right assertion for this behavior.
  4. renders mergeError when form contains errorgetByText('...') is correct for displayed error text.

Mock syntax is now correct. The previous $$-based Svelte 3/4 mock has been replaced with default: () => null, which is the correct Svelte 5 functional stub pattern. This prevents module-resolution errors from PersonTypeahead's server-only imports without introducing fragile internal coupling.

Factory function is idiomatic. makePerson() with spreaded overrides follows the project pattern.

afterEach(cleanup) is present. No test contamination between cases.

Coverage gaps — suggestions (not blockers)

The two-step confirmation flow is not covered. The sequence: (1) select a target → (2) click "Zusammenführen" → (3) confirmation message appears with showMergeConfirm = true is the primary UX regression risk in this component. It is not currently tested. This is the most important missing test; recommend a follow-up.

showMergeConfirm resets when target changes — not covered. When the user selects a new target after the confirmation is showing, showMergeConfirm should reset to false. Worth a test to guard against regression.

Both are suggestions for a follow-up issue, not blockers on this PR. The minimum viable test set for the acceptance criteria is present and correct.

Diff scope

The unrelated test files (TranscriptionEditView.svelte.spec.ts, PdfControls.svelte.spec.ts, person-typeahead.spec.ts) are no longer in this diff. The test changes are now scoped to #342 only.

No blockers. The spec file is clean and the mock fix is correct.

## 👨‍💻 Sara Holt — QA Engineer **Verdict: ✅ Approved** ### What I checked TDD evidence, spec file quality, mock correctness, test coverage completeness, and test pyramid placement. --- ### `PersonMergePanel.svelte.spec.ts` — assessment **4 tests, all behavioral, all well-named:** 1. `renders outer container with red border class` — verifies the danger signal is intrinsic to the component. DOM assertion via `classList.contains` is appropriate here because the test's explicit purpose is to assert a CSS class contract. 2. `renders merge heading` — `getByRole('heading', { level: 2 })` is correct: tests what the user sees, not internals. 3. `merge button is disabled when no target selected` — `getByRole('button', { name: /zusammenführen/i })` + `toBeDisabled()` is the right assertion for this behavior. 4. `renders mergeError when form contains error` — `getByText('...')` is correct for displayed error text. **Mock syntax is now correct.** The previous `$$`-based Svelte 3/4 mock has been replaced with `default: () => null`, which is the correct Svelte 5 functional stub pattern. This prevents module-resolution errors from `PersonTypeahead`'s server-only imports without introducing fragile internal coupling. **Factory function is idiomatic.** `makePerson()` with spreaded overrides follows the project pattern. **`afterEach(cleanup)` is present.** No test contamination between cases. ### Coverage gaps — suggestions (not blockers) **The two-step confirmation flow is not covered.** The sequence: (1) select a target → (2) click "Zusammenführen" → (3) confirmation message appears with `showMergeConfirm = true` is the primary UX regression risk in this component. It is not currently tested. This is the most important missing test; recommend a follow-up. **`showMergeConfirm` resets when target changes** — not covered. When the user selects a new target after the confirmation is showing, `showMergeConfirm` should reset to `false`. Worth a test to guard against regression. Both are suggestions for a follow-up issue, not blockers on this PR. The minimum viable test set for the acceptance criteria is present and correct. ### Diff scope The unrelated test files (`TranscriptionEditView.svelte.spec.ts`, `PdfControls.svelte.spec.ts`, `person-typeahead.spec.ts`) are no longer in this diff. The test changes are now scoped to #342 only. ### No blockers. The spec file is clean and the mock fix is correct.
Author
Owner

👨‍💻 Leonie Voss — UI/UX Design Lead

Verdict: Approved

What I checked

Brand token usage, danger signal visibility, touch targets, accessibility, mobile layout, and the senior-audience constraints (60+, 44px minimum, sufficient contrast).


Core change: danger indicator

border-red-200 intrinsic to the panel — correct direction. The danger signal is no longer conditional on an accordion being open. The red border is visible at all times on the section, which satisfies the acceptance criterion.

border-red-200 contrast note (suggestion, not blocker). Tailwind's red-200 is #fecaca — a very light red. Against bg-surface (#E4E2D7), the border contrast is subtle. For a destructive-action section on a senior audience's screen (bright ambient light, 60+ eyes), border-red-300 or border-red-400 would provide meaningfully better visual differentiation. This is a design refinement, not a blocker.

Accessibility assessment

Touch targets on buttons: The "Zusammenführen" button uses px-4 py-2 — actual rendered height is approximately 36–38px, below the 44px minimum for the senior audience (WCAG 2.2 SC 2.5.8). Consider py-3 (48px height) for both the initial button and the confirm/cancel buttons in the two-step flow.

The confirmation warning text is semantic. <p class="... text-red-700"> wraps the irreversibility warning in the confirmation step. text-red-700 on bg-red-50 passes WCAG AA contrast (approximately 5.9:1). Good.

No color-only danger signal. The red border serves as a visual cue alongside the heading text "Personen zusammenführen" — the function is communicated by text, not color alone. This is correct.

Focus rings: The buttons use Tailwind's default focus handling. No explicit focus-visible:ring-* is set on the merge buttons — worth adding for keyboard navigability, especially for the senior audience who may rely on keyboard. Suggestion only.

Persistent irreversibility hint (suggestion, not blocker)

The irreversibility warning (person_merge_warning) only appears after the user has already selected a target and clicked the first button. A senior user who skims the section may not see it. The acceptance criterion is met by the red border alone (the OR condition), but a small persistent note — e.g. text-sm text-red-500 under the section heading — would strengthen the danger signal for the target audience. This is a product enhancement for a follow-up, not a blocker on this PR.

Diff scope

The unrelated AnnotationShape.svelte changes (with inline-style color concerns raised in the previous review) are no longer in this diff. The in-diff concerns from the previous cycle are resolved by scope cleanup.

The acceptance criteria are met. Suggestions are follow-up polish items.

## 👨‍💻 Leonie Voss — UI/UX Design Lead **Verdict: ✅ Approved** ### What I checked Brand token usage, danger signal visibility, touch targets, accessibility, mobile layout, and the senior-audience constraints (60+, 44px minimum, sufficient contrast). --- ### Core change: danger indicator **`border-red-200` intrinsic to the panel — correct direction.** The danger signal is no longer conditional on an accordion being open. The red border is visible at all times on the section, which satisfies the acceptance criterion. **`border-red-200` contrast note (suggestion, not blocker).** Tailwind's `red-200` is `#fecaca` — a very light red. Against `bg-surface` (`#E4E2D7`), the border contrast is subtle. For a destructive-action section on a senior audience's screen (bright ambient light, 60+ eyes), `border-red-300` or `border-red-400` would provide meaningfully better visual differentiation. This is a design refinement, not a blocker. ### Accessibility assessment **Touch targets on buttons:** The "Zusammenführen" button uses `px-4 py-2` — actual rendered height is approximately 36–38px, below the 44px minimum for the senior audience (WCAG 2.2 SC 2.5.8). Consider `py-3` (48px height) for both the initial button and the confirm/cancel buttons in the two-step flow. **The confirmation warning text is semantic.** `<p class="... text-red-700">` wraps the irreversibility warning in the confirmation step. `text-red-700` on `bg-red-50` passes WCAG AA contrast (approximately 5.9:1). Good. **No color-only danger signal.** The red border serves as a visual cue alongside the heading text "Personen zusammenführen" — the function is communicated by text, not color alone. This is correct. **Focus rings:** The buttons use Tailwind's default focus handling. No explicit `focus-visible:ring-*` is set on the merge buttons — worth adding for keyboard navigability, especially for the senior audience who may rely on keyboard. Suggestion only. ### Persistent irreversibility hint (suggestion, not blocker) The irreversibility warning (`person_merge_warning`) only appears after the user has already selected a target and clicked the first button. A senior user who skims the section may not see it. The acceptance criterion is met by the red border alone (the OR condition), but a small persistent note — e.g. `text-sm text-red-500` under the section heading — would strengthen the danger signal for the target audience. This is a product enhancement for a follow-up, not a blocker on this PR. ### Diff scope The unrelated `AnnotationShape.svelte` changes (with inline-style color concerns raised in the previous review) are no longer in this diff. The in-diff concerns from the previous cycle are resolved by scope cleanup. ### The acceptance criteria are met. Suggestions are follow-up polish items.
marcel merged commit 108edff8d2 into main 2026-04-26 21:54:46 +02:00
marcel deleted branch feat/issue-342-person-merge-inline 2026-04-26 21:54:47 +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#347