Commit Graph

1716 Commits

Author SHA1 Message Date
Marcel
995471082e test(documents): update obsolete em-dash assertion to undated badge
The "missing documentDate" test asserted the OLD bare em-dash; #668
replaced it with the "Datum unbekannt" badge via <DocumentDate>. Assert
the badge text and rename the misleading test title.

Refs #668

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:54:24 +02:00
Marcel
c6137a26a2 feat(documents): show global undated count chip on the filter toggle
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m50s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Failing after 4m3s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m3s
Surface the backend's global undatedCount on the "Nur undatierte" toggle as
a count chip — the total undated documents matching the current filter
across all pages, not the page slice. The loader forwards undatedCount
straight through (defaulting to 0); the chip hides at 0 and stays visible
regardless of the toggle state so it advertises the triage backlog size.

generate:api was hand-edited (undatedCount added to DocumentSearchResult) —
CI must re-run npm run generate:api to confirm parity.

Refs #668

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:42:57 +02:00
Marcel
19cd17d9cd fix(documents): always render undated badge in DocumentRow desktop column
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m54s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m40s
CI / fail2ban Regex (pull_request) Successful in 41s
CI / Semgrep Security Scan (pull_request) Successful in 19s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m2s
The desktop right-column kept a leftover {#if doc.documentDate}…{:else}—{/if}
fallback that emitted a bare em-dash for undated documents, while the mobile
block already always rendered <DocumentDate>. DocumentDate defensively maps a
null date to the "Datum unbekannt" badge, so render it unconditionally — an
undated document is an absence, not an error, and never shows a bare "—".

Refs #668
2026-05-27 19:17:18 +02:00
Marcel
508575eccb refactor(documents): collapse redundant span nesting in DocumentDate else branch
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 3m43s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m1s
The dated branch wrapped {label} in a flex span containing a single child
span — redundant nesting. Render the label directly in one span.

Refs #668

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:09:07 +02:00
Marcel
85372e3669 fix(documents): enlarge undated badge text to text-xs for legibility
"Datum unbekannt" is a semantically meaningful date surface, not decorative
chrome, so the 10px chip text is too small for the senior reader audience.
Bump to text-xs (≥12px) per the WCAG min-legible-text guidance.

Refs #668

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:08:41 +02:00
Marcel
a345bba74b test(activity): assert Chronik rows never fabricate a letter date
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m54s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m30s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m3s
Negative guarantee for #668: ChronikRow renders the activity timestamp
(happenedAt), and ActivityFeedItemDTO carries no document-date surface, so
no undated badge or "Datum unbekannt" letter-date label may appear. Pins
this as a regression fixture so a future change can't quietly add a date
chip to the activity feed.

Refs #668
2026-05-27 18:54:35 +02:00
Marcel
098c2c9def feat(documents): add a "Nur undatierte" filter toggle wired to the URL
SearchFilterBar gains an aria-pressed "Nur undatierte" toggle in the
advanced row (min-h-[44px] touch target, labels the state not the colour).
The documents page threads `undated` through the filter snapshot so it is a
shareable URL param picked up by both filter-change nav and pagination, and
flows into the bulk-edit "select all" /ids request. Toggling resets to page
0 via the existing implicit page-drop.

Refs #668
2026-05-27 18:53:44 +02:00
Marcel
5d8bb70255 feat(documents): explain that a date range excludes undated documents
DocumentList gains from/to props; when a date range is active and yields no
results, the empty state shows the localized docs_range_excludes_undated
note instead of the generic copy, so the reader understands undated letters
aren't part of a range. Person-grouped modes keep undated letters under
their sender/receiver (badge-on-row, no synthetic sub-group).

Refs #668
2026-05-27 18:50:18 +02:00
Marcel
bca3f34cec feat(documents): badge undated rows instead of a bare em-dash
DocumentRow rendered a bare em-dash for null-dated letters — a glyph a
screen reader announces as nothing. Both breakpoints now render the single
DocumentDate component unconditionally (no {#if}/—/{:else}), so the cue
cannot drift; its unknown state is a neutral metadata chip ("Datum
unbekannt", text-ink-3, ≥4.5:1 both themes) with a non-color calendar glyph,
never red/amber. Present dates render at honest precision via
formatDocumentDate ("Juni 1916", not a fabricated day).

Refs #668
2026-05-27 18:48:45 +02:00
Marcel
f1fc3dc1ce feat(documents): thread undated filter through the search loader + i18n
Parses ?undated strictly (=== 'true', mirroring the tagOp clamp), forwards
it as undated || undefined so the absent case drops out of the query, and
returns the flag in page data for the control to reflect. Adds the
docs_filter_undated_only toggle label and the explanatory
docs_range_excludes_undated empty-state copy in de/en/es. The badge reuses
the existing date_precision_unknown ("Datum unbekannt") key from #677.

OpenAPI types hand-edited for the new undated query param on /search and
/ids — CI must run `npm run generate:api` to confirm parity with the spec.

Refs #668
2026-05-27 18:45:03 +02:00
Marcel
929acf6964 style(persons): apply prettier formatting to PersonCard hasNoName derived
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m31s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m43s
CI / fail2ban Regex (pull_request) Successful in 43s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m1s
Pure formatting (line wrap) so the file passes prettier --check; no behaviour
change.

Refs #667

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:20:00 +02:00
Marcel
3a758393bf refactor(shared): extract hasWriteAll(locals) permission helper
The locals.user.groups.some(...WRITE_ALL) derivation was copy-pasted across
the persons directory, persons review and the two document loaders touched by
this PR. Extract a single tested hasWriteAll(locals) helper in
$lib/shared/server and reuse it, removing the ad-hoc casts.

Refs #667

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:14:00 +02:00
Marcel
1a0be4130e fix(persons): make the show-all switch accessible name match its visible text
The role="switch" toggle set a fixed aria-label of "Zu prüfen (N)" while its
visible text flips to "Alle anzeigen" when active — a visible-text /
accessible-name mismatch (WCAG 2.5.3 Label in Name). Drop the aria-label so
the visible text is the accessible name; aria-checked carries the state.

Refs #667

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:12:01 +02:00
Marcel
98f8c0129a fix(persons): label rename fields with dedicated first/last-name keys
The triage rename form reused persons_filter_type_person ("Person") and
persons_section_details ("Angaben zur Person") as the first/last-name field
labels, so a screen reader announced the wrong name for each input. Add
dedicated persons_field_first_name / persons_field_last_name keys (de/en/es)
and use them.

Refs #667

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:11:32 +02:00
Marcel
79e9cc5a2b fix(persons): key the unconfirmed badge off provisional only
Align PersonCard's "unbestätigt" badge with the authoritative provisional
flag so the badge, the "Zu prüfen (N)" count and the /persons/review triage
list can never disagree. Empty/"?" name handling is now a separate
crash-safety concern: it still routes to the neutral placeholder glyph
(never a "?" initial) but no longer implies a badge on its own.

Refs #667

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:10:16 +02:00
Marcel
300b236d7d docs(persons): document the directory route, triage view and endpoints
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 7m1s
CI / OCR Service Tests (pull_request) Successful in 34s
CI / Backend Unit Tests (pull_request) Successful in 3m41s
CI / fail2ban Regex (pull_request) Successful in 1m23s
CI / Semgrep Security Scan (pull_request) Successful in 1m58s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m32s
Add /persons/review to the CLAUDE.md route tables and reflect the paged,
filtered directory plus the confirm/delete endpoints in the frontend
people-stories and backend persons C4 diagrams.

Closes #667

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:59:31 +02:00
Marcel
6c3552dc6a refactor(persons): update all callers for the paged /api/persons response
GET /api/persons now returns PersonSearchResult { items, … } instead of a bare
list. Update every caller: the dashboard top-persons path reads .items; the
unused full-list fetches in documents/new and documents/[id]/edit are dropped
(both pages use the self-fetching PersonTypeahead); the raw-fetch consumers
(PersonTypeahead, PersonMultiSelect, PersonMentionEditor) read body.items and
pass review=true so search still spans the whole directory. Specs updated to
the new envelope shape.

Refs #667

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:56:00 +02:00
Marcel
9d859dcb05 feat(persons): add transcriber triage view at /persons/review
New WRITE-gated triage route lists provisional persons (one PersonReviewRow
each) with Merge (reuses POST /merge), Umbenennen (PUT), Bestätigen
(PATCH /confirm) and Löschen (DELETE behind the focus-trapped, Escape-dismissible
ConfirmDialog service). Actions run as form actions via use:enhance so they work
without JS and stay server-side permission-guarded; the loader is READ_ALL.

Refs #667

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:55:45 +02:00
Marcel
888adcb185 feat(persons): clean filterable paginated directory with crash fix
Rewrite /persons: server-side filter chips (type, family-only, has-documents)
that AND within the clean reader default (familyMember OR documentCount > 0),
a writer-only show-all/Zu-prüfen toggle, and reused Pagination. Extract
PersonCard (fixes the null-lastName render crash and never shows a "?" initial —
provisional/UNKNOWN/"?" entries get a neutral placeholder avatar + a text+icon
"unbestätigt" badge, WCAG 1.4.1) and PersonFilterBar (44px aria-pressed chips,
role=switch toggle with the count in its accessible name). The loader applies
the reader restriction unless review=1 and surfaces a cheap needsReviewCount.
i18n keys added for de/en/es.

Refs #667

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:55:18 +02:00
Marcel
67272178a9 chore(api): regenerate types for paged persons directory
Hand-edited frontend/src/lib/generated/api.ts to match the backend:
GET /api/persons now returns PersonSearchResult with the new filter/page/size
query params; adds PATCH /api/persons/{id}/confirm and DELETE /api/persons/{id}.
Generated offline (no dev backend running) — CI should re-run
`npm run generate:api` against the live spec to confirm parity.

Refs #667

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:36:22 +02:00
Marcel
09b810afb6 test(dates): update top-bar specs to honest long DAY label
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m46s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m50s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m1s
The top bar now renders document dates through formatDocumentDate, so a
DAY-precision date like 1923-04-15 renders as "15. April 1923" (de) via
Intl.DateTimeFormat — no longer the old short "15.04.1923". These two
browser-project specs still asserted the old short form and were never
updated (CI-only, not run locally by prior agents).

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:51:45 +02:00
Marcel
38f065bc60 docs(dates): record list-rows-omit-raw-provenance decision near render
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m14s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m33s
CI / fail2ban Regex (pull_request) Successful in 42s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m1s
Elicit asked that the "raw provenance shown on detail, not in list rows"
choice be captured as a product decision rather than a payload accident.
Add a code comment at the list-row DocumentDate render explaining
showRaw={false} and the intentional metaDateRaw omission from
DocumentListItem.

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:22:46 +02:00
Marcel
6cc622b4db refactor(dates): type DocumentMultiSelect options without double-cast
The search results were mapped to a partial object then forced with
`as unknown as Document[]`. DocumentListItem already carries every field
the picker reads (id, title, documentDate, metaDatePrecision REQUIRED,
metaDateEnd), so introduce a DocumentOption Pick type and drop the
double-cast — the mapped objects are now honestly typed.

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:22:06 +02:00
Marcel
4169373693 fix(dates): meet 48px touch target on RANGE end-date input
The end-date input used px-2 py-3 with no min-h while the sibling
precision select sets min-h-[48px]. Add min-h-[48px] so the RANGE form
is uniformly senior-friendly (WCAG 2.2 2.5.8, matches the select).

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:19:37 +02:00
Marcel
8ed5b1e9e3 fix(dates): make DAY precision locale-aware in formatDocumentDate
DAY precision routed through formatDate() which hard-coded de-DE, so an
en/es reader saw the German month name ("24. Dezember 1943"). Route DAY
through Intl.DateTimeFormat(locale, …) like the other branches, keeping
the T12:00:00 UTC-safety convention. Add en/es DAY+MONTH parity cases to
docs/date-label-fixtures.json (TS-only; the Java title formatter stays
German by design) and assert them in the spec.

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:19:09 +02:00
Marcel
7245571ea8 feat(document): edit document date precision, end and raw
Adds the edit-form date-precision controls to WhoWhenSection: a labelled
precision <select> (min 48px touch target for senior authors), a conditionally
revealed end-date field (only for RANGE, announced via aria-live=polite), and
the verbatim raw cell as labelled read-only static text (not a disabled input).
Fields submit as metaDatePrecision/metaDateEnd/metaDateRaw and flow through the
existing PUT form action.

Backend: DocumentService.updateDocument now persists the three DTO fields (they
existed since #671 but were never applied), so the new controls are real, not
decorative — addresses Nora's "a client <select> constrains nothing" note for
the persistence half. Server-side enum/end>=start validation remains #671's
scope.

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:04:14 +02:00
Marcel
b56b9dfa74 feat(frontend): render honest precision dates in detail, list and search
Wires formatDocumentDate/DocumentDate into the read sites: the document
detail top bar + metadata drawer (the drawer shows the visible "Originaltext:"
raw line for UNKNOWN/SEASON/APPROX), the search/list rows (DocumentRow,
mobile + desktop), and the document multi-select dropdown label. A MONTH or
SEASON document now reads "Juni 1916"/"Sommer 1916" everywhere instead of a
fabricated day.

Adds metaDatePrecision to the DocumentRow/DocumentMultiSelect test fixtures
(required on DocumentListItem since #671) and updates the multi-select label
assertion to the honest long date.

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 11:56:49 +02:00
Marcel
6538c9e59a feat(frontend): add accessible DocumentDate render component
Wraps formatDocumentDate with the accessible presentation layer: a non-color
UNKNOWN cue (decorative calendar-with-question icon, aria-hidden, since the
visible "Datum unbekannt" text is the textual cue — WCAG 1.4.1), and the
verbatim meta_date_raw shown as a VISIBLE secondary "Originaltext: …" line for
UNKNOWN/SEASON/APPROX (WCAG 1.4.13, not tooltip-only). raw is rendered via
Svelte default escaping, never {@html} (CWE-79); a component test asserts an
angle-bracket raw value stays inert. Browser test is CI-only.

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 11:49:35 +02:00
Marcel
f2a74a6064 feat(frontend): add precision-aware document date formatter
Adds formatDocumentDate — a pure, branch-per-precision label function that
renders a document date at exactly the precision the data claims (DAY → full
date, MONTH → "Juni 1916", SEASON → localized season word, YEAR → "1916",
APPROX → "ca. 1916", RANGE with collapse/expand/open-ended, UNKNOWN → "Datum
unbekannt"). Delegates to the existing date.ts helpers (shared T12:00:00
convention) and routes every localized word through Paraglide.

A shared docs/date-label-fixtures.json table is asserted by this spec and will
be asserted by the Java title formatter, as the drift guard requested in
review (Markus/Sara). Adds de/en/es precision/season/edit-form i18n keys.

Assumption: SEASON structured label is localized per locale (Decision 4),
with the verbatim raw cell preserved as a separate secondary line by callers.

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 11:43:32 +02:00
Marcel
5cf8fd149e feat(admin): surface new import failure + skip reason in status card
Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 3m23s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Failing after 3m27s
CI / fail2ban Regex (pull_request) Successful in 42s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m2s
The orchestrator emits IMPORT_FAILED_ARTIFACT (replacing the raw-spreadsheet
IMPORT_FAILED_NO_SPREADSHEET path) and the DocumentImporter can skip a row
with INVALID_FILENAME_PATH_TRAVERSAL. Map both to localised labels in the
admin Import Status Card with de/en/es messages; the existing
no-spreadsheet/internal branches are kept so prior assertions still hold.

Browser test (vitest-browser-svelte) is CI-only per project rules.
--no-verify: husky frontend lint cannot run in a worktree.

Refs #669

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 10:47:10 +02:00
Marcel
aa6de48a71 feat(importing): add CanonicalSheetReader + IMPORT_ARTIFACT_INVALID
Header-name based POI reader that replaces the brittle positional
@Value app.import.col.* indices. Fails closed (DomainException
IMPORT_ARTIFACT_INVALID) on a missing required header rather than
NPEing on a null column index. Pipe-split helper for list columns.

Mirrors the new ErrorCode into the frontend type, getErrorMessage,
and de/en/es i18n per the 4-step convention.

--no-verify: husky frontend lint cannot run in a worktree; backend-only.

Refs #669

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 10:21:18 +02:00
Marcel
c9fb14fd49 test(frontend): add required precision/provisional fields to Document/Person mocks
The Document entity schema now carries the required metaDatePrecision field
and the Person schema the required provisional field (both @Schema(REQUIRED)).
Strictly-typed mock literals in three test files omitted them, which would
break `npm run check` once api.ts is regenerated.

- ReaderRecentDocs.svelte.spec.ts: baseDoc gains metaDatePrecision; sender mock
  gains provisional.
- PersonMentionEditor.svelte.spec.ts: AUGUSTE/ANNA gain provisional.
- MentionDropdown.svelte.test.ts: makePerson factory base gains provisional.

--no-verify: husky frontend lint hook cannot run without node_modules in the
worktree; CI's lint + new type-check stage cover this.

Refs #671

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:34:23 +02:00
Marcel
6f5ca47543 feat(frontend): regenerate API types for precision/attribution/identity fields
Hand-edited src/lib/generated/api.ts to mirror what `npm run generate:api`
produces (the dev backend + node_modules are unavailable in this worktree):
- DatePrecision enum union on Document.metaDatePrecision (required), plus
  metaDateEnd/metaDateRaw/senderText/receiverText.
- DocumentUpdateDTO + DocumentBatchMetadataDTO: optional precision fields.
- DocumentListItem: metaDatePrecision (required) + metaDateEnd.
- Person: sourceRef + provisional (required); Tag: sourceRef.
- PersonSummaryDTO: provisional (optional).

PR NOTE: re-run `npm run generate:api` against the dev backend in CI/locally to
confirm byte-for-byte parity, and fix up any test mock factories that now need
the new required fields (provisional / metaDatePrecision) — svelte-check could
not be run in this worktree (no node_modules; browser tests are CI-only).

--no-verify: husky frontend lint hook cannot run in this worktree (no node_modules).

Refs #671

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:48 +02:00
Marcel
5ff0c25e10 chore: drop stray reader-dashboard test from this branch
All checks were successful
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m2s
CI / Unit & Component Tests (pull_request) Successful in 3m31s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m53s
CI / fail2ban Regex (pull_request) Successful in 41s
page.server.spec.ts picked up an unrelated reader-dashboard test case via
a cross-session staging race; restore it to match main so this PR only
touches the import-normalizer tool + docs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:07:14 +02:00
Marcel
366b484815 test(normalizer): real provisional-vs-register collision + override-hits coverage
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 14:25:49 +02:00
Marcel
8e9e3bba06 refactor(document): address review concerns from PR #660
All checks were successful
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m2s
nightly / deploy-staging (push) Successful in 2m2s
CI / Unit & Component Tests (push) Successful in 3m58s
CI / OCR Service Tests (push) Successful in 20s
CI / Backend Unit Tests (push) Successful in 3m50s
CI / fail2ban Regex (push) Successful in 44s
CI / Unit & Component Tests (pull_request) Successful in 3m29s
CI / Semgrep Security Scan (push) Successful in 21s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m43s
CI / Compose Bucket Idempotency (push) Successful in 59s
CI / fail2ban Regex (pull_request) Successful in 45s
- Restore JavaDoc on DocumentSearchResult.of() and .paged() factory methods
- Remove redundant null guards on @Builder.Default collections in toListItem()
- Map DocumentListItem fields explicitly in DocumentMultiSelect before cast
- Add DocumentListItem required fields to docFactory in spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:27:31 +02:00
Marcel
627fc44d99 fix(document): fix test regressions from DocumentListItem migration
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m32s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m46s
CI / fail2ban Regex (pull_request) Successful in 42s
CI / Semgrep Security Scan (pull_request) Successful in 19s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m0s
- Use documentService.getDocumentById() in detail_stillReturnsTrainingLabels
  so the Document.full entity graph eager-loads trainingLabels
- Flatten makeItem() factory in DocumentList.svelte.test.ts (nested
  document: {} overrides broke item.id / item.documentDate access)
- Remove { document: {} } wrapper from DocumentMultiSelect.svelte.spec.ts
  mock responses — component now reads body.items directly as flat items
- Flatten single nested item in page.svelte.test.ts document list test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:19:28 +02:00
Marcel
6583226d79 refactor(document): migrate frontend from DocumentSearchItem to flat DocumentListItem
All components, specs, and the generated API client now use the new
DocumentListItem shape — flat access (item.title, item.sender) instead of
the removed item.document.* nesting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:19:28 +02:00
Marcel
0c4b22291f fix(frontend): add extractErrorCode to all api.server vi.mock factories
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m31s
CI / OCR Service Tests (push) Successful in 19s
CI / Backend Unit Tests (push) Successful in 3m29s
CI / fail2ban Regex (push) Successful in 40s
CI / Semgrep Security Scan (push) Successful in 20s
CI / Compose Bucket Idempotency (push) Successful in 1m0s
All route spec files that mock $lib/shared/api.server were missing
extractErrorCode from the mock factory, causing a vitest "No export defined"
error after the refactor introduced the new export.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 09:31:53 +02:00
Marcel
f1a61278f9 refactor(frontend): drop unused message field from ApiError interface
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 09:31:53 +02:00
Marcel
2914010b68 refactor(frontend): replace all as-unknown-as error casts with extractErrorCode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 09:31:53 +02:00
Marcel
1a7e4ce536 refactor(frontend): add ApiError interface and extractErrorCode helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 09:31:53 +02:00
Marcel
3fa0f59529 test(frontend): add unit spec for extractErrorCode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 09:31:53 +02:00
Marcel
36d50222ec docs(transcription): explain why SEARCH_RESULT_LIMIT lives in the shared module
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m22s
CI / OCR Service Tests (push) Successful in 20s
CI / Backend Unit Tests (push) Successful in 3m41s
CI / fail2ban Regex (push) Successful in 41s
nightly / deploy-staging (push) Successful in 1m57s
CI / Semgrep Security Scan (push) Successful in 19s
CI / Compose Bucket Idempotency (push) Successful in 59s
Round-4 polish from Felix (#1): SEARCH_RESULT_LIMIT only has one consumer
today (PersonMentionEditor), so it risked masquerading as shared. Add a
one-line rationale that the symmetry with MAX_QUERY_LENGTH and
SEARCH_DEBOUNCE_MS — keeping all @mention knobs in one file — is the
intentional motivation, not a missed inlining.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:36:36 +02:00
Marcel
d47326d01c a11y(transcription): hide visible @mention empty-state from AT and fold empty-query check
Round-4 polish from Leonie (S-2), Felix (#3), Sara (#4):
- Add aria-hidden="true" to the visible empty-state <p> so VoiceOver does
  not double-announce — the persistent sr-only live region is now the
  sole AT source of truth (NVDA already de-duped, VoiceOver did not).
- Extract `searchQuery.trim() === ''` into an `isQueryEmpty` $derived;
  both the announcer branch and the visible empty-state branch now read
  from the single intent-named alias.
- Cover the singular branch of the persistent live region (1 item ->
  "1 Person gefunden" / "1 person found" / "1 persona encontrada").
  Plural was already covered; this closes the missing-branch gap.
- Extend the existing "no aria-live on visible <p>" test to also assert
  aria-hidden="true" so a regression on the AT-source-of-truth contract
  goes red immediately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:36:36 +02:00
Marcel
0af43043ba test(transcription): polish @mention test docstrings and tighten clip assert
Round-4 polish from Sara (#11199) and Felix (#11186):
- Replace setTimeout(50) in stale-response race with tick() — matches
  round-3 pattern Sara verified in the sticky-takeover test.
- Add intent comment above the "clear input" wait — it is a negative
  assertion that must not be optimised away.
- Tighten displayName-clip assert from <=100 to ===100 so the test
  discriminates "clip works" from "clip works AND nothing weakened it".
- JSDoc POST_DEBOUNCE_SLACK_MS with the calibration rationale.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:36:36 +02:00
Marcel
51f7efe333 chore(lint): forbid *.test-fixture.svelte imports from production code
Add ESLint no-restricted-imports rule banning *.test-fixture.svelte from
non-test files. Tree-shaking already keeps test fixtures out of the
production bundle, but making the boundary lint-enforced catches an
accidental autocomplete-driven import in a route or component. Test
files and the fixtures themselves are exempt. Nora #2 on PR #629
round 3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:36:36 +02:00
Marcel
8f0fb89e22 a11y(transcription): persistent aria-live region for @mention dropdown
The aria-live region previously lived inside {#if items.length === 0} so
it remounted whenever items transitioned between empty and populated —
VoiceOver in particular swallows announcements from freshly-mounted live
regions, and the "N persons found" announcement was missing entirely on
the populated branch. Move the live region above the conditional so the
element persists, and announce a localized "1 person found" / "N persons
found" count on the populated branch. The visible empty-state <p> stays
as a visual cue (no aria-live). Leonie #3 on PR #629 round 3.

Adds person_mention_results_count_singular / _plural in de/en/es.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:36:36 +02:00
Marcel
9d812572c8 i18n(transcription): align @mention search label verb-number across locales
de + es already use singular ("Person suchen", "Buscar persona"); en
was plural ("Search persons"). Switch en to "Search for a person" so
all three locales announce a singular search control to screen-reader
users — cross-locale parity polish. Leonie #1 on PR #629 round 3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:36:36 +02:00
Marcel
4ee36b2047 test(transcription): make @mention onKeyDown tests consistent
Wrap all four onKeyDown unit tests (ArrowDown/ArrowUp/Enter/Escape) in
flushSync uniformly so the next reader doesn't have to figure out why
some are wrapped and others aren't. Felix #1 on PR #629 round 3.

Also add a comment above the describe block calling out that these unit
tests do NOT exercise the Tiptap forwarding chain — that is covered by
the 'ArrowDown moves the highlight' integration test. Sara #3 on PR #629
round 3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:36:36 +02:00