feat(dates): honest precision-aware date rendering (Phase 4, #666) #677

Merged
marcel merged 16 commits from feature/666-date-rendering into docs/import-migration 2026-05-27 13:19:01 +02:00

16 Commits

Author SHA1 Message Date
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
4bc96c3772 ci(dates): widen {@html} raw-date guard to cover the raw prop
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m12s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m45s
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
DocumentDate.svelte passes the untrusted raw value via a prop named `raw`,
but the guard only matched metaDateRaw/documentDateRaw/rawDate — so a future
{@html raw} would slip past. Add `\braw\b` to the token list and a self-test
asserting the guard catches {@html raw}. Code is currently safe ({raw}); this
closes the defense-in-depth gap in the guard itself.

Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:37:42 +02:00
Marcel
f99673321c test(dates): pin edit-form precision field binding to DocumentUpdateDTO
@WebMvcTest multipart PUT asserting metaDatePrecision / metaDateEnd /
metaDateRaw form field names bind to the DTO. A rename on either side
silently drops the precision edit; the captured DTO catches it.

Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:36:51 +02:00
Marcel
728078f1e5 fix(dates): preserve stored date precision when edit omits it
updateDocument unconditionally set metaDatePrecision/End/Raw from the DTO,
so saving an unrelated edit (a multipart PUT where the form omits the
precision controls) clobbered the stored precision with null — fabricating
a precision the user never chose. Apply each field only when the DTO carries
it, mirroring the existing metadataComplete/scriptType guards.

Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:34:58 +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
b1b8fa4bed docs: note honest date formatter, title formatter and drift fixture
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m17s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m47s
CI / fail2ban Regex (pull_request) Successful in 43s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m1s
Documents DocumentTitleFormatter in the document-management C4 diagram and adds
an "honest precision display" row to the CONTRIBUTING date-handling table,
pointing at formatDocumentDate / <DocumentDate>, the shared
docs/date-label-fixtures.json drift guard, and the {@html} escaping rule.

Closes #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:08:00 +02:00
Marcel
2bd5c82826 ci: guard against rendering meta_date_raw via {@html}
Adds a grep guard (with self-test) that fails the build if any {@html ...}
expression references metaDateRaw/documentDateRaw/rawDate. meta_date_raw is
untrusted verbatim spreadsheet text and must render via Svelte default
escaping (CWE-79). Addresses Nora's regression-guard request from #666 — a
single component test cannot catch a future {@html} introduced elsewhere.

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:05:17 +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
c816934391 feat(importing): build honest precision-aware document import titles
Wires DocumentTitleFormatter into DocumentImporter.buildDocument: the title
now reads "{index} – {honest date label} – {location}", so a MONTH-precision
letter's title says "Juni 1916" instead of a fabricated "1. Juni 1916", and an
UNKNOWN-date row keeps a bare index title. buildTitle stays under 20 lines by
delegating to the shared formatter (single source of truth with the UI label).

Restores the date+location title behavior that the old MassImportService had
(it appended a full GERMAN_DATE day) but now at the honest precision.

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 11:47:51 +02:00
Marcel
1caae38946 feat(importing): add precision-aware DocumentTitleFormatter
Adds the Java half of the honest date label — formatTitleDate(date,
precision, end, raw) — mirroring the frontend formatDocumentDate rules so an
import title never shows a precision the data lacks (MONTH → "Juni 1916", not
a fabricated day). Both implementations are pinned to the shared
docs/date-label-fixtures.json table, which this test asserts case-by-case, so
they cannot drift. Java's de CLDR renders the same "Jan."/"Dez." abbreviations
and en-dash the TS side produces.

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 11:45:57 +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