feat(persons): visually distinguish incomplete placeholder persons + filter by completeness on /persons #323

Open
opened 2026-04-24 13:24:54 +02:00 by marcel · 8 comments
Owner

Context

/persons renders 221 persons. Many are placeholders inherited from the ODS import — examples pulled from the live list on 2026-04-24:

  • ? ? (13 docs)
  • B.Zeer ? (3 docs)
  • Carl ? (3 docs)
  • Conrad ? (2 docs)
  • DeNaeyer ? (1 doc)
  • Elisabeth geb Fernow (1 doc) — only first name known
  • Ellen de Gruyter ? (1 doc) — partial alias only

Visually these look identical to fully resolved persons. Admins can't triage at a glance which need enrichment. Volunteers who could help fill gaps can't narrow the list.

This also undermines the crowd workflow at a different layer from the transcribe-onboarding work in #320 and #327: before transcribers draw regions, someone needs to link each letter to two real people. Today that's a needle-in-a-haystack against 221 rows.

Related but not duplicate:

  • #51 — splits combined entries (structural, different problem)
  • #214 — multi-person Von-column parsing (import-time)
  • #216 — multiple senders per document (schema)
  • #218 — adds title and personType fields
  • #223 — sorting options

Non-goals

  • No auto-merge or auto-deletion — admins still decide.
  • No crowd-contribution UI for enrichment (separate follow-up if desired).
  • No import-time changes — this is read-side only.

Proposed model

"Incompleteness" definition (tune against real data)

Sample 30 placeholders from the live list; confirm the rule set catches them.

Compute completenessStatus: INCOMPLETE | PARTIAL | COMPLETE on the fly (not stored):

  • INCOMPLETE — any of:
    • lastName IS NULL OR lastName = ''
    • lastName = '?' OR firstName = '?'
    • firstName AND lastName both missing, aliases size ≤ 1
  • PARTIAL — none of the INCOMPLETE conditions AND:
    • birthYear IS NULL AND deathYear IS NULL
    • OR aliases IS EMPTY
  • COMPLETE — otherwise.

Expose on the API

  • PersonSummaryDTO gains completenessStatus: CompletenessStatus (new enum).
  • /api/persons/search accepts optional status=incomplete|partial|complete|all.

Show in the UI

  • PersonCard renders a muted variant + small badge (Unvollständig / Teilweise) when not COMPLETE.
  • /persons gets a filter chip group under the search bar:
    Alle (221) · Unvollständig (N) · Teilweise (M) · Vollständig (L). Selection sets ?status=... in URL.
  • Person detail page shows the same badge beside the name when not COMPLETE.

Implementation plan

Backend

  • New enum CompletenessStatus { INCOMPLETE, PARTIAL, COMPLETE } in model/.
  • PersonService.computeStatus(Person) — pure function, unit-testable.
  • PersonSummaryDTO gets completenessStatus (@Schema(requiredMode = REQUIRED)).
  • PersonController.search accepts status query param; filter applied after core search.
  • Use the existing PersonSummaryDTO flow — keep the same paging/sort semantics.

Frontend

  • frontend/src/lib/components/PersonCard.svelte — read completenessStatus, add badge + muted class variant.
  • frontend/src/routes/persons/+page.svelte — filter chips, URL sync.
  • frontend/src/routes/persons/+page.server.ts — pass status to the API.
  • frontend/src/routes/persons/[id]/+page.svelte — badge beside name.
  • Regenerate API types.

i18n

3–4 new keys: persons_filter_all, persons_filter_incomplete, persons_filter_partial, persons_filter_complete, person_badge_incomplete, person_badge_partial.

Tests

  • Backend unit (PersonServiceTest): computeStatus covers each rule branch — include real placeholder samples from current data.
  • Backend controller (PersonControllerTest): /api/persons/search?status=incomplete returns only incomplete persons; status=all returns everything.
  • Frontend component: PersonCard renders each status variant.
  • E2E: visit /persons?status=incomplete, assert only placeholder rows are visible; check the header count matches.

Verification

  1. Count incomplete persons via query: curl -s .../api/persons/search?status=incomplete | jq '.total'. Eyeball against /persons list — should be roughly 40–80 based on the sample.
  2. Open a known complete person (e.g. Walter de Gruyter) — no badge, normal color.
  3. Open ? ?Unvollständig badge.

Acceptance criteria

  • CompletenessStatus enum defined + exposed on PersonSummaryDTO
  • PersonService.computeStatus covers the rules above with tests
  • List + detail render the badge for INCOMPLETE and PARTIAL
  • Filter chips in /persons (Alle / Unvollständig / Teilweise / Vollständig), URL-shareable
  • Counts in chips reflect the current search (not fixed totals)
  • i18n complete for de/en/es
  • No regression on /persons sort, search, pagination

Critical files

backend/src/main/java/org/raddatz/familienarchiv/model/CompletenessStatus.java     (new)
backend/src/main/java/org/raddatz/familienarchiv/dto/PersonSummaryDTO.java         (+ field)
backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java        (computeStatus + filter)
backend/src/main/java/org/raddatz/familienarchiv/controller/PersonController.java  (status param)
backend/src/test/java/org/raddatz/familienarchiv/service/PersonServiceTest.java
backend/src/test/java/org/raddatz/familienarchiv/controller/PersonControllerTest.java
frontend/src/lib/components/PersonCard.svelte
frontend/src/routes/persons/+page.svelte
frontend/src/routes/persons/+page.server.ts
frontend/src/routes/persons/[id]/+page.svelte
frontend/src/lib/generated/api.ts                                                  (regen)
frontend/messages/{de,en,es}.json
## Context `/persons` renders 221 persons. Many are placeholders inherited from the ODS import — examples pulled from the live list on 2026-04-24: - `? ?` (13 docs) - `B.Zeer ?` (3 docs) - `Carl ?` (3 docs) - `Conrad ?` (2 docs) - `DeNaeyer ?` (1 doc) - `Elisabeth geb Fernow` (1 doc) — only first name known - `Ellen de Gruyter ?` (1 doc) — partial alias only Visually these look identical to fully resolved persons. Admins can't triage at a glance which need enrichment. Volunteers who could help fill gaps can't narrow the list. This also undermines the crowd workflow at a different layer from the transcribe-onboarding work in #320 and #327: before transcribers draw regions, someone needs to link each letter to two real people. Today that's a needle-in-a-haystack against 221 rows. Related but not duplicate: - #51 — splits combined entries (structural, different problem) - #214 — multi-person Von-column parsing (import-time) - #216 — multiple senders per document (schema) - #218 — adds `title` and `personType` fields - #223 — sorting options ## Non-goals - No auto-merge or auto-deletion — admins still decide. - No crowd-contribution UI for enrichment (separate follow-up if desired). - No import-time changes — this is read-side only. ## Proposed model ### "Incompleteness" definition (tune against real data) Sample 30 placeholders from the live list; confirm the rule set catches them. Compute `completenessStatus: INCOMPLETE | PARTIAL | COMPLETE` on the fly (not stored): - **INCOMPLETE** — any of: - `lastName IS NULL OR lastName = ''` - `lastName = '?'` OR `firstName = '?'` - `firstName` AND `lastName` both missing, `aliases` size ≤ 1 - **PARTIAL** — none of the INCOMPLETE conditions AND: - `birthYear IS NULL AND deathYear IS NULL` - OR `aliases IS EMPTY` - **COMPLETE** — otherwise. ### Expose on the API - `PersonSummaryDTO` gains `completenessStatus: CompletenessStatus` (new enum). - `/api/persons/search` accepts optional `status=incomplete|partial|complete|all`. ### Show in the UI - `PersonCard` renders a muted variant + small badge (`Unvollständig` / `Teilweise`) when not `COMPLETE`. - `/persons` gets a filter chip group under the search bar: `Alle (221) · Unvollständig (N) · Teilweise (M) · Vollständig (L)`. Selection sets `?status=...` in URL. - Person detail page shows the same badge beside the name when not `COMPLETE`. ## Implementation plan ### Backend - New enum `CompletenessStatus { INCOMPLETE, PARTIAL, COMPLETE }` in `model/`. - `PersonService.computeStatus(Person)` — pure function, unit-testable. - `PersonSummaryDTO` gets `completenessStatus` (`@Schema(requiredMode = REQUIRED)`). - `PersonController.search` accepts `status` query param; filter applied after core search. - Use the existing `PersonSummaryDTO` flow — keep the same paging/sort semantics. ### Frontend - `frontend/src/lib/components/PersonCard.svelte` — read `completenessStatus`, add badge + muted class variant. - `frontend/src/routes/persons/+page.svelte` — filter chips, URL sync. - `frontend/src/routes/persons/+page.server.ts` — pass `status` to the API. - `frontend/src/routes/persons/[id]/+page.svelte` — badge beside name. - Regenerate API types. ### i18n 3–4 new keys: `persons_filter_all`, `persons_filter_incomplete`, `persons_filter_partial`, `persons_filter_complete`, `person_badge_incomplete`, `person_badge_partial`. ## Tests - **Backend unit (PersonServiceTest):** `computeStatus` covers each rule branch — include real placeholder samples from current data. - **Backend controller (PersonControllerTest):** `/api/persons/search?status=incomplete` returns only incomplete persons; `status=all` returns everything. - **Frontend component:** `PersonCard` renders each status variant. - **E2E:** visit `/persons?status=incomplete`, assert only placeholder rows are visible; check the header count matches. ## Verification 1. Count incomplete persons via query: `curl -s .../api/persons/search?status=incomplete | jq '.total'`. Eyeball against `/persons` list — should be roughly 40–80 based on the sample. 2. Open a known complete person (e.g. Walter de Gruyter) — no badge, normal color. 3. Open `? ?` — `Unvollständig` badge. ## Acceptance criteria - [ ] `CompletenessStatus` enum defined + exposed on `PersonSummaryDTO` - [ ] `PersonService.computeStatus` covers the rules above with tests - [ ] List + detail render the badge for INCOMPLETE and PARTIAL - [ ] Filter chips in `/persons` (Alle / Unvollständig / Teilweise / Vollständig), URL-shareable - [ ] Counts in chips reflect the current search (not fixed totals) - [ ] i18n complete for de/en/es - [ ] No regression on `/persons` sort, search, pagination ## Critical files ``` backend/src/main/java/org/raddatz/familienarchiv/model/CompletenessStatus.java (new) backend/src/main/java/org/raddatz/familienarchiv/dto/PersonSummaryDTO.java (+ field) backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java (computeStatus + filter) backend/src/main/java/org/raddatz/familienarchiv/controller/PersonController.java (status param) backend/src/test/java/org/raddatz/familienarchiv/service/PersonServiceTest.java backend/src/test/java/org/raddatz/familienarchiv/controller/PersonControllerTest.java frontend/src/lib/components/PersonCard.svelte frontend/src/routes/persons/+page.svelte frontend/src/routes/persons/+page.server.ts frontend/src/routes/persons/[id]/+page.svelte frontend/src/lib/generated/api.ts (regen) frontend/messages/{de,en,es}.json ``` ## Related - Persons Redesign v2 milestone (#51, #214, #216, #218, #223, #225, #306).
marcel added this to the Persons Redesign v2 milestone 2026-04-24 13:24:54 +02:00
marcel added the P2-mediumfeaturepersonui labels 2026-04-24 13:28:11 +02:00
Author
Owner

👨‍💻 Markus Keller — Application Architect

Observations

  • PersonSummaryDTO is an interface projection, not a class. The issue says to add completenessStatus as a field on it, but interface projections cannot carry computed values directly — they map database columns 1:1. Adding completenessStatus requires either (a) a default method that computes status from the other getters, or (b) converting to a class-based DTO. Option (a) is usable but mixes data and computation in the DTO layer, which is a smell. Option (b) breaks the native query projection pattern used by both findAllWithDocumentCount and searchWithDocumentCount.
  • Filtering in application layer vs. database layer. The issue says "filter applied after core search." With 221 persons this is fine today. But since both list queries are native SQL, the cleanest long-term approach adds a WHERE clause to those queries. Post-query filtering in Java works but bypasses pagination if pagination is ever added.
  • PersonSummaryDTO lacks nameAliases, which the completeness rule needs (aliases IS EMPTY). The interface projection does not include the alias collection — only the legacy alias varchar column (from before the person_name_aliases table existed). The issue's INCOMPLETE rule references aliases size ≤ 1, but in the current data model, aliases live in person_name_aliases (a separate table), not in Person.alias. This is a data model gap that affects the rule definition.
  • CompletenessStatus placement. The issue places the enum in model/ — but no such package exists. The person/ package owns all person domain types. The enum should live in org.raddatz.familienarchiv.person.
  • No Flyway migration needed. completenessStatus is computed on the fly — no schema change, no migration. This is correctly called out in the issue ("not stored").
  • The /api/persons/search route mentioned in the issue does not exist. The current endpoint is GET /api/persons with optional q query param. Adding status to that endpoint is the right move — consistent with the existing pattern.
  • PersonController.getPersons currently calls personService.findAll(q). Adding the status filter means findAll needs a second parameter or a new method. Given the existing clean split between findAllWithDocumentCount and searchWithDocumentCount, the cleanest approach is to keep filtering in the service layer (not the controller) and pass status through.

Recommendations

  1. Use a default method on PersonSummaryDTO to compute completenessStatus from the existing getters — getFirstName(), getLastName(), getBirthYear(), getDeathYear(), and the count of aliases from getNameAliasCount() (add one more native query projection column). This avoids DTO conversion while keeping the computation close to the data.
  2. Add nameAliasCount (integer) to the native SQL projections so the DTO can compute completeness without a second DB query. A subselect (SELECT COUNT(*) FROM person_name_aliases WHERE person_id = p.id) AS nameAliasCount alongside the existing documentCount subselect is the right pattern.
  3. Apply the status filter in PersonService.findAll — accept (String q, String status) — and filter the returned list. Keep it in Java; a SQL WHERE on a computed value would require a CTE or subquery, adding query complexity for a list that will stay small.
  4. Place CompletenessStatus in org.raddatz.familienarchiv.person, not a non-existent model/ package.
  5. Update CLAUDE.md package table and docs/architecture/c4/l3-backend-*.puml for the new CompletenessStatus enum and any controller signature change. The doc obligation table is non-negotiable for PR merge.

Open Decisions

  • Rule for "aliases": the issue uses aliases to mean the person_name_aliases table, but the legacy Person.alias varchar column also exists. Clarify whether "has at least one alias" means nameAliases.size() >= 1 (the formal alias table) or alias IS NOT NULL (the legacy column), or both. These are different data concepts in the current schema.
  • Pagination future-proofing: if /persons ever gets server-side pagination, post-Java-filtering breaks paging counts. For now it's fine, but a brief design note acknowledging this constraint is worth adding to the implementation.
## 👨‍💻 Markus Keller — Application Architect ### Observations - **`PersonSummaryDTO` is an interface projection**, not a class. The issue says to add `completenessStatus` as a field on it, but interface projections cannot carry computed values directly — they map database columns 1:1. Adding `completenessStatus` requires either (a) a default method that computes status from the other getters, or (b) converting to a class-based DTO. Option (a) is usable but mixes data and computation in the DTO layer, which is a smell. Option (b) breaks the native query projection pattern used by both `findAllWithDocumentCount` and `searchWithDocumentCount`. - **Filtering in application layer vs. database layer.** The issue says "filter applied after core search." With 221 persons this is fine today. But since both list queries are native SQL, the cleanest long-term approach adds a `WHERE` clause to those queries. Post-query filtering in Java works but bypasses pagination if pagination is ever added. - **`PersonSummaryDTO` lacks `nameAliases`**, which the completeness rule needs (`aliases IS EMPTY`). The interface projection does not include the alias collection — only the legacy `alias` varchar column (from before the `person_name_aliases` table existed). The issue's INCOMPLETE rule references `aliases size ≤ 1`, but in the current data model, aliases live in `person_name_aliases` (a separate table), not in `Person.alias`. This is a data model gap that affects the rule definition. - **`CompletenessStatus` placement.** The issue places the enum in `model/` — but no such package exists. The `person/` package owns all person domain types. The enum should live in `org.raddatz.familienarchiv.person`. - **No Flyway migration needed.** `completenessStatus` is computed on the fly — no schema change, no migration. This is correctly called out in the issue ("not stored"). - **The `/api/persons/search` route** mentioned in the issue does not exist. The current endpoint is `GET /api/persons` with optional `q` query param. Adding `status` to that endpoint is the right move — consistent with the existing pattern. - **`PersonController.getPersons`** currently calls `personService.findAll(q)`. Adding the status filter means `findAll` needs a second parameter or a new method. Given the existing clean split between `findAllWithDocumentCount` and `searchWithDocumentCount`, the cleanest approach is to keep filtering in the service layer (not the controller) and pass `status` through. ### Recommendations 1. **Use a default method on `PersonSummaryDTO`** to compute `completenessStatus` from the existing getters — `getFirstName()`, `getLastName()`, `getBirthYear()`, `getDeathYear()`, and the count of aliases from `getNameAliasCount()` (add one more native query projection column). This avoids DTO conversion while keeping the computation close to the data. 2. **Add `nameAliasCount` (integer) to the native SQL projections** so the DTO can compute completeness without a second DB query. A subselect `(SELECT COUNT(*) FROM person_name_aliases WHERE person_id = p.id) AS nameAliasCount` alongside the existing `documentCount` subselect is the right pattern. 3. **Apply the status filter in `PersonService.findAll`** — accept `(String q, String status)` — and filter the returned list. Keep it in Java; a SQL WHERE on a computed value would require a CTE or subquery, adding query complexity for a list that will stay small. 4. **Place `CompletenessStatus` in `org.raddatz.familienarchiv.person`**, not a non-existent `model/` package. 5. **Update `CLAUDE.md` package table** and `docs/architecture/c4/l3-backend-*.puml` for the new `CompletenessStatus` enum and any controller signature change. The doc obligation table is non-negotiable for PR merge. ### Open Decisions - **Rule for "aliases"**: the issue uses `aliases` to mean the `person_name_aliases` table, but the legacy `Person.alias` varchar column also exists. Clarify whether "has at least one alias" means `nameAliases.size() >= 1` (the formal alias table) or `alias IS NOT NULL` (the legacy column), or both. These are different data concepts in the current schema. - **Pagination future-proofing**: if `/persons` ever gets server-side pagination, post-Java-filtering breaks paging counts. For now it's fine, but a brief design note acknowledging this constraint is worth adding to the implementation.
Author
Owner

👨‍💻 Felix Brandt — Fullstack Developer

Observations

  • PersonSummaryDTO is an interface projection. You cannot add a non-default method to it that returns CompletenessStatus from a database column — there is no such column. A default method that derives status from the other getters works, but it puts business logic in the DTO layer. The clean alternative is PersonService.computeStatus(PersonSummaryDTO) — a pure static or instance method — and the issue correctly identifies this. Recommend keeping the computation in the service.
  • The aliases IS EMPTY rule references nameAliases, which the current PersonSummaryDTO projection does not expose. The projection only has getAlias() (the legacy varchar). To evaluate the alias count rule, you need either a new projection column (nameAliasCount) or a separate query — the former is clearly preferable.
  • PersonController.getPersons signature: currently findAll(String q). Adding status to the controller and service is straightforward, but the controller should pass it through, not apply the filter itself. Controllers delegate; services contain logic.
  • +page.svelte completeness badge inline in the card template. The current /persons/+page.svelte card content is already 170 lines. Adding badge rendering inline will push it well past the 60-line component limit. The badge rendering should be extracted — the issue already plans a PersonCard in frontend/src/lib/components/PersonCard.svelte, but note that the existing /persons/[id]/PersonCard.svelte is the detail card. The list card is currently inline in +page.svelte. The cleaner move is to extract the list card into a named component (e.g., PersonListCard.svelte) in the persons/ route folder.
  • Filter chip state management. The current page uses goto() with a URL query param for search — the right pattern for SvelteKit. The same pattern should be used for the status filter: ?status=incomplete. Derive the active chip from data.status (server-loaded), not from local $state. This ensures URL-shareability and SSR correctness.
  • $effect for q sync. The current page has $effect(() => { if (!qFocused) q = data.q || ''; }). The same effect pattern must be replicated for status if it's reactive. However, for chips where the user clicks rather than types, this is simpler — no debounce needed, goto() fires immediately on chip click.
  • Component naming conflict. The issue refers to frontend/src/lib/components/PersonCard.svelte — this path doesn't exist. The person library components live in $lib/person/. Placing a new PersonCard there is consistent. The detail-page PersonCard.svelte lives at routes/persons/[id]/PersonCard.svelte — a different visual concern. Choose names carefully to avoid confusion across these two locations.

Recommendations

  1. Add nameAliasCount as a projection column to both findAllWithDocumentCount and searchWithDocumentCount native queries. Use a (SELECT COUNT(*) FROM person_name_aliases WHERE person_id = p.id) subselect.
  2. PersonService.computeStatus(PersonSummaryDTO dto) — pure static method, unit-testable with a mock DTO, no Spring context required. Call it after loading the list, or have findAll return enriched DTOs.
  3. Extract PersonListCard.svelte from the inline card markup in +page.svelte. It receives person (with completenessStatus) and renders the full card including the new badge.
  4. **Use goto(\/persons?${params}`)with aURLSearchParamsbuilder** that mergesqandstatus` — preserving both when either changes.
  5. Keep {#each data.persons as person (person.id)} keyed — already correct in the current template. Do not lose the key when adding the badge wrapper.
  6. Run npm run generate:api after adding completenessStatus to PersonSummaryDTO. The TypeScript types will then carry completenessStatus as a required string field. Annotate with @Schema(requiredMode = REQUIRED) on the interface default method if using a class-based DTO, or document the enum string values explicitly in the OpenAPI annotation.
## 👨‍💻 Felix Brandt — Fullstack Developer ### Observations - **`PersonSummaryDTO` is an interface projection.** You cannot add a non-default method to it that returns `CompletenessStatus` from a database column — there is no such column. A `default` method that derives status from the other getters works, but it puts business logic in the DTO layer. The clean alternative is `PersonService.computeStatus(PersonSummaryDTO)` — a pure static or instance method — and the issue correctly identifies this. Recommend keeping the computation in the service. - **The `aliases IS EMPTY` rule references `nameAliases`**, which the current `PersonSummaryDTO` projection does not expose. The projection only has `getAlias()` (the legacy varchar). To evaluate the alias count rule, you need either a new projection column (`nameAliasCount`) or a separate query — the former is clearly preferable. - **`PersonController.getPersons` signature**: currently `findAll(String q)`. Adding `status` to the controller and service is straightforward, but the controller should pass it through, not apply the filter itself. Controllers delegate; services contain logic. - **`+page.svelte` completeness badge inline in the card template.** The current `/persons/+page.svelte` card content is already 170 lines. Adding badge rendering inline will push it well past the 60-line component limit. The badge rendering should be extracted — the issue already plans a `PersonCard` in `frontend/src/lib/components/PersonCard.svelte`, but note that the existing `/persons/[id]/PersonCard.svelte` is the *detail* card. The *list* card is currently inline in `+page.svelte`. The cleaner move is to extract the list card into a named component (e.g., `PersonListCard.svelte`) in the `persons/` route folder. - **Filter chip state management.** The current page uses `goto()` with a URL query param for search — the right pattern for SvelteKit. The same pattern should be used for the `status` filter: `?status=incomplete`. Derive the active chip from `data.status` (server-loaded), not from local `$state`. This ensures URL-shareability and SSR correctness. - **`$effect` for `q` sync.** The current page has `$effect(() => { if (!qFocused) q = data.q || ''; })`. The same effect pattern must be replicated for `status` if it's reactive. However, for chips where the user clicks rather than types, this is simpler — no debounce needed, `goto()` fires immediately on chip click. - **Component naming conflict.** The issue refers to `frontend/src/lib/components/PersonCard.svelte` — this path doesn't exist. The person library components live in `$lib/person/`. Placing a new `PersonCard` there is consistent. The detail-page `PersonCard.svelte` lives at `routes/persons/[id]/PersonCard.svelte` — a different visual concern. Choose names carefully to avoid confusion across these two locations. ### Recommendations 1. **Add `nameAliasCount` as a projection column** to both `findAllWithDocumentCount` and `searchWithDocumentCount` native queries. Use a `(SELECT COUNT(*) FROM person_name_aliases WHERE person_id = p.id)` subselect. 2. **`PersonService.computeStatus(PersonSummaryDTO dto)`** — pure static method, unit-testable with a mock DTO, no Spring context required. Call it after loading the list, or have `findAll` return enriched DTOs. 3. **Extract `PersonListCard.svelte`** from the inline card markup in `+page.svelte`. It receives `person` (with `completenessStatus`) and renders the full card including the new badge. 4. **Use `goto(\`/persons?${params}\`)` with a `URLSearchParams` builder** that merges `q` and `status` — preserving both when either changes. 5. **Keep `{#each data.persons as person (person.id)}`** keyed — already correct in the current template. Do not lose the key when adding the badge wrapper. 6. **Run `npm run generate:api`** after adding `completenessStatus` to `PersonSummaryDTO`. The TypeScript types will then carry `completenessStatus` as a required string field. Annotate with `@Schema(requiredMode = REQUIRED)` on the interface default method if using a class-based DTO, or document the enum string values explicitly in the OpenAPI annotation.
Author
Owner

👨‍💻 Tobias Wendt — DevOps & Platform Engineer

Observations

  • No infrastructure changes required. This feature is pure application-layer: new Java enum, service method, DTO field, and SvelteKit UI. No new Docker service, no Flyway migration, no volume, no port. Clean from a platform perspective.
  • CI pipeline impact is negligible. The new PersonServiceTest.computeStatus unit tests and PersonControllerTest additions will add a few hundred milliseconds to the test run at most. Well within the <10s unit test target and <2min integration target.
  • No new environment variables or secrets needed. The filter is URL-param-driven and computed in-process.
  • Existing health checks are unaffected. The Spring Boot actuator /actuator/health endpoint and the existing MinIO/PostgreSQL depends_on: service_healthy conditions in docker-compose.yml need no changes.
  • The docker-compose.yml has pending changes (shown in git status as modified). If those changes introduce anything relevant to the person domain (e.g., a new service), it should be reviewed before this feature branch is created. Since I can't see the diff, flagging this as a precaution.

Recommendations

  1. No Compose, CI, or infrastructure changes needed for this feature. Proceed directly to the application implementation.
  2. Verify the modified docker-compose.yml (visible in git status) is committed or intentional before branching — to avoid carrying unrelated changes into the feature branch.
  3. Smoke test addition: after this feature ships, add one smoke-test assertion: curl -s .../api/persons?status=incomplete | jq '.total' (or equivalent) to the post-deploy verification script, as the issue itself suggests. This confirms the filter is wired end-to-end in production without needing a full E2E run.
## 👨‍💻 Tobias Wendt — DevOps & Platform Engineer ### Observations - **No infrastructure changes required.** This feature is pure application-layer: new Java enum, service method, DTO field, and SvelteKit UI. No new Docker service, no Flyway migration, no volume, no port. Clean from a platform perspective. - **CI pipeline impact is negligible.** The new `PersonServiceTest.computeStatus` unit tests and `PersonControllerTest` additions will add a few hundred milliseconds to the test run at most. Well within the <10s unit test target and <2min integration target. - **No new environment variables or secrets needed.** The filter is URL-param-driven and computed in-process. - **Existing health checks are unaffected.** The Spring Boot actuator `/actuator/health` endpoint and the existing MinIO/PostgreSQL `depends_on: service_healthy` conditions in `docker-compose.yml` need no changes. - **The `docker-compose.yml` has pending changes** (shown in git status as modified). If those changes introduce anything relevant to the person domain (e.g., a new service), it should be reviewed before this feature branch is created. Since I can't see the diff, flagging this as a precaution. ### Recommendations 1. **No Compose, CI, or infrastructure changes needed for this feature.** Proceed directly to the application implementation. 2. **Verify the modified `docker-compose.yml`** (visible in git status) is committed or intentional before branching — to avoid carrying unrelated changes into the feature branch. 3. **Smoke test addition**: after this feature ships, add one smoke-test assertion: `curl -s .../api/persons?status=incomplete | jq '.total'` (or equivalent) to the post-deploy verification script, as the issue itself suggests. This confirms the filter is wired end-to-end in production without needing a full E2E run.
Author
Owner

👨‍💻 Elicit — Requirements Engineer

Observations

  • The completeness rule set is well-specified but has one ambiguity: the INCOMPLETE condition firstName AND lastName both missing, aliases size ≤ 1 appears to overlap with the first two INCOMPLETE conditions (lastName IS NULL or = '?'). If lastName is NULL, the first condition already fires. The third condition (both missing AND aliases ≤ 1) seems to target a specific edge case — a person with no name at all but one alias — but this could never pass the lastName NOT NULL DB constraint seen in the entity (@Column(nullable = false)). Clarify whether this condition is reachable given the schema constraint, or remove it.
  • aliases ambiguity: the issue uses aliases loosely. The Person entity has both alias (legacy varchar, single string) and nameAliases (the person_name_aliases table, a proper collection). The INCOMPLETE/PARTIAL rules reference aliases IS EMPTY and aliases size ≤ 1 — which collection is meant? The legacy alias field is always a single string (or null); the nameAliases collection can have 0..N entries. These are semantically different.
  • PARTIAL condition is additive and has an OR: "birthYear IS NULL AND deathYear IS NULL OR aliases IS EMPTY." This OR means a person with known birth/death years but no aliases is still PARTIAL. Is this intentional? Persons like ? ? with 13 documents might have no formal aliases but are clearly incomplete by other signals. The OR could cause many otherwise-clean persons to show as PARTIAL.
  • Counts in filter chips "reflect the current search": this is an important UX spec detail. If the user searches q=Hans and clicks Unvollständig, the count shown in the chip should be the count of incomplete persons matching "Hans" — not the global count. This requires the backend to return counts broken down by status for the current search result, which the current API design does not support. The issue says "Counts in chips reflect the current search" in the AC but does not address how the backend delivers these counts. This is a gap.
  • Acceptance criteria are strong and specific — the Gherkin-style verification steps at the bottom are particularly well-formed. No issues there.
  • Non-goals are clearly stated — no auto-merge, no crowd UI, no import changes. Good scope discipline.
  • i18n keys list is incomplete: the issue lists persons_filter_all, persons_filter_incomplete, persons_filter_partial, persons_filter_complete, person_badge_incomplete, person_badge_partial — but not person_badge_complete. If COMPLETE persons show no badge (by design), this is fine and should be explicitly stated: "COMPLETE persons render no badge."

Recommendations

  1. Clarify the aliases term in the completeness rule: specify exactly which field — alias (varchar), nameAliases (collection), or both — is used for each INCOMPLETE/PARTIAL condition. A one-line note in the implementation plan is enough.
  2. Remove or document the firstName AND lastName both missing, aliases size ≤ 1 INCOMPLETE sub-rule if lastName is constrained NOT NULL at the schema level and this case cannot occur.
  3. Add a specification for chip counts: either (a) the backend returns a { incomplete: N, partial: M, complete: L } count breakdown alongside the list (a new response field), or (b) counts are computed client-side from the full unfiltered list — which breaks when q is active. This is a genuine design decision needing resolution before implementation.
  4. Explicitly state that COMPLETE persons render no badge so the i18n key list is intentionally incomplete at the UI layer.

Open Decisions

  • Chip count data source: Does the API return per-status counts for the active search, or does the frontend compute counts from the loaded list? The issue's acceptance criterion "Counts in chips reflect the current search" implies the former, but the implementation plan does not include it.
  • OR vs. AND in PARTIAL rule: should aliases IS EMPTY alone (without missing birth/death) be sufficient to classify a person as PARTIAL? This will likely flag a large portion of the 221 persons.
## 👨‍💻 Elicit — Requirements Engineer ### Observations - **The completeness rule set is well-specified but has one ambiguity**: the INCOMPLETE condition `firstName AND lastName both missing, aliases size ≤ 1` appears to overlap with the first two INCOMPLETE conditions (lastName IS NULL or = '?'). If lastName is NULL, the first condition already fires. The third condition (`both missing AND aliases ≤ 1`) seems to target a specific edge case — a person with no name at all but one alias — but this could never pass the `lastName NOT NULL` DB constraint seen in the entity (`@Column(nullable = false)`). Clarify whether this condition is reachable given the schema constraint, or remove it. - **`aliases` ambiguity**: the issue uses `aliases` loosely. The `Person` entity has both `alias` (legacy varchar, single string) and `nameAliases` (the `person_name_aliases` table, a proper collection). The INCOMPLETE/PARTIAL rules reference `aliases IS EMPTY` and `aliases size ≤ 1` — which collection is meant? The legacy `alias` field is always a single string (or null); the `nameAliases` collection can have 0..N entries. These are semantically different. - **PARTIAL condition is additive and has an OR**: "birthYear IS NULL AND deathYear IS NULL OR aliases IS EMPTY." This OR means a person with known birth/death years but no aliases is still PARTIAL. Is this intentional? Persons like `? ?` with 13 documents might have no formal aliases but are clearly incomplete by other signals. The OR could cause many otherwise-clean persons to show as PARTIAL. - **Counts in filter chips "reflect the current search"**: this is an important UX spec detail. If the user searches `q=Hans` and clicks `Unvollständig`, the count shown in the chip should be the count of incomplete persons matching "Hans" — not the global count. This requires the backend to return counts broken down by status for the current search result, which the current API design does not support. The issue says "Counts in chips reflect the current search" in the AC but does not address how the backend delivers these counts. This is a gap. - **Acceptance criteria are strong and specific** — the Gherkin-style verification steps at the bottom are particularly well-formed. No issues there. - **Non-goals are clearly stated** — no auto-merge, no crowd UI, no import changes. Good scope discipline. - **i18n keys list is incomplete**: the issue lists `persons_filter_all`, `persons_filter_incomplete`, `persons_filter_partial`, `persons_filter_complete`, `person_badge_incomplete`, `person_badge_partial` — but not `person_badge_complete`. If COMPLETE persons show no badge (by design), this is fine and should be explicitly stated: "COMPLETE persons render no badge." ### Recommendations 1. **Clarify the `aliases` term** in the completeness rule: specify exactly which field — `alias` (varchar), `nameAliases` (collection), or both — is used for each INCOMPLETE/PARTIAL condition. A one-line note in the implementation plan is enough. 2. **Remove or document the `firstName AND lastName both missing, aliases size ≤ 1` INCOMPLETE sub-rule** if `lastName` is constrained NOT NULL at the schema level and this case cannot occur. 3. **Add a specification for chip counts**: either (a) the backend returns a `{ incomplete: N, partial: M, complete: L }` count breakdown alongside the list (a new response field), or (b) counts are computed client-side from the full unfiltered list — which breaks when `q` is active. This is a genuine design decision needing resolution before implementation. 4. **Explicitly state that COMPLETE persons render no badge** so the i18n key list is intentionally incomplete at the UI layer. ### Open Decisions - **Chip count data source**: Does the API return per-status counts for the active search, or does the frontend compute counts from the loaded list? The issue's acceptance criterion "Counts in chips reflect the current search" implies the former, but the implementation plan does not include it. - **OR vs. AND in PARTIAL rule**: should `aliases IS EMPTY` alone (without missing birth/death) be sufficient to classify a person as PARTIAL? This will likely flag a large portion of the 221 persons.
Author
Owner

👨‍💻 Nora "NullX" Steiner — Security Engineer

Observations

  • No new attack surface introduced. The status query parameter is a filter on an enum (incomplete|partial|complete|all). It drives a Java-side list filter — it does not enter a JPQL query or native SQL string. There is no injection risk.
  • The GET /api/persons endpoint already requires @RequirePermission(Permission.READ_ALL), which is correct and carries over naturally to the status-filtered response. No new endpoint means no new permission gap.
  • The PersonController uses ResponseStatusException for validation errors in several places (e.g., validatePersonNames) rather than DomainException. Adding status param validation should use the same lightweight approach: if status is an unrecognized value, return 400. Since it's an enum, Spring can deserialize it automatically with @RequestParam(required = false) CompletenessStatus status — an invalid string will trigger a Spring-managed MethodArgumentTypeMismatchException resulting in a 400 before any service code runs. This is safe by default.
  • No new ErrorCode required unless the feature decides to surface invalid status values as structured errors. Given this is a filter param (not a domain operation), a plain 400 from Spring's type coercion is sufficient and consistent with the existing style.
  • The PersonSummaryDTO will expose completenessStatus in the API response. This is metadata about data quality, not sensitive personal data. No privacy concern.
  • TypeScript type generation: once completenessStatus is in the OpenAPI spec, it must not be typed as any on the frontend. The @Schema annotation (with requiredMode = REQUIRED if it is always present) drives the generated type. Verify after npm run generate:api that the field is non-optional in the generated TypeScript.

Recommendations

  1. Use @RequestParam(required = false) CompletenessStatus status in the controller — let Spring's enum deserialization handle validation. Invalid values produce a 400 automatically, no manual validation code needed.
  2. No new @RequirePermission annotation needed — the existing READ_ALL check on GET /api/persons is the correct gate. Confirm it remains on the updated endpoint signature.
  3. Audit PersonController for missing @RequirePermission on getCorrespondents and getPersonDocuments: these endpoints currently have no permission annotation (visible in the current code). They are accessible to any authenticated user. This is pre-existing, but worth flagging — especially getPersonDocuments, which returns full document objects. Consider adding @RequirePermission(Permission.READ_ALL) to close this gap as part of this PR.
## 👨‍💻 Nora "NullX" Steiner — Security Engineer ### Observations - **No new attack surface introduced.** The `status` query parameter is a filter on an enum (`incomplete|partial|complete|all`). It drives a Java-side list filter — it does not enter a JPQL query or native SQL string. There is no injection risk. - **The `GET /api/persons` endpoint already requires `@RequirePermission(Permission.READ_ALL)`**, which is correct and carries over naturally to the status-filtered response. No new endpoint means no new permission gap. - **The `PersonController` uses `ResponseStatusException` for validation errors** in several places (e.g., `validatePersonNames`) rather than `DomainException`. Adding `status` param validation should use the same lightweight approach: if `status` is an unrecognized value, return 400. Since it's an enum, Spring can deserialize it automatically with `@RequestParam(required = false) CompletenessStatus status` — an invalid string will trigger a Spring-managed `MethodArgumentTypeMismatchException` resulting in a 400 before any service code runs. This is safe by default. - **No new `ErrorCode` required** unless the feature decides to surface invalid `status` values as structured errors. Given this is a filter param (not a domain operation), a plain 400 from Spring's type coercion is sufficient and consistent with the existing style. - **The `PersonSummaryDTO` will expose `completenessStatus`** in the API response. This is metadata about data quality, not sensitive personal data. No privacy concern. - **TypeScript type generation**: once `completenessStatus` is in the OpenAPI spec, it must not be typed as `any` on the frontend. The `@Schema` annotation (with `requiredMode = REQUIRED` if it is always present) drives the generated type. Verify after `npm run generate:api` that the field is non-optional in the generated TypeScript. ### Recommendations 1. **Use `@RequestParam(required = false) CompletenessStatus status`** in the controller — let Spring's enum deserialization handle validation. Invalid values produce a 400 automatically, no manual validation code needed. 2. **No new `@RequirePermission` annotation needed** — the existing `READ_ALL` check on `GET /api/persons` is the correct gate. Confirm it remains on the updated endpoint signature. 3. **Audit `PersonController` for missing `@RequirePermission` on `getCorrespondents` and `getPersonDocuments`**: these endpoints currently have no permission annotation (visible in the current code). They are accessible to any authenticated user. This is pre-existing, but worth flagging — especially `getPersonDocuments`, which returns full document objects. Consider adding `@RequirePermission(Permission.READ_ALL)` to close this gap as part of this PR.
Author
Owner

👨‍💻 Sara Holt — QA Engineer & Test Strategist

Observations

  • The test plan is well-structured. Unit tests for computeStatus, controller slice tests for the status param, component tests for PersonCard badge rendering, and an E2E test for the filter — this covers all four pyramid layers correctly.
  • PersonServiceTest uses @ExtendWith(MockitoExtension.class) with @Mock + @InjectMocks — the correct pattern. The new computeStatus unit tests should follow the same class and use the same setup. No Spring context overhead.
  • The computeStatus rule set has multiple branches (INCOMPLETE conditions are OR-chained, PARTIAL has an OR). Each branch needs an isolated test with a factory method producing exactly the right person state. The issue calls this out ("include real placeholder samples from current data") — this is the right instinct. I'd formalize it as: one test per INCOMPLETE condition, one per PARTIAL condition, one COMPLETE test, and at least two real-world placeholder names from the live data (e.g., ? ?, Carl ?).
  • PersonControllerTest will need a mockPersonSummaryWithStatus helper that includes completenessStatus. The existing mockPersonSummary anonymous implementation does not return a completenessStatus getter — it will need to be extended.
  • The E2E test ("visit /persons?status=incomplete, assert only placeholder rows visible") is appropriate for the critical journey layer. However, this test will depend on seed data. Make sure the E2E seed includes at least one known INCOMPLETE person and one known COMPLETE person so the assertion is deterministic.
  • Component test scope: PersonCard badge rendering should test three states — INCOMPLETE (badge visible, muted class), PARTIAL (badge visible), and COMPLETE (no badge). Use render() from vitest-browser-svelte and getByRole('status') or a data-testid if the badge has no semantic role.
  • The filter chip count accuracy (chips reflect current search) is identified in the requirements as an AC but no test is specified for it. This is a gap — if the counts are wrong under search, the AC is not met.
  • Regression test for /persons without status param: confirm existing sort, search, and pagination still work unchanged. The issue calls this out, but it should be an explicit test case, not just a manual check.

Recommendations

  1. Add computeStatus test cases as a @ParameterizedTest with a stream of Arguments covering each rule branch — one per INCOMPLETE sub-condition, PARTIAL with/without birth year, PARTIAL with/without aliases, and COMPLETE. Parameterized tests are more readable for rule tables than individual @Test methods.
  2. Test status=all explicitly in the controller test: verify it returns the same count as no-status, and that the service is called with null status (or an explicit ALL enum value) in that case.
  3. Test status=unknown_value in the controller test: verify it returns 400 (Spring enum deserialization rejection).
  4. Add a chip-count test — either a controller integration test returning { counts: { incomplete: N, partial: M, complete: L } } in the response, or (if counts are client-computed) a frontend unit test on the count derivation logic.
  5. Use data-testid="completeness-badge" on the badge so component and E2E tests have a stable selector that doesn't break on label text changes (e.g., when i18n translations change).
## 👨‍💻 Sara Holt — QA Engineer & Test Strategist ### Observations - **The test plan is well-structured.** Unit tests for `computeStatus`, controller slice tests for the `status` param, component tests for `PersonCard` badge rendering, and an E2E test for the filter — this covers all four pyramid layers correctly. - **`PersonServiceTest` uses `@ExtendWith(MockitoExtension.class)` with `@Mock` + `@InjectMocks`** — the correct pattern. The new `computeStatus` unit tests should follow the same class and use the same setup. No Spring context overhead. - **The `computeStatus` rule set has multiple branches** (INCOMPLETE conditions are OR-chained, PARTIAL has an OR). Each branch needs an isolated test with a factory method producing exactly the right person state. The issue calls this out ("include real placeholder samples from current data") — this is the right instinct. I'd formalize it as: one test per INCOMPLETE condition, one per PARTIAL condition, one COMPLETE test, and at least two real-world placeholder names from the live data (e.g., `? ?`, `Carl ?`). - **`PersonControllerTest` will need a `mockPersonSummaryWithStatus` helper** that includes `completenessStatus`. The existing `mockPersonSummary` anonymous implementation does not return a `completenessStatus` getter — it will need to be extended. - **The E2E test** ("visit `/persons?status=incomplete`, assert only placeholder rows visible") is appropriate for the critical journey layer. However, this test will depend on seed data. Make sure the E2E seed includes at least one known INCOMPLETE person and one known COMPLETE person so the assertion is deterministic. - **Component test scope**: `PersonCard` badge rendering should test three states — INCOMPLETE (badge visible, muted class), PARTIAL (badge visible), and COMPLETE (no badge). Use `render()` from `vitest-browser-svelte` and `getByRole('status')` or a `data-testid` if the badge has no semantic role. - **The filter chip count accuracy** (chips reflect current search) is identified in the requirements as an AC but no test is specified for it. This is a gap — if the counts are wrong under search, the AC is not met. - **Regression test for `/persons` without `status` param**: confirm existing sort, search, and pagination still work unchanged. The issue calls this out, but it should be an explicit test case, not just a manual check. ### Recommendations 1. **Add `computeStatus` test cases as a `@ParameterizedTest`** with a stream of `Arguments` covering each rule branch — one per INCOMPLETE sub-condition, PARTIAL with/without birth year, PARTIAL with/without aliases, and COMPLETE. Parameterized tests are more readable for rule tables than individual `@Test` methods. 2. **Test `status=all` explicitly** in the controller test: verify it returns the same count as no-status, and that the service is called with `null` status (or an explicit `ALL` enum value) in that case. 3. **Test `status=unknown_value`** in the controller test: verify it returns 400 (Spring enum deserialization rejection). 4. **Add a chip-count test** — either a controller integration test returning `{ counts: { incomplete: N, partial: M, complete: L } }` in the response, or (if counts are client-computed) a frontend unit test on the count derivation logic. 5. **Use `data-testid="completeness-badge"` on the badge** so component and E2E tests have a stable selector that doesn't break on label text changes (e.g., when i18n translations change).
Author
Owner

👨‍💻 Leonie Voss — UI/UX Design Lead

Observations

  • The badge must use redundant cues — not color alone. The audience includes users 60+. A muted color tint + small text badge is insufficient if color is the only differentiator. Each status badge needs an icon or a visible text label (or both) alongside the color.
  • "Muted variant + small badge" is vague as a design spec. The existing card pattern uses bg-surface border border-line shadow-sm for the card body. A badge that reads Unvollständig in text-[11px] on a muted background may fail the WCAG 4.5:1 contrast requirement. The brand-mint color on white already fails at ~2.8:1 — do not use mint for badge text. Use text-ink-2 on bg-muted (which has sufficient contrast based on the current palette).
  • Filter chip group placement. The issue proposes chips "under the search bar." The current layout has the search bar in the top-right, separate from the title. Putting the chip group below the search bar (floating right) creates a visually disconnected filter strip. Better: place the chip group as a horizontal row below the full header section, spanning the full width — consistent with how other list pages handle filters in this codebase.
  • Chip tap targets. The filter chips must be at least 44×44px touch targets. A compact row of four chips labeled Alle (221) · Unvollständig (N) · Teilweise (M) · Vollständig (L) needs sufficient padding — py-2 px-4 minimum for each chip.
  • "Muted" person card in the list. The issue mentions a "muted variant" for INCOMPLETE/PARTIAL cards. The risk here is usability: if muting reduces contrast of the person's name, it may fall below AA for senior users. Recommend a distinct border indicator (e.g., border-l-4 border-amber-400 for PARTIAL, border-l-4 border-red-400 for INCOMPLETE) rather than a text-color mute — which preserves readability while adding a clear visual signal.
  • Person detail page badge. Placing the badge "beside the name" in the PersonCard.svelte detail view is straightforward. The badge should be inline after the <h1> name — using flex items-baseline gap-2 to keep the name and badge on the same line without vertical misalignment.
  • No dark-mode specification. The badge colors need a dark-mode variant. Amber and red border accents work well in both modes; text-amber-600 (light) / text-amber-400 (dark) is a common safe pairing. Add dark: variants to any color class used on badges.
  • Focus ring on filter chips. Each chip that is a button or link must have focus-visible:ring-2 focus-visible:ring-brand-navy — the existing project focus pattern. Chips without visible focus indicators fail WCAG 2.4.7.

Recommendations

  1. Use a left border accent (border-l-4) for incomplete card variants rather than a full muted overlay — preserves name readability for senior users while adding a clear visual cue.
  2. Add both a status label text and a small icon to the badge — e.g., a warning triangle (⚠) for INCOMPLETE, a partial circle for PARTIAL. Never rely on color alone.
  3. Place the filter chip group as a full-width row below the page header, not attached to the search bar. Use role="group" aria-label="Nach Vollständigkeit filtern" on the container.
  4. Minimum chip sizing: min-h-[44px] with px-4 on each chip. Use aria-pressed="true/false" (or aria-current="true") on the active chip.
  5. Verify contrast before shipping: run the badge text color against its background in both light and dark mode with a tool. Target 4.5:1 minimum; 7:1 preferred for the senior audience.
  6. Add data-theme dark mode test in the Playwright E2E for the badge — the axe-core a11y check should run in both light and dark mode as per the existing QA standard.

Open Decisions

  • INCOMPLETE card visual treatment: left-border accent vs. muted overlay vs. a subtle background tint — all achieve differentiation, each has trade-offs. A muted overlay affects all text contrast; a left border is additive. Choose before implementation so the component spec is unambiguous.
## 👨‍💻 Leonie Voss — UI/UX Design Lead ### Observations - **The badge must use redundant cues — not color alone.** The audience includes users 60+. A muted color tint + small text badge is insufficient if color is the only differentiator. Each status badge needs an icon or a visible text label (or both) alongside the color. - **"Muted variant + small badge"** is vague as a design spec. The existing card pattern uses `bg-surface border border-line shadow-sm` for the card body. A badge that reads `Unvollständig` in `text-[11px]` on a muted background may fail the WCAG 4.5:1 contrast requirement. The brand-mint color on white already fails at ~2.8:1 — do not use mint for badge text. Use `text-ink-2` on `bg-muted` (which has sufficient contrast based on the current palette). - **Filter chip group placement.** The issue proposes chips "under the search bar." The current layout has the search bar in the top-right, separate from the title. Putting the chip group below the search bar (floating right) creates a visually disconnected filter strip. Better: place the chip group as a horizontal row below the full header section, spanning the full width — consistent with how other list pages handle filters in this codebase. - **Chip tap targets.** The filter chips must be at least 44×44px touch targets. A compact row of four chips labeled `Alle (221) · Unvollständig (N) · Teilweise (M) · Vollständig (L)` needs sufficient padding — `py-2 px-4` minimum for each chip. - **"Muted" person card in the list.** The issue mentions a "muted variant" for INCOMPLETE/PARTIAL cards. The risk here is usability: if muting reduces contrast of the person's name, it may fall below AA for senior users. Recommend a distinct border indicator (e.g., `border-l-4 border-amber-400` for PARTIAL, `border-l-4 border-red-400` for INCOMPLETE) rather than a text-color mute — which preserves readability while adding a clear visual signal. - **Person detail page badge.** Placing the badge "beside the name" in the `PersonCard.svelte` detail view is straightforward. The badge should be inline after the `<h1>` name — using `flex items-baseline gap-2` to keep the name and badge on the same line without vertical misalignment. - **No dark-mode specification.** The badge colors need a dark-mode variant. Amber and red border accents work well in both modes; `text-amber-600` (light) / `text-amber-400` (dark) is a common safe pairing. Add `dark:` variants to any color class used on badges. - **Focus ring on filter chips.** Each chip that is a button or link must have `focus-visible:ring-2 focus-visible:ring-brand-navy` — the existing project focus pattern. Chips without visible focus indicators fail WCAG 2.4.7. ### Recommendations 1. **Use a left border accent (`border-l-4`) for incomplete card variants** rather than a full muted overlay — preserves name readability for senior users while adding a clear visual cue. 2. **Add both a status label text and a small icon to the badge** — e.g., a warning triangle (⚠) for INCOMPLETE, a partial circle for PARTIAL. Never rely on color alone. 3. **Place the filter chip group as a full-width row below the page header**, not attached to the search bar. Use `role="group" aria-label="Nach Vollständigkeit filtern"` on the container. 4. **Minimum chip sizing**: `min-h-[44px]` with `px-4` on each chip. Use `aria-pressed="true/false"` (or `aria-current="true"`) on the active chip. 5. **Verify contrast before shipping**: run the badge text color against its background in both light and dark mode with a tool. Target 4.5:1 minimum; 7:1 preferred for the senior audience. 6. **Add `data-theme` dark mode test** in the Playwright E2E for the badge — the axe-core a11y check should run in both light and dark mode as per the existing QA standard. ### Open Decisions - **INCOMPLETE card visual treatment**: left-border accent vs. muted overlay vs. a subtle background tint — all achieve differentiation, each has trade-offs. A muted overlay affects all text contrast; a left border is additive. Choose before implementation so the component spec is unambiguous.
Author
Owner

Decision Queue

Consolidated open decisions raised by the review personas. These require a human call before implementation starts.


Theme 1 — "Aliases" field ambiguity (Markus, Elicit)

The completeness rules reference aliases IS EMPTY and aliases size ≤ 1, but the Person entity has two different alias concepts:

  • alias — a single legacy varchar column (from ODS import, always a string or null)
  • nameAliases — the person_name_aliases table collection (0..N formal alias entries)

Decision needed: For each INCOMPLETE/PARTIAL rule condition, specify which field is checked — the legacy alias varchar, the person_name_aliases count, or both.


Theme 2 — Chip count data source (Elicit, Sara)

The acceptance criterion says "Counts in chips reflect the current search (not fixed totals)." Two implementation options exist:

  • Option A: Backend returns a { counts: { incomplete: N, partial: M, complete: L } } breakdown alongside the person list — computed for the active q search. Requires a new response structure.
  • Option B: Frontend computes counts from the loaded list client-side. Simple, but breaks if only a filtered subset is loaded (i.e., when status is already applied, the chip counts for other statuses are wrong).

Decision needed: Choose Option A or B. If A, the API response shape and endpoint design need to be specified before backend implementation.


Theme 3 — PARTIAL rule: OR vs. AND (Elicit, Leonie)

The PARTIAL condition is currently: birthYear IS NULL AND deathYear IS NULL OR aliases IS EMPTY.

Using OR means a person with fully known birth/death years but no aliases is still PARTIAL. This will likely classify a large fraction of the 221 persons as PARTIAL (most historical persons have no formal aliases).

Decision needed: Should aliases IS EMPTY alone be sufficient for PARTIAL, or must it be combined with missing life dates? Consider running a quick count against real data before deciding.


Theme 4 — INCOMPLETE card visual treatment (Leonie)

Three options for visually differentiating INCOMPLETE/PARTIAL cards in the /persons list:

  • Left border accent (border-l-4 border-amber-400 / border-red-400) — additive, preserves text contrast
  • Muted overlay (reduced opacity or gray tint) — risks dropping name text below WCAG AA contrast for senior users
  • Subtle background tint (bg-amber-50 dark:bg-amber-950/20) — visible but conservative

Decision needed: Choose a visual treatment before the PersonListCard.svelte component is built. The treatment must work in both light and dark mode.

## Decision Queue Consolidated open decisions raised by the review personas. These require a human call before implementation starts. --- ### Theme 1 — "Aliases" field ambiguity (Markus, Elicit) The completeness rules reference `aliases IS EMPTY` and `aliases size ≤ 1`, but the `Person` entity has **two different alias concepts**: - `alias` — a single legacy varchar column (from ODS import, always a string or null) - `nameAliases` — the `person_name_aliases` table collection (0..N formal alias entries) **Decision needed:** For each INCOMPLETE/PARTIAL rule condition, specify which field is checked — the legacy `alias` varchar, the `person_name_aliases` count, or both. --- ### Theme 2 — Chip count data source (Elicit, Sara) The acceptance criterion says "Counts in chips reflect the current search (not fixed totals)." Two implementation options exist: - **Option A**: Backend returns a `{ counts: { incomplete: N, partial: M, complete: L } }` breakdown alongside the person list — computed for the active `q` search. Requires a new response structure. - **Option B**: Frontend computes counts from the loaded list client-side. Simple, but breaks if only a filtered subset is loaded (i.e., when `status` is already applied, the chip counts for other statuses are wrong). **Decision needed:** Choose Option A or B. If A, the API response shape and endpoint design need to be specified before backend implementation. --- ### Theme 3 — PARTIAL rule: OR vs. AND (Elicit, Leonie) The PARTIAL condition is currently: `birthYear IS NULL AND deathYear IS NULL` **OR** `aliases IS EMPTY`. Using OR means a person with fully known birth/death years but no aliases is still PARTIAL. This will likely classify a large fraction of the 221 persons as PARTIAL (most historical persons have no formal aliases). **Decision needed:** Should `aliases IS EMPTY` alone be sufficient for PARTIAL, or must it be combined with missing life dates? Consider running a quick count against real data before deciding. --- ### Theme 4 — INCOMPLETE card visual treatment (Leonie) Three options for visually differentiating INCOMPLETE/PARTIAL cards in the `/persons` list: - **Left border accent** (`border-l-4 border-amber-400` / `border-red-400`) — additive, preserves text contrast - **Muted overlay** (reduced opacity or gray tint) — risks dropping name text below WCAG AA contrast for senior users - **Subtle background tint** (`bg-amber-50 dark:bg-amber-950/20`) — visible but conservative **Decision needed:** Choose a visual treatment before the `PersonListCard.svelte` component is built. The treatment must work in both light and dark mode.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#323