As a reader I want undated and imprecisely-dated letters to be honestly labelled in browse views so I always understand a document's date position #668
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
A meaningful fraction of the archive has no usable letter date. The import normalizer reported 575 unparsed dates (7.9% of rows) and 99 literal
?values — roughly one in twelve documents either has no date or only an imprecise one.The date-ordered browse surfaces silently mishandle these today:
Document.documentDateis a single nullableLocalDate(backend/.../document/Document.java:91-92, columnmeta_date). There is no model for imprecision — an undated letter is justnull.d.meta_date DESC NULLS LAST(DocumentRepository.java:123) — correct, but only for that one query.resolveSort(...)→Sort.by(direction, "documentDate")with no explicit null handling (DocumentService.java:778-789). On PostgresDESChappens to put NULLs last, butASCputs undated documents FIRST — flipping sort direction surfaces the undated pile at the top with no explanation.resolveSortat all. SENDER, RECEIVER, and filtered-RELEVANCE load the full match set viadocumentRepository.findAll(spec)and sort in-memory (sortBySender/sortByFirstReceiver/ rank comparator,DocumentService.java:667-687). AresolveSort.nullsLast()change does NOT reach them. "Undated last regardless of sort mode" must therefore be enforced wherever each sort actually executes — in SQL for the DATE/TITLE/UPLOAD/UPDATED fast path, and in the in-memory comparators for SENDER/RECEIVER/RELEVANCE.groupByYearbuckets null-dated items underdocs_group_undated()(DocumentList.svelte:37), and the i18n key exists in all three locales. But there is no per-row badge, no "undated only" filter, and the bucket only exists when grouping by year. Undated rows in every other mode render a bare em-dash (DocumentRow.svelte:167mobile,:181desktop) — a glyph-only cue with no text, which a screen reader announces as nothing.In one sentence: date-ordered timelines and search silently mishandle undated documents — they sink to the bottom unexplained, or surface at the top on ASC sort — and the reader can never tell "we don't know the date" from "the field is broken". The reader must always understand WHY a document has no date position.
Scope — which surfaces
/documents, loaded byfrontend/src/routes/documents/+page.server.ts, rendered byDocumentList.svelte/DocumentRow.svelte. This is the searchable, date-ordered list. (The dashboard homefrontend/src/routes/+page.server.tsis recent/incomplete tiles, not a date-ordered timeline — out of scope.)/aktivitaeten. Caveat from review:ChronikRow.svelterenders the activity event timestamp (auditoccurredAt), not the letterdocumentDate, and has no document-date surface today (actor + verb + document title + relative activity time). So there is nothing to badge. The work here is a negative guarantee only: ensure no fabricated letter date is introduced into a Chronik row. Do NOT invent a date chip that does not exist.Dependency
nulland full-format for a present date.documentDatecolumn.db-orm.pumldoc gate would apply — keep them split.User journey
/documents, sorted by date (default). Year-grouped letters render; at the very bottom a clearly-labelled "Datum unbekannt" group collects every undated letter, with a count.from/to). Undated documents are excluded (no date to fall in range); if nothing matches, the empty-state copy says explicitly that undated documents are not part of a date range.Backend breakdown
DocumentService.java— NULLS-LAST on every sort path (the core fix):resolveSort, lines 778-789): useSort.by(new Sort.Order(direction, "documentDate").nullsLast())so undated docs order last for bothASCandDESC. This closes the ASC bug.sortBySender/sortByFirstReceiver/ filtered-RELEVANCE comparator, lines 667-687): these never order bydocumentDate, so they do not surface undated-at-top — but any tiebreaker that touchesdocumentDatemust treatnullas last. Confirm and lock with a test; do not assumeresolveSortcovers them (it does not).DocumentSpecifications.java— add a staticundatedOnly(boolean)factory returningnullwhen false andcb.isNull(root.get("documentDate"))when true. Compose it throughbuildSearchSpec(DocumentService.java:503-518) likehasStatus— this is the single source of truth shared bysearchDocumentsandfindIdsForFilter, so the filter also applies to the bulk-edit "select all" path (intended: a triager filtering to undated wants bulk-edit on the result). Confirm the existingisBetweenrange predicate (:41-51,cb.between/>=/<=) naturally excludes NULL — it does on Postgres; this issue makes that intentional and tested (real-Postgres, not H2).DocumentController.java—/search(:365): add@RequestParam(required = false) Boolean undated, threaded into the service/spec. Read-onlyGET— no@RequirePermissionwrite guard (confirmed correct by security review; the write endpoints in this controller carry the guard, the read GET does not). After the param change, runnpm run generate:api.Frontend breakdown
frontend/src/routes/documents/+page.server.ts— parseundatedstrictly (url.searchParams.get('undated') === 'true', mirroring thetagOpclamp), forward asundated: undated || undefined, return it in page data so the control reflects URL state. Page-reset-on-filter-change is already implicit (documents/+page.svelte:88dropspageon any filter change) — do not reimplement it.DocumentRow.svelte— replace the bare em-dash at:167(mobile) and:181(desktop) with the "Datum unbekannt" badge. Define it once (route both blocks throughformatDocumentDateor a single<DateCell>), so the cue cannot drift. Badge styling: a neutral chip (not red/amber — undated is an absence, not an error), matching the existing metadata-chip pattern (rounded border border-line px-1.5 py-0.5 font-sans text-[10px] tracking-widest text-ink-3 uppercase, the pattern at:135-137), paired with the existingtext-ink-3 italic"unbekannt" treatment used for unknown sender/receiver (:196).text-[10px]is the hard minimum — never below — and verifytext-ink-3onbg-surfacehits 4.5:1 in both light and dark themes (bump totext-ink-2if it fails AA).DocumentList.svelte— keep the existing yeardocs_group_undatedbucket. For sender/receiver/relevance grouping, do NOT force a synthetic undated sub-group — the per-row badge fromDocumentRowis the chosen pattern, keeping each letter under its actual person group (review consensus). Add a friendly empty-state branch (:74-85currently only handles the API-error state): when afrom/tofilter yields nothing, show thedocs_range_excludes_undatedcopy.SearchFilterBar.svelte— add the "Nur undatierte" toggle wired to theundatedURL param. Reuse the advanced-row tag AND/ORaria-pressedpill pattern (:225-250). Wrap the control in a real<label for>withmin-h-[44px]touch target (WCAG 2.5.5 / 2.2, senior audience) — reference the bulk-select label atDocumentRow.svelte:61-72. Label the state, not the color.frontend/src/lib/shared/utils/date.ts— addformatDocumentDate(doc): returnsm.docs_badge_undated()fornull, delegates toformatDatefor a present full date, and (once #666 lands) renders the precision branches frommeta_date_precision/meta_date_end/meta_date_raw. KeepformatDate/formatMCDatefor full-precision dates. This function is the dependency boundary — stub the precision branches as TODOs referencing #666 until then.ChronikRow.svelte) — negative guarantee only. Confirm no fabricated letter date is rendered. Do not add a date chip (none exists today; the row shows the activity timestamp).i18n
docs_group_undatedalready exists in all three locales (reuse for the bucket header). Add tofrontend/messages/{de,en,es}.json:docs_filter_undated_only→ "Nur undatierte" / "Undated only" / "Solo sin fecha"docs_badge_undated→ "Datum unbekannt" / "Date unknown" / "Fecha desconocida"docs_date_circa_prefix→ "ca. " / "c. " / "h. " (circa prefix for the precision formatter)docs_range_excludes_undated→ static localized empty-state note that a date range excludes undated documents (must be a localized constant, never a reflected backend string)Acceptance criteria
Tests
DocumentServiceTest, Mockito): parameterizeresolveSortover{ASC, DESC}asserting the producedSortcarriesNullHandling.NULLS_LASTondocumentDate. Today ASC fails — that is the red. (~2 tests.)@DataJpaTest+ Testcontainerspostgres:16-alpine): real Postgres is required (these verify Postgres null-ordering andBETWEENnull-exclusion — H2 gives false confidence). Assert (a) DATE ASC returns dated-first, undated-last; (b)undated=truereturns exactly the null-dated rows; (c) afrom/torange returns zero undated. (~3 tests.) Reuse theDocument.builder().documentDate(null)fixture pattern (DocumentServiceTest.java:2380).@WebMvcTest):GET /api/documents/search?undated=truereachable by an authenticated READ_ALL user; an unauthenticated request gets 401 — guards against a future refactor accidentallypermitAll()-ing or write-guarding the read path.DocumentList.svelte.spec.ts, vitest-browser — file exists): an undated row carries thedocs_badge_undatedbadge under sender AND receiver grouping; a group with both dated and undated rows renders correctly; no synthetic undated sub-group appears in person-grouped modes.date.spec.ts— file exists):formatDocumentDate(null)→ "Datum unbekannt"; present date → full format. Add.skipplaceholders for the precision cases ("Juni 1916" etc.) — each must carry the #666 reference, never a bare skip.+page.server.tsspec): importload, mockfetch, assertundated=trueis forwarded as a query param and that page resets (nopagecarried)./documents. Do not add an E2E per sort mode.Open Decisions
DocumentRowchange). A forced "Datum unbekannt" sub-group would pull letters out of their person's group — rejected.docs_range_excludes_undated) explain the contradiction, rather than disabling the date-range inputs. Both filters are independently shareable URL params; an empty result with an honest explanation is the rule.documentDatenull, pagination order is undefined unless a tiebreaker is chosen (title vs. upload date vs. insertion order). This is a triage-workflow preference the code cannot decide. Default if unanswered: upload date ascending (stable, reflects ingestion order for triage). (Raised by: Elicit.)Out of scope
meta_date_precision/meta_date_end/meta_date_rawcolumns, migration, entity fields, OpenAPI regen) — produced by Phase 2 #671 and surfaced via the Phase 4 #666 formatter. This issue only consumes those fields.frontend/src/routes/+page.server.ts) — not a date-ordered timeline.ChronikRowshows the activity timestamp by design; this issue only guarantees no fabricated letter date appears, it does not add a date chip.marcel referenced this issue2026-05-26 21:35:53 +02:00
Markus Keller — Senior Application Architect
Observations
documentDateis the existing nullablemeta_datecolumn and nothing here touches it. Good. Nodb-orm.puml/db-relationships.pumlgate is triggered.resolveSort(sortBySender/sortByFirstReceiver/ the in-memory rank comparator atDocumentService.java:667-687). A junior reading "add.nullsLast()" would assume one change fixes everything. The spec naming each execution site is exactly the right framing. I confirmedbuildSearchSpec(DocumentService.java:503-518) is the single Specification chain shared bysearchDocumentsandfindIdsForFilter— composingundatedOnly(boolean)there is the correct seam and keeps the bulk-edit "select all" path consistent for free.DocumentSpecificationsis a static-factory utility class with no injected state — addingundatedOnly(boolean)returningnull/cb.isNull(...)matches the existinghasStatuspattern exactly (:53-55). No boundary crossed; this stays inside thedocumentdomain.formatDocumentDate(doc)stub-as-seam is a clean dependency inversion. The boundary between this issue and #666/#671 is a single function. That is the right place to draw the line — Phase 4's formatter slots in without touching call sites.Recommendations
meta_date_precisionetc. appear in a migration, the DB-diagram doc gate applies and this PR's scope balloons. Two PRs, two review surfaces.undatedOnlyshould be a pure boolean-driven Specification with no clever short-circuit beyondreturn false ? null : cb.isNull(...). Push the null-ordering correctness to where Postgres enforces it (theSort.Order.nullsLast()and theIS NULLpredicate) rather than re-deriving "undated" logic in Java in three places — one definition, three call sites referencing it.Open Decisions
Felix Brandt — Senior Fullstack Developer
Observations
resolveSortparameterized over{ASC, DESC}assertingNullHandling.NULLS_LAST— ASC is the red. I confirmed the bug atDocumentService.java:778-789:Sort.by(direction, "documentDate")carries no null handling, so ASC surfaces undated docs first on Postgres. That test fails today; good red.DocumentRow.svelte. The bare em-dash is at two spots::167(mobile,{doc.documentDate ? formatDate(doc.documentDate) : '—'}) and:181(desktop, identical expression). The spec's "route both blocks throughformatDocumentDateor a single<DateCell>" is the right call — these two expressions are a duplication waiting to drift. I'd extract a single<DateCell date={doc.documentDate} />so the badge cannot diverge between breakpoints.formatDocumentDate(doc)is the dependency seam. Stubbing the precision branches as TODOs referencing #666 is correct — but the function signature should take the document (or the precision fields), not just the ISO string, so the #666 follow-up doesn't change the signature and break call sites. The spec saysformatDocumentDate(doc)— good, keep it doc-shaped from day one.+page.server.tsalready clampstagOpstrictly (=== 'OR', line 47). Theundatedparam must mirror that:url.searchParams.get('undated') === 'true'. Forward asundated: undated || undefinedso the absent case drops out of the query string. I verified page-reset-on-filter-change is already handled — do not reimplement it.Recommendations
sortBySender/sortByFirstReceiver): they sort by person name, not date, so they don't have the undated-at-top bug. But the spec is right to demand a locking test — writeshould_keep_undated_documents_last_when_sorting_by_senderasserting a null-dated doc isn't pulled out of its sender group. Don't assume; prove it green.formatDate/formatMCDateuntouched.formatDocumentDatewraps them; it does not replace them. The unit testformatDocumentDate(null) → "Datum unbekannt"plus a present-date passthrough is the whole green for the independent half. The precision cases get.skipwith a// #666reference — never a bare skip.<DateCell>badge: define the chip class once as a$derivedor a const, not inline-duplicated. Reuse the metadata-chip pattern atDocumentRow.svelte:135-137verbatim so it reads as "same family of chip."docs_badge_undatedreads as a noun-phrase badge label — good. Keepdocs_filter_undated_onlyas the toggle label. Both reveal intent.Open Decisions
Nora Steiner ("NullX") — Application Security Engineer
Observations
/api/documents/searchendpoint is a read-only GET and correctly carries no@RequirePermissionwrite guard. I verifiedDocumentController.java:365:search(...)has no permission annotation, while the sibling endpoints (/incomplete,/incomplete/next) carry@RequirePermission(Permission.WRITE_ALL). Adding@RequestParam(required = false) Boolean undateddoes not change the method's safety class — it stays a safe, idempotent read. The spec's "no write guard" note is correct; do not let a reviewer "helpfully" addWRITE_ALLhere, that would break read access for READ_ALL users.undatedparam is a strict server-side boolean threaded into a JPA Criteriacb.isNull(...)predicate — no string concatenation, no injection surface. The whole existing spec chain uses parameterized Criteria (cb.equal,cb.between, named joins). Clean.docs_range_excludes_undatedis specified as a localized constant, never a reflected backend string. This is the right instinct — an empty-state message must never echo a server-supplied value. I'm glad the spec calls this out explicitly; reflecting backend text into the empty state would be a stored/reflected-XSS vector via Paraglide-bypassed content.Recommendations
@WebMvcTestauthz test as written — but make it two assertions, not one:GET /api/documents/search?undated=truewith an authenticated READ_ALL user → 200 (reachable).This guards against a future refactor accidentally
permitAll()-ing the read path or, conversely, write-guarding it. The spec lists both; treat them as non-negotiable regression fixtures.undatedparam defensively:=== 'true'exact-match on the frontend and a nullableBooleanon the backend (Spring will reject non-boolean garbage with 400). Do not accept"1","yes", etc. — narrow the accepted truthy surface to exactlytrue.ErrorCodeis introduced (the empty state is a frontend i18n constant, not a backend error) — so noErrorCode.java/errors.ts/getErrorMessage()cascade. Confirm that stays true; if anyone adds a backend 4xx for "undated+range conflict," that's wrong — the spec correctly models it as an empty result, not an error.Open Decisions
Sara Holt — Senior QA Engineer
Observations
resolveSort, Testcontainerspostgres:16-alpinefor the null-ordering +BETWEENexclusion,@WebMvcTestfor authz, vitest-browser for the badge, plain TS forformatDocumentDateand the load function. No E2E-per-permutation. This is exactly the right shape.BETWEEN-excludes-null assertions is correct and load-bearing. H2 and Postgres disagree onNULLS FIRST/LASTdefaults and on whetherBETWEENexcludes NULL — an H2 suite here would give false green. I confirmedisBetween(DocumentSpecifications.java:41-51) usescb.between/>=/<=, all of which evaluate to UNKNOWN (excluded) for a NULLdocumentDateon Postgres. The test makes that intentional and pinned.Document.builder().documentDate(null)fixture pattern is reused (DocumentServiceTest.java:2380cited). Good — no new builder.Recommendations
backend/CLAUDE.md:159. The two new branches — thenullsLastdirection branch inresolveSortand theundatedOnly(true/false)branch — each need explicit coverage or JaCoCo fails the PR. Parameterize:resolveSortover{ASC, DESC}(2 cases),undatedOnlyover{true, false}(thefalse → nullbranch is easy to forget and will cost you the gate).undated=trueAND afrom/torange → zero rows. That's the one Postgres can prove that H2 might fudge. Add it as its own one-assertion test (undated_with_date_range_returns_empty), not folded into another.DocumentList.svelte.spec.tsexists): assert the badge appears under both sender and receiver grouping, and explicitly assert no synthetic undated sub-group node exists in person-grouped modes (getByText(docs_group_undated)should NOT match inside a sender card). Test the absence, not just the presence..skipplaceholders for precision cases indate.spec.tsmust each carry the#666reference inline. A bare.skipis a hidden regression risk — I want a ticket reference on every disabled assertion so it's not orphaned.undated=trueis forwarded as a query param and that toggling it does not carrypageforward (page reset). Mock onlyfetch.Open Decisions
Leonie Voss — Senior UX Designer & Accessibility Strategist
Observations
DocumentRow.svelte:167and:181both render'—'for a null date. A lone em-dash is announced by screen readers as nothing (or "dash") — the reader literally cannot tell "we don't know the date" from "the field is broken." Replacing it with a text badge ("Datum unbekannt") is a genuine SR improvement, not cosmetic.rounded border border-line px-1.5 py-0.5 ... text-[10px] ... text-ink-3 uppercase, the pattern at:135-137) keeps it visually in the "metadata" family. I confirmed that chip class exists. Pairing with thetext-ink-3 italictreatment used for unknown sender/receiver (:196,docs_list_unknown) gives a consistent "we don't know this" visual language across date AND person — that's good IA consistency.aria-pressedpill pattern, wrapped in a real<label for>withmin-h-[44px], is correct. I verified the 44px touch-target precedent at the bulk-select label (DocumentRow.svelte:61-72). Senior audience (60+) on phones: 44px is the floor, label-the-state-not-the-color is the rule.Recommendations
text-ink-3onbg-surfacein BOTH themes. This is the one thing the spec flags and I'm escalating it.text-[10px]uppercase tracking-widest is small text — it must hit 4.5:1.text-ink-3is the lightest ink token; onbg-surfacein dark mode it is the most likely to fail AA. Run the axe check in light AND dark (don't test light-only). If it fails, bump totext-ink-2— do not drop belowtext-[10px], that's the hard minimum for the senior audience.aria-hiddenspan. The text content IS the accessibility here.docs_range_excludes_undated) when a date range matches nothing: write it as a full sentence that explains the cause, not "Keine Ergebnisse." Seniors need "Datumsfilter schließen undatierte Dokumente aus" so they understand why the list is empty and aren't left thinking the app broke. The spec's intent is right; make the copy explanatory.Open Decisions
text-ink-3badge — that's a gate, not a decision, so I'm logging it as a hard requirement rather than an open question.Tobias Wendt — DevOps & Platform Engineer
Observations
meta_datecolumn and the existing search endpoint. From an ops standpoint this is the cheapest possible feature — nothing for me to size or operate.postgres:16-alpineintegration tests. That image is already in use across the suite, so no new pull, no new cache key. CI cost is ~3 extra integration tests + a handful of unit/component tests — well under the 2-minute integration-layer budget.npm run generate:apiafter theundatedparam change is a build-step the spec correctly calls out. This is the usual gotcha: if codegen isn't re-run, the frontendopenapi-fetchclient won't know the param and TS will fail or silently drop it. Make sure that regen is committed in the same PR so CI's frontend type-check passes.Recommendations
@DataJpaTest+ the sharedPostgresContainerConfig).generate:apiin CI ordering, so a forgotten regen fails loudly rather than shipping a stale client. This is the single most common "works locally, breaks CI" failure mode for backend-param additions.Open Decisions
generate:api+ type-check ordering, which is a process reminder, not a decision.Elicit — Requirements Engineer & Business Analyst (Brownfield)
Observations
ChronikRow.svelterendersrelativeTime(item.happenedAt)— the activity timestamp, notdocumentDate— so the negative guarantee matches reality.VALID_SORTSin+page.server.tsis['DATE','TITLE','SENDER','RECEIVER','UPLOAD_DATE','RELEVANCE']— it has noUPDATED_AT, yet the backendresolveSortand the issue's backend breakdown mentionUPDATEDas a sort path needing NULLS-LAST coverage. The backend can be reached withUPDATED_ATdirectly even though the UI doesn't expose it. Confirm whether the AC "this holds for ... the DATE fast path alike" is meant to coverUPDATED_AT/UPLOAD_DATEtoo (those sort bycreatedAt/updatedAt, which are non-null, so NULLS-LAST is moot for them) — if so, say so explicitly so the test author doesn't chase a non-bug.Recommendations
Open Decisions
COUNTquery). (Raised by: Elicit.)Decision Queue — Action Required
2 decisions need your input before implementation starts. Everything else is already resolved or specified — the personas made concrete recommendations on all other points.
Product / Triage Workflow
Secondary sort order for undated-only results. When the "Nur undatierte" filter is active, every row has a null
documentDate, so pagination order is undefined unless a tiebreaker is fixed. This is a triage-workflow preference the code cannot decide (title A→Z vs. upload date vs. insertion order). It also carries a test-determinism cost: without a stable total order, the integration test asserting "page 1 contains rows X, Y, Z" will be flaky across runs. Issue's stated default if unanswered: upload date ascending (stable, reflects ingestion order for triage). Confirm or override. (Raised by: Elicit; reinforced as a flaky-test risk by Sara.)Undated group count semantics — page-local vs. total-across-pages. Scenario 1 says the "Datum unbekannt" group "shows how many undated documents it contains." #315 already descoped cross-page per-year totals. Page-local is one extra-query cheaper; a grand total needs a dedicated
COUNT. Issue's stated default if unanswered: page-local count (consistent with #315). Confirm or override. (Raised by: Elicit.)Note (not a decision — a one-line clarification request)
VALID_SORTShas noUPDATED_AT, while the backendresolveSorthandlesUPDATED/UPLOAD_DATE(which sort by non-nullcreatedAt/updatedAt, so NULLS-LAST is moot for them). Worth one sentence in the issue confirming the NULLS-LAST scope isDATE/RELEVANCEonly, so the test author doesn't chase a non-bug. This is a doc tidy-up, not a blocker.Implemented on
feature/668-undated-documents(committed locally, not pushed)8 atomic commits, red/green TDD throughout. Branch base:
docs/import-migration.NULLS-LAST across every sort path
resolveSortnow returnsSort.by(new Sort.Order(dir, "documentDate").nullsLast(), Sort.Order.asc("createdAt"))— closes the ASC bug (NULLs were first) and gives a deterministic total order when every row is null-dated (the upload-date-ascending secondary sort, per the open-decision default). Parameterized unit tests assertNULLS_LASTfor ASC and DESC.@DataJpaTest(postgres:16-alpine) pins: DATE ASC dated-first/undated-last,undatedOnly(true)returns exactly null rows,BETWEENexcludes nulls, and undated=true + range → empty (the collision rule).Undated badge + bucket
DocumentRowno longer renders a bare em-dash. Both breakpoints route through the singleDocumentDatecomponent (cue can't drift); its unknown state is a neutral metadata chip "Datum unbekannt" (text-ink-3, non-color calendar glyph, never red/amber). The year-groupingdocs_group_undatedbucket is kept; person-grouped modes keep undated letters under their sender/receiver (no synthetic sub-group).Undated-only filter + range collision
DocumentSpecifications.undatedOnly(boolean)composed throughbuildSearchSpec(shared by search and/idsbulk-edit).GET /search+/idstake@RequestParam(required=false) Boolean undated(read GET stays unguarded; WebMvc authz test pins 200 authenticated / 401 unauthenticated). The loader parses?undated === 'true', forwardsundated || undefined, returns it in page data; theSearchFilterBar"Nur undatierte" toggle (44px,aria-pressed) is a shareable URL param. When afrom/torange yields nothing, the empty state showsdocs_range_excludes_undated.Precision rendering
Note: this base branch already has #671's precision fields on the DTO (
metaDatePrecision/metaDateEnd) and #677'sformatDocumentDate, so the "precision half" was not blocked — rows render honest precision ("Juni 1916", "ca. 1916") viaDocumentDate, not stubs.Chronik
Negative assertion only —
ChronikRowshows the activity timestamp;ActivityFeedItemDTOhas no date surface. A regression test pins that no undated badge / fabricated letter date appears.i18n
Added
docs_filter_undated_only,docs_range_excludes_undated(de/en/es). Reused existingdate_precision_unknown("Datum unbekannt") anddate_precision_approx_prefix("ca.") from #677.Tests
npm run lintclean (prettier + eslint). New browser/component specs (DocumentRow badge, DocumentList badge/empty-state, SearchFilterBar toggle, ChronikRow negative) written but CI-only per project policy.OpenAPI
undatedquery param hand-edited intoapi.tsfor/searchand/ids— CI must runnpm run generate:apito confirm parity.Decisions applied (defaults, unanswered)
Not pushed; owner to push + open PR.