Commit Graph

2021 Commits

Author SHA1 Message Date
Marcel
0862d43ba3 fix(timeline): mark the event form dirty on date/precision/picker changes
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 4m53s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 5m46s
CI / fail2ban Regex (pull_request) Failing after 46s
CI / Semgrep Security Scan (pull_request) Successful in 25s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
SDD Gate / RTM Check (pull_request) Successful in 17s
SDD Gate / Contract Validate (pull_request) Successful in 24s
SDD Gate / Constitution Impact (pull_request) Successful in 19s
The beforeNavigate unsaved-changes guard only fired for title/type/description
(their oninput/onchange hooks set `dirty`). Editing only the date, precision,
end-date, or the linked persons/documents left `dirty` false, so a curator could
navigate away and silently lose those edits — defeating the guard for the senior
author audience. Add an $effect that watches those values and marks dirty on any
change after the initial prop snapshot (first run only arms the watcher).

Addresses PR #832 review (round-2 clean-agent concern).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:44:45 +02:00
Marcel
9cb856b376 test(e2e): update document-title-autosync precision selector to new id
DatePrecisionField derives the precision select's id from dateInputName, so the
document form's id moved from #metaDatePrecision to #documentDatePrecision (the
name attribute is unchanged). This maintained E2E still queried the old id and
would fail when run. Not CI-gated, but a real silent breakage.

Addresses PR #832 review (round-2 clean-agent out-of-diff finding).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:44:25 +02:00
Marcel
d330510777 test(document): update WhoWhenSection.test ids after DatePrecisionField extraction
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m58s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 5m32s
CI / fail2ban Regex (pull_request) Successful in 55s
CI / Semgrep Security Scan (pull_request) Successful in 32s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m8s
SDD Gate / RTM Check (pull_request) Successful in 16s
SDD Gate / Contract Validate (pull_request) Successful in 28s
SDD Gate / Constitution Impact (pull_request) Successful in 21s
The DatePrecisionField extraction derives element ids from dateInputName, so the
document form's precision/end-date ids changed (metaDatePrecision →
documentDatePrecision, metaDateEnd → documentDateEnd, date-error →
documentDate-error, end-date-error → documentDate-end-error). The name attributes
are unchanged, so form submission is unaffected — but the pre-existing
WhoWhenSection.svelte.test.ts (a separate file from the .spec.ts) still queried
the old ids and was failing 5 assertions in CI's client project. It wasn't in
the PR diff, so the multi-persona review missed it. Re-point the selectors.

Addresses PR #832 review (round-1 clean-agent blocker).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:39:38 +02:00
Marcel
719274ef88 docs(permissions): note requireWriteAll can replace the inline guard elsewhere
Some checks failed
CI / Semgrep Security Scan (pull_request) Successful in 25s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
SDD Gate / RTM Check (pull_request) Successful in 14s
SDD Gate / Contract Validate (pull_request) Successful in 23s
SDD Gate / Constitution Impact (pull_request) Successful in 21s
CI / Unit & Component Tests (pull_request) Failing after 3m19s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 4m49s
CI / fail2ban Regex (pull_request) Successful in 47s
Architect/Developer review suggestion: flag that other WRITE_ALL-gated author
loads (e.g. documents/[id]/edit) still inline the throw-403 guard and can adopt
requireWriteAll so it doesn't diverge. Comment-only.

Addresses PR #832 review (Architect suggestion).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:33:38 +02:00
Marcel
d48a89ba5c test(timeline): assert submit-disabled transition and ≥44px chip targets
Tighten the review-flagged test gaps (no production change):
- submitting state: the old test only asserted the button was initially
  enabled (a tautology). Now stub a never-resolving fetch, click Speichern, and
  assert the button gains `disabled` — exercising the double-submit guard
  (Decision 8).
- the two redirect-throwing save tests now use `await expect(...).rejects` so a
  future missing redirect fails loudly instead of being swallowed by try/catch.
- the YEAR end-date-hide assertion uses getByLabelText('Enddatum') not-present,
  symmetric with the RANGE reveal, instead of a raw #eventDateEnd querySelector.
- PersonMultiSelect + DocumentMultiSelect: assert the chip remove button carries
  the min-h-[44px]/min-w-[44px] target the rtm cites for REQ-017.

Addresses PR #832 review (Tester + Requirements Engineer test-coverage concerns).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:33:01 +02:00
Marcel
4dc5e3278f fix(timeline): wire the server date error to the date field's aria-invalid
REQ-011 asks for both required-field errors via per-field aria-invalid, but the
blank-date error was rendered as a standalone <p> after the date card while the
date input's aria-invalid only reflected the client-side malformed-date cue.

DatePrecisionField gains a `dateError` prop: a server error now marks the field
aria-invalid and renders inline under the input (sharing the same error id), and
EventForm drops its detached <p>. While here, migrate the field's two error
texts from hard-coded text-red-600 to the semantic `text-danger` token so they
keep ≥4.5:1 contrast in dark mode (the token remaps; #dc2626 was borderline) —
this also fixes the contrast for WhoWhenSection, the other consumer.

Component test asserts the date input gains aria-invalid on a server date error.

Addresses PR #832 review (Requirements Engineer REQ-011; UI/UX dark-mode contrast).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:30:40 +02:00
Marcel
c13baa4785 fix(timeline): rehydrate event-form pickers after a validation failure
Decision 6 / REQ-010 promised the pickers survive a fail(400) "including
pre-selected persons/documents", but EventForm only seeded them from
event/initialPersons — never from the fail payload — and the payload carried
only bare ids, which can't rebuild a chip (chips need displayName/title). On
the use:enhance path the in-memory selection survived; on a no-JS full reload
the chips were silently dropped.

Now the save action re-fetches the selected persons/documents by id
(lookupSelections, non-ok swallowed like the prefill path) and returns full
chip data; EventForm seeds the pickers from form.persons/documents ahead of
the seeded event. Extract preservedFormFields() to DRY the four fail payloads;
validateEventForm now returns the error pair and the route owns the fail().

Component test pins the rehydration; the server spec now asserts the fail
payload carries labelled chips, not just ids.

Addresses PR #832 review (Developer + Requirements Engineer concern, REQ-010).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:28:28 +02:00
Marcel
cd5649b96e fix(timeline): harden curator event precision field
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m51s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 4m35s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
SDD Gate / RTM Check (pull_request) Successful in 13s
SDD Gate / Contract Validate (pull_request) Successful in 22s
SDD Gate / Constitution Impact (pull_request) Successful in 17s
- Validate the submitted precision against the DatePrecision allow-list in
  parseEventForm (falls back to DAY) so an untrusted token can't flow into
  the request body — symmetric with the existing `type` narrowing.
- Parameterize the precision input name via DatePrecisionField's new
  precisionInputName prop; the timeline form now submits `precision` instead
  of the misleading document-domain `metaDatePrecision`. Document form keeps
  the default, so its behaviour is unchanged.
- Capture EventTypeSelect's onchange into EventForm's `type` state so it no
  longer goes stale (the submitted value was already correct via the hidden
  input; this keeps the local state in sync).

Addresses PR #832 review (#781).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 23:22:11 +02:00
Marcel
9f17c4538f refactor(timeline): extract requireWriteAll route guard
Both curator-event loaders repeated the same null-user + hasWriteAll block.
hasWriteAll already returns false for an anonymous user, so a single
requireWriteAll(locals) helper covers both REQ-002 (null user → 403) and
REQ-003 (no WRITE_ALL → 403) without the redundant pre-check.

Addresses PR #832 review (#781).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 23:21:37 +02:00
Marcel
068c2ef256 fix(document): render full date for precision-less document chips
A TimelineEvent's DocumentRef carries documentDate but no precision, so
formatDocumentOption hit formatDocumentDate's undefined-precision path and
surfaced the UNKNOWN label instead of the date. Default a missing precision
to DAY so the chip shows the full date; add formatDocumentOption unit specs.

Addresses PR #832 review (#781).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 23:21:07 +02:00
Marcel
94d7d8099f fix(types): make DocumentOption precision fields optional; narrow spec data access
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m0s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 4m54s
CI / fail2ban Regex (pull_request) Successful in 47s
SDD Gate / RTM Check (pull_request) Successful in 15s
SDD Gate / Contract Validate (pull_request) Successful in 22s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m8s
SDD Gate / Constitution Impact (pull_request) Successful in 16s
DocumentOption's metaDatePrecision/metaDateEnd are now optional so a
TimelineEvent DocumentRef (id/title/documentDate only) maps cleanly into a
picker chip — formatDocumentOption already degrades gracefully when precision is
absent. The server specs read fail()'s union data via a small failData() cast
that TS cannot narrow. svelte-check shows zero new errors in the #781 files.

Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 23:00:18 +02:00
Marcel
a50bdfa7f4 docs(timeline): document curator event routes (CLAUDE.md, C4, rtm)
Adds the events/new + events/[id]/edit children to both CLAUDE.md route tables
and the frontend C4 people-stories diagram (new zeitstrahlEvents component +
backend relation), and traces REQ-001..017 (feature timeline-curator-forms) in
.specify/rtm.md mirroring the sibling #776/#777/#778/#779 timeline rows.

Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:55:58 +02:00
Marcel
be26a2e1b3 test(e2e): thin curator event-editor journey (create, 403, 320px)
One critical create journey (fill form with precision RANGE → HTTP 200 on
/zeitstrahl; the card assertion depends on #7), one security counterpart
(logged-out → 403), and one 320px no-overflow guarantee. Intentionally thin —
ci.yml does not run test:e2e today, so regression coverage lives in the
component + server specs that DO run in CI. Written, not executed locally.

Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:53:30 +02:00
Marcel
5cfb4608f6 feat(timeline): add /zeitstrahl/events/[id]/edit curator edit + delete route
Load gates on hasWriteAll (null-user guard first, 403 error page) and seeds the
form from GET /api/timeline/events/{id}, failing closed with 404 on ANY non-ok
response so derived person-events (no UUID) and unknown ids never render a blank
create form. The save action PUTs with the optimistic-lock version (threaded via
a hidden input EventForm now emits), mapping 409 to the generic conflict message
without redirecting. The delete action DELETEs behind getConfirmService(),
returns fail(status) on a non-ok response (no redirect), and otherwise redirects
to the UUID-validated nav target. 8/8 server specs green; EventForm 6/6 green.

Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:51:56 +02:00
Marcel
59d78150b3 feat(timeline): add /zeitstrahl/events/new curator create route
Server load gates on hasWriteAll with a null-user guard first (403 error page,
the persons/new idiom — not a redirect); prefills ?personId/?documentId via
Promise.all, swallowing 404/403 so unknown ids never leak. The save action
parses the form, surfaces title+date required errors simultaneously via
fail(400) preserving picker arrays, builds a TimelineEventRequest (eventDateEnd
explicit null off RANGE), POSTs, maps API/409 errors via getErrorMessage without
redirecting, and redirects to a UUID-validated nav target (CWE-601). Shared
parse/validate/build/nav helpers live in eventFormServer.ts for reuse by the
edit route. 11/11 server specs green.

Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:49:05 +02:00
Marcel
15ff6db1d3 feat(timeline): add EventForm curator create/edit form
One component for both routes: /new renders it empty, /[id]/edit seeds it from a
TimelineEventView. Composes EventTypeSelect, the shared DatePrecisionField, a
plain-textarea description, PersonMultiSelect and DocumentMultiSelect (personIds
/documentIds hidden inputs). lg:grid-cols-[2fr_1fr] collapsing to one column
below lg, sticky save bar, beforeNavigate unsaved-changes guard, submitting flag
via use:enhance (disabled submit), and a delete form gated by getConfirmService()
read lazily so the component mounts cleanly in isolation. Title/description/chip
labels render via default {...} escaping (CWE-79). Seeded DocumentRefs degrade
gracefully to DocumentOption (no precision fields). Pickers gain an inputId prop
so <label for> associates the control; eslint boundaries now lets timeline import
person+document (mirrors the geschichte editor). 6/6 component specs green.

Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:45:39 +02:00
Marcel
54f9d8fdd5 feat(timeline): add EventTypeSelect segmented radio (PERSONAL/HISTORICAL)
grid-cols-2 segmented radio group modelled on PersonTypeSelector: role=radiogroup
with role=radio buttons, roving tabindex, radioGroupNav arrow-key support, and an
sr-only aria-live type-change announcement. Each option pairs a decorative
aria-hidden icon with a visible localized text label (icon is never the sole
differentiator), min-h-48px target. Emits a hidden input for form submission.

Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:40:00 +02:00
Marcel
423aedcd87 fix(a11y): 44px remove targets + empty states on person/document pickers
Both PersonMultiSelect and DocumentMultiSelect remove buttons were ~12px tap
targets (below the 44px WCAG minimum) — pad them to min-h/min-w 44px with a
focus-visible ring (SVG stays 12px). Add an optional emptyLabel slot inside the
chip container and a hiddenInputName prop on PersonMultiSelect (mirroring
DocumentMultiSelect) so EventForm can wire personIds without touching
WhoWhenSection. Document the intentional bare typeahead fetch in
documentTypeahead.ts (same-origin in prod, Vite-proxied in dev).

Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:38:00 +02:00
Marcel
0ed7fb4c0e i18n(timeline): add event-editor keys (de/en/es)
Labels, section headings, type options (PERSONAL/HISTORICAL), picker empty
states, required-field errors, delete-confirm and unsaved-changes copy for the
curator event create/edit forms. No new ErrorCode introduced — the feature
reuses existing TIMELINE_EVENT_* + CONFLICT codes from #3.

Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:35:57 +02:00
Marcel
62fcc53f5c refactor(shared): extract DatePrecisionField primitive from WhoWhenSection
Pulls the date + precision + RANGE-end-date region into a generic primitive in
$lib/shared/primitives/ so both document/ (WhoWhenSection) and timeline/
(EventForm, #781) can consume it without a cross-domain import. Preserves the
aria-live="polite" outer wrapper, onMount one-time seeding, $bindable
precision/endDateIso, the PRECISIONS array, and forwards data-testid attributes
so the existing WhoWhenSection spec selectors survive. WhoWhenSection spec stays
green (7/7).

Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:33:50 +02:00
Marcel
36f7bdad45 test(document): fence RANGE end-date reveal before DatePrecisionField extraction
Adds a RANGE-reveal regression test to WhoWhenSection's spec. The existing
spec covered only date pre-fill / hideDate / location, leaving the end-date
region without a red signal. This must stay green across the #781 extraction
of DatePrecisionField into $lib/shared/primitives/.

Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:30:56 +02:00
Marcel
696a86799d fix(a11y): add aria-modal to ConfirmDialog for older AT
NVDA+Chrome <2022 and VoiceOver iOS <16 need explicit aria-modal="true";
showModal() implicit modal semantics are not enough for older AT. One-line
patch benefits all dialog uses.

Refs #781
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:29:46 +02:00
Marcel
ce1b4c748e test(i18n): pin localized timeline layer/derived labels per locale
REQ-024 was updated (issue #779) to require localized sr-only/aria
labels instead of German-only. Pin the de/en/es values so they cannot
silently drift back to the German source strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 21:30:02 +02:00
Marcel
4a6fd770d7 fix(i18n): translate timeline sr-only labels in en/es locales
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 5m18s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 5m17s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m8s
SDD Gate / RTM Check (pull_request) Successful in 15s
SDD Gate / Contract Validate (pull_request) Successful in 23s
SDD Gate / Constitution Impact (pull_request) Successful in 18s
timeline_layer_* and timeline_derived_* shipped German values in the
English and Spanish catalogs, so EN/ES screen-reader users heard German
for the world/family layer and birth/death/marriage cues. Translate them;
de.json stays canonical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:34:54 +02:00
Marcel
732651959e fix(timeline): render undated events as pills/bands, not letter cards
The undated bucket is assembled from all entries, so it can contain
events as well as letters. Rendering every undated entry with LetterCard
produced a dead /documents/undefined link and "Unknown -> Unknown" for
events. Dispatch on kind/type like YearBand does (WorldBand/EventPill/
LetterCard).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:32:32 +02:00
Marcel
7902f4e6ac refactor(timeline): extract entryKey helper from YearBand
Move the per-entry {#each} key logic into a shared entryKey.ts so the
undated bucket in TimelineView can reuse it. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:30:59 +02:00
Marcel
fee519b8a9 docs(timeline): document /zeitstrahl, lib/timeline, monthBuckets move; RTM #779
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 5m28s
CI / OCR Service Tests (pull_request) Successful in 26s
CI / Backend Unit Tests (pull_request) Successful in 6m25s
CI / fail2ban Regex (pull_request) Successful in 48s
CI / Semgrep Security Scan (pull_request) Successful in 25s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m10s
SDD Gate / RTM Check (pull_request) Successful in 18s
SDD Gate / Contract Validate (pull_request) Successful in 28s
SDD Gate / Constitution Impact (pull_request) Successful in 17s
Route tables (CLAUDE.md + frontend/CLAUDE.md), the document/timeline.ts ->
$lib/shared/utils/monthBuckets move (document + shared READMEs), GLOSSARY
Lebensweg entry, the c4 l3-frontend people-stories diagram, and the RTM rows
REQ-001..027 for feature zeitstrahl-global-view (#779), all marked Done.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:00:00 +02:00
Marcel
b501592156 docs(timeline): reword LetterCard comment so the REQ-021 @html grep is zero
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:59:41 +02:00
Marcel
852fb71ee7 test(timeline): add /zeitstrahl E2E spec
Nav-link smoke + timeline-in-<main> (empty-or-populated), and the 320px
no-overflow guarantee on a timeline seeded with 25+char correspondent names
(REQ-005). Runs against the real stack via the seeded admin session.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:54:07 +02:00
Marcel
6f32299255 feat(timeline): add /zeitstrahl route, SSR load, and nav link
SSR-first load fetches GET /api/timeline via createApiClient (auth cookie
forwarded), no query params for the global view (REQ-001), returns { timeline }
with no client-side fetch (REQ-002); 401 -> /login, any other non-ok ->
error(status, getErrorMessage(...)), never raw JSON, no PII logged (REQ-022).
The page renders <TimelineView> under the layout's <main>. Adds the Zeitstrahl
nav link (desktop + mobile) and 'timeline' to the eslint routes boundary
allow-list so the route may import the domain.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:50:28 +02:00
Marcel
dbef0e1e60 fix(timeline): wrap long letter names/titles to avoid 320px overflow
break-words on sender/receiver/title so a 25+char correspondent name cannot
force horizontal overflow on a 320px phone (REQ-005).

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:44:23 +02:00
Marcel
588314f862 feat(timeline): add TimelineView orchestrator
Renders year bands in DTO order with interior empty-year runs folded into one
GapSpan (REQ-015), a single <ol> in chronological DOM order (REQ-006), the undated
bucket via {#if} (REQ-016), and a calm empty state (REQ-017). personId is a
declared seam (issue #10), undefined here, never passed to leaf cards (REQ-025).
Centered desktop spine / left phone spine via scoped CSS. Owns no <main>.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:44:05 +02:00
Marcel
f9ddcf0374 feat(timeline): add YearBand (section + sticky h2, cards vs strip)
One <section> per year with a sticky <h2> at top:4rem (REQ-006). Events render in
DTO order as pills/bands; letters render as individual cards while <= 12 (REQ-011)
or collapse to one density strip above that (REQ-012); DTO order is never re-sorted
(REQ-003). Letters carry an alternating data-side for the centered desktop axis
(REQ-004); single left column on phone (REQ-005). Derived-safe {#each} key.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:43:47 +02:00
Marcel
5bff428954 feat(timeline): add YearLetterStrip for dense years
Letter count + 12-month density sparkline + a >=44px keyboard-focusable expand
toggle that reveals that year's LetterCards (REQ-012). Sparkline values from the
shared monthHistogram.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:36:57 +02:00
Marcel
bea0e0d056 feat(timeline): add GapSpan for folded empty-year runs
A thin dashed span rendering '{from}–{to} · keine Einträge', collapsing to a
single year when the run has length 1 (REQ-015).

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:36:39 +02:00
Marcel
e75448ba14 feat(timeline): add WorldBand for HISTORICAL context bands
Full-width muted band; RANGE renders a span pill (1914–1918) with a Zeitraum
aria-label (REQ-009); a RANGE with no end degrades to the start year, no pill,
no crash (REQ-010). World glyph is a redundant non-color cue with sr-only label
(REQ-018); text uses text-ink-2 to hold AA in both themes (REQ-019).

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:36:21 +02:00
Marcel
b031f2736b feat(timeline): add EventPill for derived + curated event pills
Centered axis pill: derived life-events (* Geburt / † Tod / ⚭ Heirat) and curated
PERSONAL events (★, mint border) via getAccentConfig. Glyph wrapped aria-hidden +
sr-only label (REQ-018). Edit affordance only for a curated event with eventId,
never derived/null (REQ-008). REQ-007.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:36:03 +02:00
Marcel
e25001f7c9 feat(timeline): add LetterCard component
Single archive letter: sender → receiver (Unbekannt fallback for empty names,
REQ-014), title, precision date chip via timelineDateLabel (omitted when null,
REQ-013), linking to exactly /documents/{documentId} with no target (REQ-023).
44px touch target enforced inline + focus-visible ring (REQ-020). OCR/import
text via {...} escaping + whitespace-pre-line, no {@html} (REQ-021).

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:30:55 +02:00
Marcel
6a35e8510b feat(timeline): add eventCardConfig accent matrix + DTO test factories
getAccentConfig(entry) maps each EVENT to its glyph (* / † / ⚭ / ★ / ◍), German
redundant-cue label, and accent kind (REQ-007/008/018). test-factories build
TimelineEntryDTO/TimelineDTO mirroring the real wire shape for component specs.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:26:29 +02:00
Marcel
607112afc2 feat(shared): add Sparkline primitive for fixed-series density bars
A minimal presentational bar series (one bar per value, heights scaled to the
max, faint floor for empty buckets). Lives in shared so both the timeline
density strip and the document chart can use it. REQ-012 (supports).

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:26:11 +02:00
Marcel
4e119f098d feat(timeline): add timeline i18n keys (de/en/es)
14 Paraglide keys for the /zeitstrahl view: nav link, heading, empty/undated/
gap/unknown-person chrome, letters count, strip expand, range aria, and the
layer/derived labels. The layer (Weltgeschehen/Familie) and derived (Geburt/
Tod/Heirat) labels carry the German term across all locales by design
(documented MVP decision). REQ-024.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:25:43 +02:00
Marcel
f34d42a09f feat(timeline): add timelineDensity helper (isDense, monthHistogram)
isDense(count) thresholds dense year bands at >12 letters (REQ-012);
monthHistogram(letters, year) buckets a band's letters into exactly 12 month
buckets via the shared fillDensityGaps, counting each letter on its eventDate
anchor month and ignoring undated entries (REQ-027). Imports shared only.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:20:37 +02:00
Marcel
1dc3b91458 refactor(timeline): move pure month-bucket math to $lib/shared/utils/monthBuckets
Relocate the 10 pure helpers (monthBoundaryFrom/To, buildMonthSequence,
fillDensityGaps, clipBucketsToRange, aggregateToYears, selectionBoundaryFrom/To,
tickIndicesFor, formatTickLabel) and their unit tests out of document/timeline.ts
into a shared module so lib/timeline/ can consume them without importing
lib/document/. The /api/documents/density glue (buildDensityUrl, fetchDensity,
DensityState, DensityFilters) stays in document/timeline.ts. Re-point the three
density components and the density-filter spec at the shared module.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:18:50 +02:00
Marcel
5017d17b11 chore(api): regenerate TypeScript types for GET /api/timeline
Adds TimelineDTO, TimelineYearDTO, TimelineEntryDTO with kind union
("EVENT"|"LETTER"), eventId, documentId, senderName, receiverName,
linkedPersonIds, derivedType fields.

Refs #777
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 16:43:24 +02:00
Marcel
6d81471294 docs(timeline): flag DatePrecision as a hand-maintained backend mirror
Note above the DatePrecision type that it mirrors the Java DatePrecision enum,
must be updated manually in lockstep with that enum, and must not be migrated
to the OpenAPI-generated type — it drives the shared client-side formatter
shared by documents and the timeline date-label facade.

Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 13:17:24 +02:00
Marcel
956a23d0a8 feat(timeline): add precision-aware date-label facade
timelineDateLabel delegates to the shared formatDocumentDate so a timeline
chip renders identically to the same date on a document, in the active
locale (REQ-001/REQ-002). UNKNOWN precision and null/undefined/'' eventDate
short-circuit to null with no formatter call (REQ-003/REQ-004); raw is always
null since timeline events carry no verbatim spreadsheet cell. The facade
owns no precision logic of its own (REQ-005).

Register the new `timeline` frontend domain in the eslint boundaries config
(allowed to import only `shared`) and add src/lib/timeline/** to the vitest
coverage include (REQ-006). The spec partially mocks the paraglide runtime
via importOriginal so getLocale is stubbed while the formatter still resolves
real season/range message exports.

Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 13:16:33 +02:00
Marcel
f46f153f33 test(timeline): add failing dateLabel facade spec
Red phase for the timeline date-label helper. Asserts delegation to the
shared formatDocumentDate (localized DAY de/en, SEASON de, same-year RANGE)
and the null cases for UNKNOWN/empty eventDate. The runtime mock path keeps
the `.js` suffix so it matches the import under test.

Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 13:13:32 +02:00
Marcel
3de4ff55ea chore(timeline): regenerate API types for event CRUD endpoints
Regenerated frontend/src/lib/generated/api.ts from the OpenAPI spec — adds the
/api/timeline/events paths and TimelineEventRequest/TimelineEventView schemas.
CI has no OpenAPI drift guard, so the regen is committed here. (Operation-id
churn create->create_1 etc. is cosmetic; the typed client keys off paths, not
operation ids; the timeline PersonView merges with geschichte's identical one.)

Per #775.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 12:29:47 +02:00
Marcel
96e04dbda9 feat(timeline): map new ErrorCodes to localized messages
errors.ts ErrorCode union + getErrorMessage() cases for the four new codes,
with de/en/es i18n keys. Conflict messages are calm/recoverable
('...wurde zwischenzeitlich geändert. Bitte neu laden.'). Per #775.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 12:29:47 +02:00
Marcel
d11378c254 fix(deps): pin esbuild 0.28.1 and cookie >=0.7.0 to clear npm audit gate
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 4m46s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 5m42s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m8s
CI / Unit & Component Tests (push) Successful in 5m23s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 6m4s
CI / fail2ban Regex (push) Successful in 48s
CI / Semgrep Security Scan (push) Successful in 25s
CI / Compose Bucket Idempotency (push) Successful in 1m12s
nightly / deploy-staging (push) Successful in 5m20s
The CI step "Security audit (no dev deps)" (npm audit --audit-level=high
--omit=dev) failed repo-wide on every PR: newly-published advisories landed
against the already-pinned esbuild 0.27.7 (GHSA-gv7w-rqvm-qjhr,
GHSA-g7r4-m6w7-qqqr — both high), pulled in transitively via
vite -> @sveltejs/vite-plugin-svelte. The fix for both only exists at
esbuild@0.28.1. A scoped vite@7 minor bump cannot help — all vite 7.x pin
esbuild ^0.27.0.

Add an overrides block:
  - esbuild 0.28.1 (exact, no caret — a future 0.29.x must not silently
    float in and re-break vite; let Renovate propose bumps explicitly)
  - cookie >=0.7.0 (clears the low GHSA-pxg6-pf52-xh8x reaching the prod
    tree via @sentry/sveltekit; drop-in, done in the same pass)

npm audit --audit-level=high --omit=dev now exits 0 with 0 vulnerabilities.
npm run build, lint, and a dev-server boot all succeed with the forced
esbuild 0.28.1 (validated, not assumed — it sits outside vite@7.3.3's
declared ^0.27.0 range).

Closes #817

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 00:23:31 +02:00