As a reader I want imprecise and unknown dates rendered honestly (e.g. "Juni 1916", "ca. 1916", "Datum unbekannt") so a document never shows a precision the data doesn't have #666
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
The schema work (Phase 2 #671) gives
Documentfour date fields instead of one:meta_date(documentDate,LocalDate) — the sort/filter anchor: a normalized representative day. It is not a claim of exactness.meta_date_precision(DatePrecisionenum:DAY | MONTH | SEASON | YEAR | RANGE | APPROX | UNKNOWN, mirroring the normalizer's seven values verbatim) — descriptive metadata.meta_date_end(LocalDate, nullable) — the end day forRANGEprecision only.meta_date_raw(text, nullable) — the original spreadsheet cell preserved verbatim (e.g.Sommer 1916,?).The data is honest at rest. But the presentation layer still lies. Two concrete bugs remain after #671/#669 land:
MassImportService.buildTitle(MassImportService.java:476-485) doessb.append(date.format(GERMAN_DATE))— it formats the anchorLocalDateas a fulldd.MM.yyyy. AJuni 1916letter (anchor1916-06-01, precisionMONTH) gets a persisted title reading "…1. Juni 1916" — exactly the fabricated-day lie the whole milestone exists to kill. The title must never show a precision the data does not have.documentDatetoday callsformatDate/formatMCDateon the anchor and prints an exact day, ignoringmeta_date_precision/meta_date_end/meta_date_rawentirely.This issue makes the displayed date as honest as the stored one — a single precision-aware label, a single source of truth shared by titles and UI, an accessible visible raw cell, and edit controls to set precision/end/raw.
For evidence of why imprecision is the norm in this archive (7942 rows, 207 unparsed/2.8%,
?×99, long tail of30.April/Sommer 1916/um 1920), see #670's normalizer review output — this issue inherits that reality through the populated columns.Scope
In scope:
buildTitlesingle-source-of-truth fix so persisted titles stop fabricating an exact day.Out of scope (owned elsewhere — see Dependencies): the
DatePrecisionenum, the three columns, the migration/backfill, DBCHECKconstraints, the importer reading the canonical export, and addingdate_endto the normalizer export.File-level breakdown
Frontend — the formatter (the core deliverable)
frontend/src/lib/shared/utils/documentDate.ts— a single pure functionformatDocumentDate(iso, precision, end?, raw?)returning the honest label. Co-located next to the existingdate.tshelpers — delegate toformatDate/formatMCDateand reuse theT12:00:00UTC-safety convention; do not reimplementIntl.DateTimeFormat. Rules:DAY→formatDate(iso, 'long')→ "24. Dezember 1943".MONTH→ "Juni 1916" (month + year only).SEASON→ "Sommer 1916" (season word + year; see Open Decisions on localize-vs-verbatim and therawfallback).YEAR→ "1916".APPROX→ "ca. 1916" (German label for the normalizer'sAPPROX; prefix from i18n).RANGE→ "10.–11. Jan. 1917"; collapse shared month/year, expand cross-month ("30. Jan. – 2. Feb. 1917"). A null end (open-ended range) renders the start with an open-range indicator and never fabricates an end — see the AC.UNKNOWN→ "Datum unbekannt".frontend/src/lib/shared/utils/documentDate.spec.ts— one test per precision branch plus edge cases (RANGE shared-month collapse, RANGE cross-month, RANGE open-ended/null-end, UNKNOWN with nullraw, SEASON with nullrawfallback to month-derived, an angle-bracketrawrendering inert — see Security).Frontend — wiring & a11y
documentDaterendering withformatDocumentDate(...)in document detail, list/search rows, and edit views (every currentformatDate/formatMCDatecall ondocumentDate).UNKNOWN/SEASON, rendermeta_date_rawas small muted static text under the label (e.g. "Originaltext: Sommer 1916"), not behind atitle=attribute. Tooltip-only fails WCAG 1.4.13 and is invisible to touch/keyboard users — and the reader audience skews mobile.rawvia Svelte default{...}interpolation — never{@html}.meta_date_rawis untrusted verbatim spreadsheet text; default escaping neutralizes XSS (CWE-79). A formatter test asserts an angle-bracket raw value comes back as literal inert text.UNKNOWN, the textual "ca." prefix forAPPROX. a11y: date-precision cues (UNKNOWN / APPROX / RANGE) must be conveyed by text or icon, not color alone (WCAG 1.4.1).Frontend — edit form
<select>with an associated<label>(not placeholder), ≥44px (prefer 48px) touch targets for the senior author audience.RANGEis selected (progressive disclosure; announce the reveal politely). Keep the existing Germandd.mm.yyyyinput viahandleGermanDateInputfor both the anchor and the end.Backend —
buildTitlesingle source of truthbackend/src/main/java/org/raddatz/familienarchiv/importing/MassImportService.java:476-485—buildTitlemust stop appending the rawdd.MM.yyyyday. Make it precision-aware via an extractedformatTitleDate(date, precision, end, raw)helper that produces the same honest label the frontend formatter does (keepbuildTitleunder 20 lines). See Open Decisions for the chosen single-source-of-truth approach.i18n
frontend/messages/{de,en,es}.json, routed through ParaglidegetMessage(no hardcoded German in the formatter):date_precision_unknown("Datum unbekannt" / "Date unknown" / "Fecha desconocida"),date_precision_approx_prefix("ca." / "c." / "ca."), season-word keys if localized, and edit-form labels for the precision selector and end-date field.OpenAPI / types
npm run generate:apiinfrontend/soDatePrecision,documentDatePrecision,documentDateEnd,documentDateRaware present infrontend/src/lib/generated/for this issue's frontend code to consume. If they are missing, this issue is blocked on #671 — do not redeclare them here.Acceptance criteria (Gherkin)
Implementation plan
Frontend (TDD the formatter first — pure, branch-heavy, highest-value)
documentDate.spec.tswith one failing test per precision branch + edge cases before any implementation.formatDocumentDate(...)delegating todate.tshelpers; go green branch by branch. Do not add behavior beyond the seven branches mid-refactor — new edge case = new failing test first.documentDaterendering in detail / list/search / edit with the formatter.Backend
6. Red test in
MassImportServiceTest: a MONTH-precision row produces a title containing "Juni 1916", not "1. Juni 1916". Green: extractformatTitleDate(...)so title + UI share one honest label.Types & i18n
7. After #671 lands,
npm run generate:api; add de/en/es precision keys via Paraglide.Verification
8. Frontend formatter unit tests (fast Vitest layer, no browser). Backend
buildTitletest. No new Playwright journey — this is a metadata-rendering change, cheaper to verify below the E2E line.9. Backend coverage gate — the JaCoCo branch gate is currently 0.77 (77%), ratcheting toward 80% (see
pom.xmland issue #496); the precision-awarebuildTitleis branch-heavy, so plan coverage against that, not 88%, or CI blocks the merge.Open Decisions
RANGE end is now sourced. Earlier review (Sara + Elicit) found the canonical export had no
date_endcolumn, leavingmeta_date_endunpopulatable. Resolved upstream: #670 now sources the RANGE end day and exports it; #669 persists it intometa_date_end. This issue therefore renders a real end for RANGE (the "10.–11. Jan. 1917" / cross-month AC is live), and must defensively handle a null end (fall back to rendering the start day only) for any legacy row that predates the populated column.Title single source of truth (Felix).
buildTitle(Java, at import) andformatDocumentDate(TS, at render) must agree on the label. Resolved: extract one JavaformatTitleDate(date, precision, end, raw)helper that mirrors the formatter's rules exactly, so the persisted title carries the same honest label the UI shows. The title keeps containing the date (no behavioral break to title-based search/sort), but never at a finer precision than the data. Drift between "ca." vs "circa" / "–" vs "-" is avoided by keeping both implementations to one agreed rule set, tested on both sides.meta_date_rawon the lean list/search payload (Markus).documentDateRawonDocumentListItemis marked optional in #671's DTO. Open: include it on the list projection so directory/search rows render the raw cell forUNKNOWN/SEASON(one short text column, honest rendering applies to lists too), or omit it (leaner payload, but listUNKNOWNrows can only show "Datum unbekannt" andSEASONloses its word in lists). Lean toward include — this issue's honest-rendering goal covers list views, and decision 4 depends on the raw cell being available client-side.Localize the SEASON/RANGE structured label, or keep verbatim German (Leonie). A SEASON cell is German ("Sommer 1916"); an en/es reader could see "Summer 1916"/"Verano 1916" (localized, friendlier) or "Sommer 1916" (archival fidelity). Same for the RANGE month abbreviation. Lean: localize the structured part via Paraglide AND keep the verbatim raw cell visible as the secondary line, satisfying both reader accessibility and archival fidelity. This ties into decision 3 — the verbatim secondary line needs
meta_date_rawavailable in the relevant view.Out of scope
DatePrecisionenum, the three columns, the migration/backfill, and any DBCHECKconstraints — owned by #671.date_iso/date_precision/date_raw/date_endfrom the canonical export and persisting them — owned by #669.date_endto the normalizer export — owned by #670.@RequirePermission(WRITE_ALL)write path defined with the DTO fields in #671; this issue only renders and edits, it does not redefine the endpoint guards.meta_date_end, and any change to how the activity timeline or search facets consume precision — descriptive-only here; deferred to the consumer/timeline sibling issue.Dependencies
DatePrecisionenum +meta_date_precision/meta_date_end/meta_date_rawcolumns + DTO fields. Hard compile/serialize dependency — must merge before this issue's frontend types regenerate.meta_date_end) from the canonical export. Effective dependency — without it there is no real precision data to render; this issue's frontend can be built against #671's types but is only meaningfully verifiable against #669's data.date_iso/date_raw/date_precision/date_end. Upstream of #669; the RANGE end is now sourced here.marcel referenced this issue2026-05-26 21:07:04 +02:00
marcel referenced this issue2026-05-26 21:33:53 +02:00
marcel referenced this issue2026-05-26 21:34:38 +02:00
marcel referenced this issue2026-05-26 21:35:00 +02:00
marcel referenced this issue2026-05-26 21:35:20 +02:00
marcel referenced this issue2026-05-26 21:35:41 +02:00
marcel referenced this issue2026-05-26 21:36:11 +02:00
marcel referenced this issue2026-05-26 21:36:18 +02:00
Markus Keller — Senior Application Architect
Observations
DatePrecision,documentDatePrecision,documentDateEnd,documentDateRaware absent fromfrontend/src/lib/generated/api.tsand from the backendDocumententity today, so the hard compile dependency on #671 is real, not theoretical.formatTitleDate(at import) and TSformatDocumentDate(at render). Per my own module-boundary stance, duplication between modules is cheaper than coupling, and these two run in different runtimes at different lifecycle moments. I agree with duplicating rather than extracting a shared artifact. But duplicated rules drift silently.buildTitle(MassImportService.java:477-485) currently appendsGERMAN_DATE, which isDateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.GERMAN)— i.e. it already produces "1. Juni 1916", not "01.06.1916". The fabricated-day bug is real (it shows a day for MONTH precision), but the issue body's "01.06.1916" example is factually wrong for this codebase. Whoever implements should not "fix" add.MM.yyyythat isn't there.Recommendations
(precision, anchor, end, raw) → expected label) used verbatim by both the Java test and the TS spec is the cheapest drift guard. This is the "tested on both sides against one agreed rule set" the issue already gestures at — make the rule set a real artifact, not prose.formatTitleDatea pure private helper onMassImportServicetaking(LocalDate, DatePrecision, LocalDate end, String raw)and returningString. Do not introduce a new shared "DateLabel" service module — that would coupleimportingand a hypothetical formatting module for no benefit.buildTitlestays under 20 lines by delegating.documentDateRawonDocumentListItem. One nullable text column is negligible payload, and honest rendering is the whole point of the milestone — a list that can only ever say "Datum unbekannt" for UNKNOWN rows reintroduces information loss at the list layer. This also unblocks Decision 4's verbatim secondary line in lists.documentDate.tsutil is not a domain module. I'd only expect a GLOSSARY touch if a new term is coined, but "date precision" is already owned by #671's GLOSSARY entry. Confirm #671 added it; if not, that's #671's blocker, not this one.Open Decisions
Felix Brandt — Senior Fullstack Developer
Observations
documentDate.spec.tswith one failing test per precision branch before any implementation is exactly right, and the plan already sequences it that way. Good.formatDocumentDateshould delegate to the existing helpers infrontend/src/lib/shared/utils/date.ts:formatDate(iso, 'long')already yields "24. Dezember 1943" for DAY, andformatMCDate(iso, locale)yields "15. Jun. 1920" — the RANGE month-abbreviation building block. Both already apply theT12:00:00noon anchor. Do not reimplementIntl.DateTimeFormat— the issue says so, and the helpers exist.WhoWhenSection.sveltealready owns the Germandd.mm.yyyyanchor input viahandleGermanDateInput+ a hidden ISO input (name="documentDate"). The new precision<select>and conditional end-date belong here (or a small extracted child) — reusehandleGermanDateInputfor the end field exactly as the anchor does. ThedateDirty/onMountseeding pattern is subtle; mirror it for the end field, don't invent a new one.buildTitleusesGERMAN_DATE = "d. MMMM yyyy", so it already emits "1. Juni 1916", not "01.06.1916". The MONTH/SEASON bug (showing a day at all) is real; the literal string in AC scenario 2 ("not 01.06.1916") is wrong. Write the backend red test as "title contains 'Juni 1916' and does NOT contain '1. Juni'" — assert the real current output, or the test goes green for the wrong reason.Recommendations
formatDocumentDate(iso: string | null, precision: DatePrecision, end?: string | null, raw?: string | null): string.isomust be nullable — UNKNOWN rows have a null anchor (AC scenario 9: "null anchor, raw '?'"). A non-nullisotyping would force callers to lie. Guard-clause the null/UNKNOWN branches first.getLocale()at the call site and pass it) —ReadyColumn.sveltealready doesformatMCDate(doc.documentDate, getLocale()). Hardcodingde-DEinside the formatter breaks Decision 4's localized structured label.date_precision_unknown/date_precision_approx_prefix/ season words through Paraglidem.*, not string literals — keep the formatter free of hardcoded German. The function stays pure because Paraglide messages are synchronous.formatDate/formatMCDatecall ondocumentDate:DocumentRow.svelte(×2, mobile + desktop),ReadyColumn.svelte,SegmentationColumn.svelte,ThumbnailRow.svelte(×2, incl. anaria-label),DocumentMultiSelect.svelte,DocumentMetadataDrawer.svelte,DocumentTopBarTitle.svelte,PersonDocumentList.svelte,personFormat.ts, andgeschichten/[id]/+page.svelte. The issue names three views but there are ~10 call sites. Either widen the scope explicitly or the milestone's honesty goal leaks through the uncovered ones.briefwechsel/ConversationTimeline.svelteis exempt — the issue says Briefwechsel is being removed.<DocumentDate>component rather than copy-pasting the markup (one nameable visual region).Open Decisions (none)
Nora "NullX" Steiner — Application Security Engineer
Observations
meta_date_raw— untrusted verbatim spreadsheet text rendered into the DOM. The issue handles this correctly up front: render via Svelte default{...}interpolation, never{@html}, with an AC (scenario 10) and a formatter test asserting an angle-bracket value comes back inert. That is the right control for CWE-79 (stored XSS) and I fully endorse it. Svelte's default text interpolation HTML-escapes —<img src=x onerror=alert(1)>becomes literal text.meta_date_raw. Phase 4 is the render sink, so it's the right place to enforce escaping even though it didn't introduce the data.Recommendations
documentDate.spec.tsassertion proves the string is inert, but the formatter returns a label — therawcell is rendered separately as the "Originaltext: …" secondary line in Svelte markup. Add at least one component-level test (vitest-browser-svelte) that mounts the view with a maliciousrawand asserts no script/img element is created in the DOM. The pure-string test does not cover an accidental future{@html}on the raw line.{@htmlanywhere neardocumentDateRaw/meta_date_raw. The next developer "simplifying" the secondary line is the realistic regression vector; a test on one component won't catch a new{@html}introduced in a different view.<select>and end-date are editable here, but enum binding (→ clean 400 on a bogus value),end >= start, and the RANGE/end invariant ride the existing@RequirePermission(WRITE_ALL)write path defined with the DTO in #671. I want to confirm those server-side guards actually exist in #671's scope before this UI ships an editable enum — a client<select>constrains nothing. A crafted PATCH withdocumentDatePrecision: "DROP"orRANGEwithend < startmust be rejected by the backend, not just absent from the dropdown. If #671 did not add that validation, file it on #671/#669 — do not let the editable UI imply protection that isn't there.Open Decisions (none)
Sara Holt — Senior QA Engineer
Observations
documentDate.spec.ts. This is the cheapest, highest-value test layer — pure function, no browser, fast Vitest. Good call keeping it below the E2E line.formatTitleDate(date, precision, end, raw)is a 7-branch switch plus the RANGE same-month/cross-month sub-branches and null-end fallback — that's ~10 branches in one Java helper. Plan theMassImportServiceTestcases against 88% or JaCoCo blocks the merge. I'll want a test per branch, not one happy-path test that grazes coverage.Recommendations
(precision, anchor, end, raw) → expectedfixture table (Markus's drift guard) would let me assert the same expectations on both sides — I strongly support that; it turns "two suites that hopefully agree" into "two suites proving one table."end === start(degenerate range → should it render as a single day or "10.–10."? decide and test).1916-06-15renders "1916", never "Juni" or "15").title=attribute — assert it's real text), (b) malicious raw renders inert (shared with Nora's finding). Per my own rule these are integration-layer, not E2E — no new Playwright journey needed, agreed.formatTitleDateis pure Java logic, unit-testable with Mockito, no DB. The only DB-touching behavior (precision columns) is #671/#669's test surface.Open Decisions
end == start, andend == null) — render as a single day, or as a collapsed "10.–10."? The null-end legacy fallback is half-specified (AC text, no scenario). Pick the rendering, because it determines both the formatter test and the Java test expectation. Low-stakes but must be decided before the tests are written, since TDD needs the expected string.Leonie Voss — UX Designer & Accessibility Strategist
Observations
<select>(not placeholder), ≥44px touch targets (WCAG 2.5.5/2.5.8), and read-only raw as static text rather than a disabled input. I agree with every one of those, and the reasoning (touch/keyboard users, mobile-skewed reader audience, seniors 60+) is exactly the hardest-constraint-first lens I'd apply.WhoWhenSection.sveltedate field uses a proper<label for="documentDate">,inputmode="numeric",aria-describedbyfor the error, andpy-3padding. The new precision/end controls should match that pattern — it's already accessible, so extend it, don't reinvent.Recommendations
aria-live="polite"(or move focus to it). A field that silently appears is invisible to a non-sighted user who just changed the select. Pair the select with the revealed field via the DOM order so keyboard tab lands on it next.aria-label(or adjacent visually-hidden text) — an icon alone is color/shape-only to a screen reader. The "ca." prefix for APPROX is already textual, so that one's fine. For UNKNOWN, the visible "Datum unbekannt" label is the redundant text cue, so the icon can bearia-hidden="true"and decorative — that's cleaner than labelling both. Decide: label the icon OR rely on the visible text, not neither.text-xs text-ink-3(the project's muted token) — but verifytext-ink-3onbg-surfaceclears 4.5:1; muted greys often fail. If it's below AA, bump totext-ink-2. Never go below 12px even for "Originaltext: …". Prefix the raw with a translatable label ("Originaltext:" / "Original:" / "Texto original:") so it's self-describing, not a bare orphaned string.<select>: native selects are notoriously short. Enforcemin-h-[44px](prefer 48px) explicitly — don't trust the browser default, which is ~28-32px and fails 2.5.5 for the senior author audience.Open Decisions
Tobias Wendt — DevOps & Platform Engineer
Observations
formatTitleDatethat's under-tested will fail the pipeline. Sara owns the test count; I just confirm the gate is enforced in CI and will block.Recommendations
documentDatePrecision/documentDateEnd/documentDateRaw, which do not exist infrontend/src/lib/generated/api.tstoday (I checked). The build will failnpm run checkuntil #671 merges andnpm run generate:apiregenerates types. Concretely: #671 → regenerate types → then this PR. Do not open this PR against a base where #671 hasn't landed, or CI red-flags on missing types and wastes a run. The issue says this; I'm reinforcing it as a pipeline ordering constraint.npm run generate:apirequires the dev-profile backend running. Whoever regenerates types must boot the backend with--spring.profiles.active=dev(Swagger/api-docs are dev-only) or the regen produces stale output. Easy to forget; it's the most common cause of "types missing" confusion.Open Decisions (none)
Elicit — Requirements Engineer & Business Analyst
Observations
Ambiguities / contradictions I must name
GERMAN_DATE = "d. MMMM yyyy") never emits01.06.1916— it emits1. Juni 1916. The requirement (don't show a day for MONTH precision) is sound and clear; the example output is wrong. An implementer taking the AC literally would write a test against a string the code never produces. Fix the example to the real current output before this is built.end == startis undefined. A requirement without an observable outcome isn't testable. Add the two scenarios.Recommendations
end == startbehavior into explicit Given-When-Then scenarios. Every defensive behavior named in prose should have a matching AC, or it won't be verified.Open Decisions
Decision Queue — Action Required
3 decisions need your input before implementation starts. Everything else was a concrete recommendation, not a question.
UX / Localization
Behavior / Test expectations
end == start→ render a single day, or a collapsed "10.–10."? (b)end == nullon a legacy pre-#669 row → Decision 1 says "fall back to start day only" but there's no Gherkin scenario for it. Pick both renderings, then they become the formatter test + the JavaformatTitleDatetest expectations. (Raised by: Sara, Elicit.)Architecture / Testing
buildTitle's JavaformatTitleDateand the TSformatDocumentDatemust produce the same label, forever. Either commit a single shared(precision, anchor, end, raw) → expected labelfixture table that both test suites assert against (one extra fixture file; catches "ca." vs "circa" / en-dash vs hyphen drift); or trust two independently-written test sets to stay in sync (no extra file; silent divergence is possible because each side only tests itself). Lean: shared fixture table. (Raised by: Markus, reinforced by Sara.)Note for the implementer (not a decision — a factual fix): AC scenario 2's "not 01.06.1916" is wrong.
buildTitleusesGERMAN_DATE = "d. MMMM yyyy", so it already emits "1. Juni 1916". The MONTH-precision bug (showing any day) is real; the literal example string is not. Write the red test against the real current output ("contains 'Juni 1916', does NOT contain '1. Juni'"). (Flagged by: Markus, Felix, Elicit.)As an archivist I want imprecise and unknown document dates modelled honestly so that letters dated "Sommer 1916", "15.II.60", or "10./11.I.1917" keep their real precision instead of being fabricated or lostto As a reader I want imprecise and unknown dates rendered honestly (e.g. "Juni 1916", "ca. 1916", "Datum unbekannt") so a document never shows a precision the data doesn't have