feat: Remove the Briefwechsel view; retarget its links to document search (#716) #721

Merged
marcel merged 13 commits from feat/issue-716-remove-briefwechsel into main 2026-06-03 10:26:56 +02:00
Owner

Closes #716.

Removes the standalone Briefwechsel view (/briefwechsel) entirely — frontend and backend — and retargets its one inbound link (the "Häufige Korrespondenten" card) into the existing document search. Grep-driven removal per the spec; the "no dangling references" AC holds.

Behaviour change (deliberate, see ADR-030)

The card link now deep-links to /documents?senderId=A&receiverId=B, which filters sender AND receiver → shows only the A→B direction. The old bidirectional view's reverse direction (replies) is intentionally dropped; a bidirectional search filter is a separate future enhancement. The ×N badge stays bilateral (shared-letters signal) with a clarifying title.

Commits (11, atomic)

  • feat(persons) retarget card → document search (new personName prop, both-names search title, magnifier icon, badge title, new person_correspondents_search_* i18n keys, test updated)
  • feat(briefwechsel) delete the route in full + e2e specs/fixture + the stale korrespondenz.spec.ts (grep-caught, targeted the route's former /korrespondenz path)
  • test active briefwechsel-removed.spec.ts (404 guard) + cleaned stammbaum.spec.ts / auth.spec.ts
  • feat(i18n) remove 22 orphaned keys/locale (kept conv_sort_newest/oldest)
  • refactor(document) move ConversationThumbnaillib/document/, delete empty lib/conversation/
  • feat(document) remove endpoint, service methods (+ dead 2-arg getConversation), repo queries, with tests
  • chore(api) trim the generated TS client
  • docs route tables, ARCHITECTURE.md, C4 diagrams, GLOSSARY.md, superseded design specs
  • docs(adr) ADR-030 records the bilateral→unidirectional regression

Verification

  • Backend clean packageBUILD SUCCESS; DocumentServiceTest (169) + DocumentRepositoryTest (29) green.
  • Prettier + ESLint clean on every commit.
  • "No dangling references" grep across frontend/src, frontend/e2e, backend/src clean — only intentional matches: the new 404 guard and a comment in the immutable Flyway migration V62__index_fk_columns.sql (its FK indexes still serve document search).
  • Browser/e2e assertions run on CI per project convention.

Notes

  • ADR number is 030 (028/029 were taken since the spec was written).
  • No briefwechsel-*.png baselines existed → that cleanup was a no-op.
  • The 7 other docs/specs/*.html mockups that only show Briefwechsel in static nav chrome are left as frozen point-in-time artifacts; only the two Briefwechsel-specific specs were superseded.
  • npm run generate:api was not run against a live backend (needs the full stack); the generated client was surgically trimmed to exactly the removed endpoint — worth a regen on CI to confirm.

🤖 Generated with Claude Code

Closes #716. Removes the standalone **Briefwechsel** view (`/briefwechsel`) entirely — frontend and backend — and retargets its one inbound link (the "Häufige Korrespondenten" card) into the existing document search. Grep-driven removal per the spec; the "no dangling references" AC holds. ## Behaviour change (deliberate, see ADR-030) The card link now deep-links to `/documents?senderId=A&receiverId=B`, which filters **sender AND receiver** → shows only the **A→B** direction. The old bidirectional view's reverse direction (replies) is **intentionally dropped**; a bidirectional search filter is a separate future enhancement. The ×N badge stays bilateral (shared-letters signal) with a clarifying `title`. ## Commits (11, atomic) - `feat(persons)` retarget card → document search (new `personName` prop, both-names search title, magnifier icon, badge title, new `person_correspondents_search_*` i18n keys, test updated) - `feat(briefwechsel)` delete the route in full + e2e specs/fixture **+ the stale `korrespondenz.spec.ts`** (grep-caught, targeted the route's former `/korrespondenz` path) - `test` active `briefwechsel-removed.spec.ts` (404 guard) + cleaned `stammbaum.spec.ts` / `auth.spec.ts` - `feat(i18n)` remove 22 orphaned keys/locale (kept `conv_sort_newest/oldest`) - `refactor(document)` move `ConversationThumbnail` → `lib/document/`, delete empty `lib/conversation/` - `feat(document)` remove endpoint, service methods (+ dead 2-arg `getConversation`), repo queries, with tests - `chore(api)` trim the generated TS client - `docs` route tables, `ARCHITECTURE.md`, C4 diagrams, `GLOSSARY.md`, superseded design specs - `docs(adr)` **ADR-030** records the bilateral→unidirectional regression ## Verification - Backend `clean package` → **BUILD SUCCESS**; `DocumentServiceTest` (169) + `DocumentRepositoryTest` (29) green. - Prettier + ESLint clean on every commit. - "No dangling references" grep across `frontend/src`, `frontend/e2e`, `backend/src` clean — only intentional matches: the new 404 guard and a comment in the immutable Flyway migration `V62__index_fk_columns.sql` (its FK indexes still serve document search). - Browser/e2e assertions run on CI per project convention. ## Notes - ADR number is **030** (028/029 were taken since the spec was written). - No `briefwechsel-*.png` baselines existed → that cleanup was a no-op. - The 7 other `docs/specs/*.html` mockups that only show Briefwechsel in static nav chrome are left as frozen point-in-time artifacts; only the two Briefwechsel-specific specs were superseded. - `npm run generate:api` was not run against a live backend (needs the full stack); the generated client was surgically trimmed to exactly the removed endpoint — worth a regen on CI to confirm. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
marcel added 11 commits 2026-06-02 20:53:28 +02:00
The "Häufige Korrespondenten" card linked into the standalone Briefwechsel
view. Retarget each chip to the existing document search pre-filtered by
sender and receiver (/documents?senderId=A&receiverId=B), naming both
persons in a search-action title, swapping the chat-bubble icon for a
magnifier, and clarifying that the ×N badge counts shared letters in both
directions (not the unidirectional search result count).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete the /briefwechsel route in full (page, server load, eight
components and all co-located unit tests) and its end-to-end coverage
(briefwechsel-rows.visual, briefwechsel-a11y, the bilateral-correspondence
fixture, and the stale korrespondenz spec which targeted the route's
former /korrespondenz path). The card link now deep-links into document
search, so this view has no remaining inbound references.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an active e2e spec asserting /briefwechsel 404s on the styled app
error page. The old assertion lived in stammbaum.spec.ts inside a
test.skip() block (never executed) and asserted the opposite — remove it.
Drop /briefwechsel from the auth protected-route loop; /documents (the
redirect target) sits behind the same authenticated() rule, so coverage
is preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the 22 message keys that only the deleted Briefwechsel view used
(conv_* except the still-used conv_sort_newest/oldest, plus
nav_conversations, doc_conversation_title and person_correspondents_hint,
all now superseded by the retargeted card's new search keys).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With the Briefwechsel view gone, lib/conversation/ held a single shared
component whose only consumer is lib/document/ThumbnailRow. Move it (and
its spec) into lib/document/, update the import, delete the now-empty
lib/conversation/ folder, and fix the stale frontend/CLAUDE.md lib map.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete GET /api/documents/conversation and its controller handler — the
only client was the removed Briefwechsel view. Drops the now-unused Sort
import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete getConversationFiltered (the endpoint's only caller is gone) and
the dead 2-arg getConversation(personA, personB) which had zero callers,
along with both getConversationFiltered test blocks. The hasSender/
hasReceiver specifications stay — document search still uses them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete findConversation and findSinglePersonCorrespondence (no remaining
callers after the service methods were removed) and their integration
test section. Drops the now-unused LocalDate import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the /api/documents/conversation path and its getConversation
operation from the generated client to match the removed backend
endpoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the Briefwechsel route and the conversation derived-domain /
conversation-thread prose from the route tables (CLAUDE.md,
frontend/CLAUDE.md), ARCHITECTURE.md, the C4 frontend/backend diagrams,
and GLOSSARY.md (term + derived-domain list). Delete the two superseded
Briefwechsel design specs. Historical ADRs and dated analyses are left
untouched as point-in-time context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs(adr): record the bilateral->unidirectional search regression (ADR-030)
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m51s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m33s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m4s
36457f28ff
Removing the Briefwechsel view retargets its one inbound link to document
search, which filters sender AND receiver — A->B only. The bidirectional
"replies" direction is intentionally dropped. ADR-030 records the
context, decision and consequences, and notes a bidirectional search
filter as the superseding future enhancement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
Owner

🏛️ Markus Keller — Senior Application Architect

Verdict: ⚠️ Approved with concerns

I reviewed this as a clean, grep-driven feature removal: does the dead code go away completely, do the layer boundaries stay intact, and — most important for me — do the docs match the code after the change?

What's done well

  • Layering is respected. The endpoint was removed at all three layers (controller → service → repository) in lockstep, with the unused imports (Sort in the controller, LocalDate in the repository) cleaned up. No dangling service method, no dormant repository query. This is exactly how a removal should look.
  • The shared hasSender/hasReceiver specifications are correctly preserved — they still back document search (DocumentService.java:557-558). The ADR explicitly records this. Good discipline; an over-eager removal would have broken search.
  • Documentation currency is excellent — and for me that is normally where removals rot. The PR updated both route tables (CLAUDE.md + frontend/CLAUDE.md), ARCHITECTURE.md (derived-domain prose), both C4 component descriptions (l3-backend-3b, l3-frontend-3c — including the in-prose endpoint mentions, not just nodes), the c4-diagrams.md mirror, both GLOSSARY touchpoints, and superseded the two design specs. That is the full grep set from the issue.
  • ADR-030 is well-formed — Context / Decision / Consequences, names the regression honestly, and correctly notes "no schema change, no migration." The 030 numbering (028/029 taken) is a sensible call.

Concerns (non-blocking)

  1. frontend/e2e/CLAUDE.md:28 was missed. It still lists korrespondenz.spec.ts # Correspondence timeline — but that spec file was deleted in this PR. This is a stale documentation reference that my doc-currency rule treats as a real omission. It's a one-line fix in a doc, so I'm filing it as a concern rather than a hard blocker, but it should be cleaned up: the "no dangling references" AC is the spec's headline criterion and this is a (doc-level) dangling reference.
  2. api.ts was hand-trimmed, not regenerated. The PR notes this and asks for a CI regen to confirm. Architecturally I'd prefer the generated artifact never be edited by hand — but the surgical removal here is exactly the diff a regen would produce (one path + one operation), so the risk is contained. Confirm on CI.

No accidental complexity introduced, no boundary violations, no missing DB-layer concern (tables untouched). Once the e2e/CLAUDE.md line is fixed, this is a clean approve from the architecture side.

## 🏛️ Markus Keller — Senior Application Architect **Verdict: ⚠️ Approved with concerns** I reviewed this as a clean, grep-driven feature removal: does the dead code go away completely, do the layer boundaries stay intact, and — most important for me — **do the docs match the code after the change?** ### What's done well - **Layering is respected.** The endpoint was removed at all three layers (controller → service → repository) in lockstep, with the unused imports (`Sort` in the controller, `LocalDate` in the repository) cleaned up. No dangling service method, no dormant repository query. This is exactly how a removal should look. - **The shared `hasSender`/`hasReceiver` specifications are correctly preserved** — they still back document search (`DocumentService.java:557-558`). The ADR explicitly records this. Good discipline; an over-eager removal would have broken search. - **Documentation currency is excellent** — and for me that is normally where removals rot. The PR updated *both* route tables (`CLAUDE.md` + `frontend/CLAUDE.md`), `ARCHITECTURE.md` (derived-domain prose), both C4 component descriptions (`l3-backend-3b`, `l3-frontend-3c` — including the in-prose endpoint mentions, not just nodes), the `c4-diagrams.md` mirror, both GLOSSARY touchpoints, and superseded the two design specs. That is the full grep set from the issue. - **ADR-030 is well-formed** — Context / Decision / Consequences, names the regression honestly, and correctly notes "no schema change, no migration." The 030 numbering (028/029 taken) is a sensible call. ### Concerns (non-blocking) 1. **`frontend/e2e/CLAUDE.md:28` was missed.** It still lists `korrespondenz.spec.ts # Correspondence timeline` — but that spec file was deleted in this PR. This is a stale documentation reference that my doc-currency rule treats as a real omission. It's a one-line fix in a doc, so I'm filing it as a concern rather than a hard blocker, but it should be cleaned up: the "no dangling references" AC is the spec's headline criterion and this is a (doc-level) dangling reference. 2. **`api.ts` was hand-trimmed, not regenerated.** The PR notes this and asks for a CI regen to confirm. Architecturally I'd prefer the generated artifact never be edited by hand — but the surgical removal here is exactly the diff a regen would produce (one path + one operation), so the risk is contained. Confirm on CI. No accidental complexity introduced, no boundary violations, no missing DB-layer concern (tables untouched). Once the `e2e/CLAUDE.md` line is fixed, this is a clean approve from the architecture side.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: Approved

This is a clean deletion-heavy PR (5433 lines out, 137 in). I focused on the code that survives — the retargeted card — and on whether the tests were updated honestly rather than just made to pass.

What I like

  • The CoCorrespondentsList change is textbook props discipline. A new personName: string prop threads person A's name in from the parent (+page.svelte:71 passes person.displayName), instead of reaching for a store or refetching. Specific domain-named props, not a data blob. 👍
  • Tests were rewritten, not patched into silence. CoCorrespondentsList.svelte.test.ts updates all three stale spots the issue called out: the href assertion (/documents?...), the hint-text assertion, and the personName prop on every render(). The new test labels the link as a search action naming both persons asserts the title contains both names via regex — and it imports m from paraglide instead of retyping the German copy, which is exactly right (the copy lives in one place).
  • i18n is clean. New keys (person_correspondents_search_title/_hint/_badge_title) added to all three locales with the {A}/{B} placeholders matched to usage; 22 orphaned conv_*/nav_conversations/doc_conversation_title/person_correspondents_hint keys removed — and conv_sort_newest/conv_sort_oldest correctly kept (still consumed by PersonDocumentList). Usages were rewritten before keys were deleted, so no key-first build break.
  • The lib/conversation/lib/document/ move is correct. Single surviving component, single consumer (ThumbnailRow.svelte:58), import updated, empty folder deleted, and the frontend/CLAUDE.md lib description line removed. No leftover empty package.
  • Backend deletion is tidy — guard-clause-free dead methods gone, unused imports removed, both DocumentServiceTest blocks and the DocumentRepositoryTest section removed (grep confirms zero residual references to the removed method names). The PR reports clean package green.

Minor (not blocking)

  • The ×N badge tooltip is a title attribute on a <span>. Functionally fine and matches the spec's "don't recompute, just clarify" decision. No change requested.

TDD-wise I can't see the red phase from the diff, but the test deltas line up precisely with the behavior changes and nothing looks reverse-engineered to pass. Solid, focused work. Approve.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ✅ Approved** This is a clean deletion-heavy PR (5433 lines out, 137 in). I focused on the code that *survives* — the retargeted card — and on whether the tests were updated honestly rather than just made to pass. ### What I like - **The `CoCorrespondentsList` change is textbook props discipline.** A new `personName: string` prop threads person A's name in from the parent (`+page.svelte:71` passes `person.displayName`), instead of reaching for a store or refetching. Specific domain-named props, not a `data` blob. 👍 - **Tests were rewritten, not patched into silence.** `CoCorrespondentsList.svelte.test.ts` updates all three stale spots the issue called out: the href assertion (`/documents?...`), the hint-text assertion, and the `personName` prop on every `render()`. The new test `labels the link as a search action naming both persons` asserts the `title` contains both names via regex — and it imports `m` from paraglide instead of retyping the German copy, which is exactly right (the copy lives in one place). - **i18n is clean.** New keys (`person_correspondents_search_title/_hint/_badge_title`) added to all three locales with the `{A}`/`{B}` placeholders matched to usage; 22 orphaned `conv_*`/`nav_conversations`/`doc_conversation_title`/`person_correspondents_hint` keys removed — and `conv_sort_newest`/`conv_sort_oldest` correctly **kept** (still consumed by `PersonDocumentList`). Usages were rewritten before keys were deleted, so no key-first build break. - **The `lib/conversation/` → `lib/document/` move is correct.** Single surviving component, single consumer (`ThumbnailRow.svelte:58`), import updated, empty folder deleted, and the `frontend/CLAUDE.md` lib description line removed. No leftover empty package. - **Backend deletion is tidy** — guard-clause-free dead methods gone, unused imports removed, both `DocumentServiceTest` blocks and the `DocumentRepositoryTest` section removed (grep confirms zero residual references to the removed method names). The PR reports `clean package` green. ### Minor (not blocking) - The `×N` badge tooltip is a `title` attribute on a `<span>`. Functionally fine and matches the spec's "don't recompute, just clarify" decision. No change requested. TDD-wise I can't see the red phase from the diff, but the test deltas line up precisely with the behavior changes and nothing looks reverse-engineered to pass. Solid, focused work. Approve.
Author
Owner

🛡️ Nora Steiner ("NullX") — Application Security Engineer

Verdict: Approved

A feature removal is mostly attack-surface reduction, so my job here is to confirm nothing was opened up and that the removed endpoint didn't leave a security hole behind. I checked the removed endpoint, the surviving search path, and the retargeted link.

Attack-surface assessment

  • Endpoint removal reduces surface — cleanly. GET /api/documents/conversation is gone from the controller, the OpenAPI path (api.ts), and the C4 docs. It sat behind the same anyRequest().authenticated() rule as the rest of /api/documents, and the destination it's replaced by (/documents search) sits behind the identical rule, so no authorization boundary changed. No IDOR delta: both old and new paths take person UUIDs as filter params, both require an authenticated session.
  • No injection introduced. The removed repository queries were parameterized JPQL (:person1, :from, etc.) — and the surviving search still composes parameterized hasSender/hasReceiver specifications. The retargeted link builds /documents?senderId={personId}&receiverId={c.id} from server-provided UUIDs interpolated into an href; these are trusted IDs from the person's own correspondent list, not user free-text, and they flow back through the typed senderId/receiverId query params that the search load function already validates. No string-concatenated query anywhere.
  • auth.spec.ts change is safe. Dropping /briefwechsel from the protected-route loop is correct — the route no longer exists. /documents/new and /persons remain as authenticated-route guards, and /documents (the new destination) is covered by the same authenticated() rule, so protected-route coverage is not meaningfully reduced.
  • No data exposure in the new copy. The title/hint strings are i18n keys mapped through Paraglide — no raw backend errors, no implementation details leaked. The icon SVG is aria-hidden and inert.

Notes (not findings)

  • The new briefwechsel-removed.spec.ts asserts a genuine 404 in the authenticated project — good: it confirms the old URL doesn't silently redirect (which could be an open-redirect smell). A 404 on the styled error page is the secure outcome.

Nothing to fix. No new CWE introduced, one endpoint of attack surface retired. Approve.

## 🛡️ Nora Steiner ("NullX") — Application Security Engineer **Verdict: ✅ Approved** A feature removal is mostly attack-surface *reduction*, so my job here is to confirm nothing was opened up and that the removed endpoint didn't leave a security hole behind. I checked the removed endpoint, the surviving search path, and the retargeted link. ### Attack-surface assessment - **Endpoint removal reduces surface — cleanly.** `GET /api/documents/conversation` is gone from the controller, the OpenAPI path (`api.ts`), and the C4 docs. It sat behind the same `anyRequest().authenticated()` rule as the rest of `/api/documents`, and the destination it's replaced by (`/documents` search) sits behind the identical rule, so **no authorization boundary changed**. No IDOR delta: both old and new paths take person UUIDs as filter params, both require an authenticated session. - **No injection introduced.** The removed repository queries were parameterized JPQL (`:person1`, `:from`, etc.) — and the surviving search still composes parameterized `hasSender`/`hasReceiver` specifications. The retargeted link builds `/documents?senderId={personId}&receiverId={c.id}` from server-provided UUIDs interpolated into an `href`; these are trusted IDs from the person's own correspondent list, not user free-text, and they flow back through the typed `senderId`/`receiverId` query params that the search load function already validates. No string-concatenated query anywhere. - **`auth.spec.ts` change is safe.** Dropping `/briefwechsel` from the protected-route loop is correct — the route no longer exists. `/documents/new` and `/persons` remain as authenticated-route guards, and `/documents` (the new destination) is covered by the same `authenticated()` rule, so protected-route coverage is not meaningfully reduced. - **No data exposure in the new copy.** The `title`/hint strings are i18n keys mapped through Paraglide — no raw backend errors, no implementation details leaked. The icon SVG is `aria-hidden` and inert. ### Notes (not findings) - The new `briefwechsel-removed.spec.ts` asserts a genuine 404 in the authenticated project — good: it confirms the old URL doesn't silently redirect (which could be an open-redirect smell). A 404 on the styled error page is the secure outcome. Nothing to fix. No new CWE introduced, one endpoint of attack surface retired. Approve.
Author
Owner

🧪 Sara Holt — Senior QA Engineer

Verdict: ⚠️ Approved with concerns

I care about one thing above all on a removal: the acceptance criteria each have a test that actually executes, and the "no dangling references" guard is real and not skipped. The issue itself flagged a trap here (a 404 guard hidden inside a test.skip() block), so I looked closely.

What's solid

  • The 404 guard is in an ACTIVE spec. The old assertion that lived inside the test.skip() describe in stammbaum.spec.ts was deleted, and briefwechsel-removed.spec.ts was added as a standalone spec. I confirmed the Playwright config has a single chromium project that runs with stored auth (storageState) — so this spec genuinely executes authenticated and the route 404s on the styled error page rather than bouncing to /login. The trap the issue warned about was avoided. 👍
  • stammbaum.spec.ts and auth.spec.ts both cleaned as the AC required — the contradicting "still renders without 404" assertion is gone, and /briefwechsel is removed from the protected-route loop.
  • Unit coverage for the surviving card maps to ACs: href retarget, search-hint text, both-names accessible label, and personName added to every render() (otherwise the title formatter throws on undefined — they got that right). Assertions use the paraglide message, not retyped copy.
  • Backend test deletions are clean — both getConversationFiltered blocks and the findSinglePersonCorrespondence repo section removed; zero residual references; PR reports DocumentServiceTest (169) and DocumentRepositoryTest (29) green.

Concerns

  1. The briefwechsel-removed.spec.ts 404 assertion is slightly brittle. expect(page.getByText('404')).toBeVisible()getByText('404') is a substring match and 404 could appear elsewhere, and it relies on the error page rendering that literal. The HTTP-status assertion expect(response?.status()).toBe(404) is the load-bearing one and is solid; the text assertion is belt-and-suspenders but a touch fragile. Consider getByRole('heading', { name: '404' }) or scoping it. Minor — not a merge blocker.
  2. No regression test asserting the badge count stays bilateral (AC: "N is understood as shared letters, not search results"). The clarifying title was added but there's no test asserting person_correspondents_badge_title is present on the badge. The card test would be the natural home for a one-line toHaveAttribute('title', ...) check. Nice-to-have.
  3. Deleted visual baselines / the new icon — the PR notes no briefwechsel-*.png baselines existed (no-op), and the icon swap (chat → magnifier) has no visual-regression snapshot. Acceptable given the project runs browser/visual tests on CI, but worth a CI confirmation that the icon change didn't shift any existing person-detail snapshot.

The headline AC (404 guard executes for real) is correctly handled, which was the risky part. Tidy up the brittle text assertion and ideally add the badge-title test. Approve with those concerns.

## 🧪 Sara Holt — Senior QA Engineer **Verdict: ⚠️ Approved with concerns** I care about one thing above all on a removal: **the acceptance criteria each have a test that actually executes**, and the "no dangling references" guard is real and not skipped. The issue itself flagged a trap here (a 404 guard hidden inside a `test.skip()` block), so I looked closely. ### What's solid - **The 404 guard is in an ACTIVE spec.** The old assertion that lived inside the `test.skip()` describe in `stammbaum.spec.ts` was deleted, and `briefwechsel-removed.spec.ts` was added as a standalone spec. I confirmed the Playwright config has a single `chromium` project that runs with stored auth (`storageState`) — so this spec genuinely executes authenticated and the route 404s on the styled error page rather than bouncing to `/login`. The trap the issue warned about was avoided. 👍 - **`stammbaum.spec.ts` and `auth.spec.ts` both cleaned** as the AC required — the contradicting "still renders without 404" assertion is gone, and `/briefwechsel` is removed from the protected-route loop. - **Unit coverage for the surviving card maps to ACs:** href retarget, search-hint text, both-names accessible label, and `personName` added to every `render()` (otherwise the title formatter throws on undefined — they got that right). Assertions use the paraglide message, not retyped copy. - **Backend test deletions are clean** — both `getConversationFiltered` blocks and the `findSinglePersonCorrespondence` repo section removed; zero residual references; PR reports `DocumentServiceTest` (169) and `DocumentRepositoryTest` (29) green. ### Concerns 1. **The `briefwechsel-removed.spec.ts` 404 assertion is slightly brittle.** `expect(page.getByText('404')).toBeVisible()` — `getByText('404')` is a substring match and `404` could appear elsewhere, and it relies on the error page rendering that literal. The HTTP-status assertion `expect(response?.status()).toBe(404)` is the load-bearing one and is solid; the text assertion is belt-and-suspenders but a touch fragile. Consider `getByRole('heading', { name: '404' })` or scoping it. Minor — not a merge blocker. 2. **No regression test asserting the badge count stays bilateral** (AC: "N is understood as shared letters, not search results"). The clarifying `title` was added but there's no test asserting `person_correspondents_badge_title` is present on the badge. The card test would be the natural home for a one-line `toHaveAttribute('title', ...)` check. Nice-to-have. 3. **Deleted visual baselines / the new icon** — the PR notes no `briefwechsel-*.png` baselines existed (no-op), and the icon swap (chat → magnifier) has no visual-regression snapshot. Acceptable given the project runs browser/visual tests on CI, but worth a CI confirmation that the icon change didn't shift any existing person-detail snapshot. The headline AC (404 guard executes for real) is correctly handled, which was the risky part. Tidy up the brittle text assertion and ideally add the badge-title test. Approve with those concerns.
Author
Owner

🔧 Tobias Lange — DevOps Engineer

Verdict: Approved (LGTM)

I checked this for infrastructure impact: Compose services, CI workflow, reverse-proxy/Caddy config, secrets, migrations, and generated-artifact handling.

What I checked

  • No infrastructure files touched in this PR. No docker-compose*.yml, no Caddyfile, no CI workflow, no env vars. (The infra/caddy/Caddyfile, vite.config.ts, and renovate.json changes you may see in a main...HEAD diff belong to other merged commits — they are not in this PR's diff against its base sha. Confirmed.)
  • No Flyway migration added or modified. The only migration mention is a comment in the immutable V62__index_fk_columns.sql (it names /briefwechsel as one of several flows the FK indexes serve). Correct call to leave it: Flyway migrations are immutable once shipped, and those idx_documents_sender_id indexes still serve document search. No schema change, no migration risk, no restore-test impact.
  • Generated artifact (api.ts) was hand-edited, not regenerated. This is the one thing I'd normally grumble about — the OpenAPI client should come out of npm run generate:api against a live backend, not a manual trim. The PR is transparent about it and the diff is exactly the one removed path + operation, so it'll reconcile on the next CI regen. Flagging as a note, not a blocker: ensure CI runs generate:api and the diff comes back empty.
  • No new ports, services, secrets, or image tags. Nothing for me to size or operate. Monthly bill unchanged. 😄

Pure code/test/docs removal with zero ops surface. Nothing to fix from my side. Approve.

## 🔧 Tobias Lange — DevOps Engineer **Verdict: ✅ Approved (LGTM)** I checked this for infrastructure impact: Compose services, CI workflow, reverse-proxy/Caddy config, secrets, migrations, and generated-artifact handling. ### What I checked - **No infrastructure files touched in this PR.** No `docker-compose*.yml`, no Caddyfile, no CI workflow, no env vars. (The `infra/caddy/Caddyfile`, `vite.config.ts`, and `renovate.json` changes you may see in a `main...HEAD` diff belong to *other* merged commits — they are not in this PR's diff against its base sha. Confirmed.) - **No Flyway migration added or modified.** The only migration *mention* is a comment in the immutable `V62__index_fk_columns.sql` (it names `/briefwechsel` as one of several flows the FK indexes serve). Correct call to leave it: Flyway migrations are immutable once shipped, and those `idx_documents_sender_id` indexes still serve document search. No schema change, no migration risk, no restore-test impact. - **Generated artifact (`api.ts`) was hand-edited, not regenerated.** This is the one thing I'd normally grumble about — the OpenAPI client should come out of `npm run generate:api` against a live backend, not a manual trim. The PR is transparent about it and the diff is exactly the one removed path + operation, so it'll reconcile on the next CI regen. Flagging as a note, not a blocker: ensure CI runs `generate:api` and the diff comes back empty. - **No new ports, services, secrets, or image tags.** Nothing for me to size or operate. Monthly bill unchanged. 😄 Pure code/test/docs removal with zero ops surface. Nothing to fix from my side. Approve.
Author
Owner

📋 "Elicit" — Requirements Engineer & Business Analyst

Verdict: ⚠️ Approved with concerns

I assessed this against the issue #716 acceptance criteria (the stated single source of truth) for traceability and completeness, and probed the one deliberate behaviour change for whether it was properly surfaced rather than silently swallowed.

AC traceability — every Gherkin scenario maps to an implementation

Acceptance criterion Covered?
/briefwechsel returns 404 (no redirect) on styled error page briefwechsel-removed.spec.ts, active project
Card deep-links to /documents?senderId=A&receiverId=B CoCorrespondentsList.svelte href + test
Card no longer promises a conversation (copy + icon) search-action copy in 3 locales + magnifier icon
Bilateral badge not presented as result count ⚠️ title added, but see concern 1
Card still renders / sort labels still work heading kept, conv_sort_* retained
Backend /conversation endpoint gone (404) controller method removed
No dangling references remain ⚠️ see concern 2

The deliberate bilateral → unidirectional regression was correctly handled the way the issue demanded: accepted, not "fixed," and recorded in ADR-030 with Context/Decision/Consequence. The "future bidirectional filter" remains explicitly out of scope. Good requirements hygiene — the regression is documented, not hidden.

Concerns

  1. The "accessible name names both persons" AC is only partially met as written. The Gherkin says "the link's accessible name names both persons." The accessible name of the link (its text content) is only person B's name ({c.name}); both names live in the title attribute, which is a tooltip, not the computed accessible name. The test asserts against title, which matches the issue's own structural hint ("assert against title"), so this is internally consistent with the spec — but a strict reading of "accessible name" vs "title" is a latent ambiguity. Acceptable as specified; flagging it so it isn't mistaken for full accessible-name coverage.
  2. One dangling reference survives the "no dangling references" ACfrontend/e2e/CLAUDE.md:28 still lists the now-deleted korrespondenz.spec.ts. The AC's grep set is briefwechsel/getConversation/findConversation/etc., not korrespondenz, so this technically slips through the letter of the AC — but the intent ("only intentional matches remain") is violated by a stale doc pointer to a deleted file. Low-effort fix; worth closing for completeness.

No scope creep, no contradictions, no un-surfaced TBDs. The two items above are completeness gaps against a generally well-traced PR. Approve once concern 2 is tidied.

## 📋 "Elicit" — Requirements Engineer & Business Analyst **Verdict: ⚠️ Approved with concerns** I assessed this against the issue #716 acceptance criteria (the stated single source of truth) for traceability and completeness, and probed the one deliberate behaviour change for whether it was properly surfaced rather than silently swallowed. ### AC traceability — every Gherkin scenario maps to an implementation | Acceptance criterion | Covered? | |---|---| | `/briefwechsel` returns 404 (no redirect) on styled error page | ✅ `briefwechsel-removed.spec.ts`, active project | | Card deep-links to `/documents?senderId=A&receiverId=B` | ✅ `CoCorrespondentsList.svelte` href + test | | Card no longer promises a conversation (copy + icon) | ✅ search-action copy in 3 locales + magnifier icon | | Bilateral badge not presented as result count | ⚠️ `title` added, but see concern 1 | | Card still renders / sort labels still work | ✅ heading kept, `conv_sort_*` retained | | Backend `/conversation` endpoint gone (404) | ✅ controller method removed | | No dangling references remain | ⚠️ see concern 2 | The deliberate **bilateral → unidirectional regression** was correctly handled the way the issue demanded: accepted, not "fixed," and recorded in ADR-030 with Context/Decision/Consequence. The "future bidirectional filter" remains explicitly out of scope. Good requirements hygiene — the regression is documented, not hidden. ### Concerns 1. **The "accessible name names both persons" AC is only partially met as written.** The Gherkin says "the link's accessible name names both persons." The *accessible name* of the link (its text content) is only person B's name (`{c.name}`); both names live in the `title` attribute, which is a tooltip, not the computed accessible name. The test asserts against `title`, which matches the issue's own structural hint ("assert against `title`"), so this is internally consistent with the spec — but a strict reading of "accessible name" vs "title" is a latent ambiguity. Acceptable as specified; flagging it so it isn't mistaken for full accessible-name coverage. 2. **One dangling reference survives the "no dangling references" AC** — `frontend/e2e/CLAUDE.md:28` still lists the now-deleted `korrespondenz.spec.ts`. The AC's grep set is `briefwechsel`/`getConversation`/`findConversation`/etc., not `korrespondenz`, so this technically slips through the *letter* of the AC — but the *intent* ("only intentional matches remain") is violated by a stale doc pointer to a deleted file. Low-effort fix; worth closing for completeness. No scope creep, no contradictions, no un-surfaced TBDs. The two items above are completeness gaps against a generally well-traced PR. Approve once concern 2 is tidied.
Author
Owner

🎨 Leonie Voss — UI/UX Design Lead & Accessibility Strategist

Verdict: ⚠️ Approved with concerns

The whole UI surface of this PR is one card — the "Häufige Korrespondenten" chip — so I went deep on it: the icon swap, the copy, the badge tooltip, the accessible name, and brand/contrast compliance.

What's right

  • The icon swap is exactly the correct fix. The old chat/speech-bubble SVG promised a two-way conversation; the new magnifier (d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z") honestly signals a search action. It keeps aria-hidden="true", so it stays inert to screen readers — correct, because the action's meaning must come from the link's text/label, not a decorative glyph. 👍
  • The chip remains a real <a> with visible text ({c.name}) and the existing hover:border-primary/hover:bg-surface states. The focus-visible ring is unchanged. Keyboard users keep a discernible target.
  • Copy is honest and localized across all three locales ("Briefe von {A} an {B} durchsuchen" / "Search letters from {A} to {B}" / "Buscar cartas de {A} a {B}"), driven by Paraglide keys — no raw strings, no leaked detail.
  • Brand tokens are respectedborder-line, bg-muted, text-ink, text-ink-3. No hardcoded hex, no off-palette colors introduced.

Concerns

  1. Tiny type — text-[10px] on the hint and the badge. Both the search hint (person_correspondents_search_hint) and the ×N badge render at text-[10px]. My hard floor is 12px for any visible text, and this is a senior-facing (60+) audience. To be fair, this is pre-existing — the old hint was also 10px and this PR didn't introduce it — so I'm not blocking this PR on it. But since the file is being touched and the copy is being rewritten anyway, it's the natural moment to bump the hint to at least text-xs (12px). Flagging as a concern, not a blocker, because it's inherited.
  2. The badge tooltip is title-only, which seniors and touch users won't discover. The bilateral-count clarification ("Gemeinsame Briefe in beide Richtungen") lives entirely in a title attribute on a <span>. title tooltips don't appear on touch, are often missed by older users, and aren't reliably announced by screen readers on a non-interactive <span>. The spec explicitly accepted "a small caption/title … is enough," so this satisfies the letter of the requirement — but from a pure UX standpoint the disambiguation is effectively invisible to a chunk of the audience. If you want the clarification to actually land, a <span class="sr-only"> companion or a visible micro-caption would do it. Non-blocking given the spec's allowance.
  3. No visual-regression snapshot for the icon change. The chip's silhouette changed (chat → magnifier). There's no toHaveScreenshot covering the person-detail card at 320/768/1440. Not introduced by this PR's scope, but worth a CI glance that the swap didn't shift any existing person-detail baseline.

Touch target on the chip looks fine (it's a padded px-3 py-1.5 pill with text, comfortably tappable). No contrast regressions — the colors are unchanged. The two type/tooltip items are inherited or spec-sanctioned, so: approve with concerns, and please consider the text-xs bump while you're in the file.

## 🎨 Leonie Voss — UI/UX Design Lead & Accessibility Strategist **Verdict: ⚠️ Approved with concerns** The whole UI surface of this PR is one card — the "Häufige Korrespondenten" chip — so I went deep on it: the icon swap, the copy, the badge tooltip, the accessible name, and brand/contrast compliance. ### What's right - **The icon swap is exactly the correct fix.** The old chat/speech-bubble SVG promised a two-way conversation; the new magnifier (`d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"`) honestly signals a search action. It keeps `aria-hidden="true"`, so it stays inert to screen readers — correct, because the action's meaning must come from the link's text/label, not a decorative glyph. 👍 - **The chip remains a real `<a>`** with visible text (`{c.name}`) and the existing `hover:border-primary`/`hover:bg-surface` states. The focus-visible ring is unchanged. Keyboard users keep a discernible target. - **Copy is honest and localized** across all three locales ("Briefe von {A} an {B} durchsuchen" / "Search letters from {A} to {B}" / "Buscar cartas de {A} a {B}"), driven by Paraglide keys — no raw strings, no leaked detail. - **Brand tokens are respected** — `border-line`, `bg-muted`, `text-ink`, `text-ink-3`. No hardcoded hex, no off-palette colors introduced. ### Concerns 1. **Tiny type — `text-[10px]` on the hint and the badge.** Both the search hint (`person_correspondents_search_hint`) and the `×N` badge render at `text-[10px]`. My hard floor is **12px for any visible text**, and this is a senior-facing (60+) audience. To be fair, this is **pre-existing** — the old hint was also 10px and this PR didn't introduce it — so I'm not blocking *this* PR on it. But since the file is being touched and the copy is being rewritten anyway, it's the natural moment to bump the hint to at least `text-xs` (12px). Flagging as a concern, not a blocker, because it's inherited. 2. **The badge tooltip is `title`-only, which seniors and touch users won't discover.** The bilateral-count clarification ("Gemeinsame Briefe in beide Richtungen") lives entirely in a `title` attribute on a `<span>`. `title` tooltips don't appear on touch, are often missed by older users, and aren't reliably announced by screen readers on a non-interactive `<span>`. The spec explicitly accepted "a small caption/`title` … is enough," so this satisfies the letter of the requirement — but from a pure UX standpoint the disambiguation is effectively invisible to a chunk of the audience. If you want the clarification to actually land, a `<span class="sr-only">` companion or a visible micro-caption would do it. Non-blocking given the spec's allowance. 3. **No visual-regression snapshot for the icon change.** The chip's silhouette changed (chat → magnifier). There's no `toHaveScreenshot` covering the person-detail card at 320/768/1440. Not introduced by this PR's scope, but worth a CI glance that the swap didn't shift any existing person-detail baseline. Touch target on the chip looks fine (it's a padded `px-3 py-1.5` pill with text, comfortably tappable). No contrast regressions — the colors are unchanged. The two type/tooltip items are inherited or spec-sanctioned, so: approve with concerns, and please consider the `text-xs` bump while you're in the file.
marcel added 1 commit 2026-06-03 07:53:31 +02:00
docs(e2e): fix stale spec listing after Briefwechsel removal
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m52s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m36s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
ed7b19fac0
The e2e README still listed the deleted korrespondenz.spec.ts. Replace it
with the new briefwechsel-removed.spec.ts guard entry — closing the last
dangling reference flagged in review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
marcel added 1 commit 2026-06-03 08:18:10 +02:00
test(persons): assert the card title by exact message, not regex
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m18s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 3m36s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m4s
aa13e40bd6
toHaveAttribute compares by equality, so passing a regex asserted against
the literal RegExp object and failed. Assert the full title against
m.person_correspondents_search_title(...) instead — it names both persons
and avoids retyping the copy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
marcel merged commit caea0d5633 into main 2026-06-03 10:26:56 +02:00
marcel deleted branch feat/issue-716-remove-briefwechsel 2026-06-03 10:26:56 +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#721