Commit Graph

2082 Commits

Author SHA1 Message Date
Marcel
b54a35322b docs(adr): ADR-045 + RTM rows for the #827 grouping modes
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m50s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 5m7s
CI / fail2ban Regex (pull_request) Successful in 44s
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 16s
SDD Gate / Contract Validate (pull_request) Successful in 23s
SDD Gate / Constitution Impact (pull_request) Successful in 15s
Record the three #827 forks (client-side regroup transport, computed letter→event
link reusing timeline_event_documents, filter-then-group composition with #780)
as ADR-045, trace REQ-001..019 (+005b) into the RTM as Done, and list the new
timeline components in the frontend domain inventory.

Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:08:02 +02:00
Marcel
9551bbd1ca test(i18n): assert the #827 grouping + bucket keys exist in every locale
REQ-012 coverage: the new grouping/segment/bucket keys are present in de/en/es
and the pre-existing timeline_grouping_date / timeline_tag_chip_label are reused,
not re-declared.

Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:07:43 +02:00
Marcel
38250606d9 test(timeline): add the e2e grouping spec (zero-fetch, 320px, axe)
Mirrors the #780 layer-filter e2e for #827: switching Datum/Ereignis/Thema
issues zero extra GET /api/timeline (REQ-002), the control stays overflow-free
and ≥44px with full-word aria-labels at 320px (REQ-011), and a 320px axe pass
holds in light and dark mode (REQ-010g). Local-only like the filter e2e (E2E is
not yet in CI).

Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:02:59 +02:00
Marcel
6c85f47794 test(timeline): gate the event layer identity and the {@html} ban
Add the REQ-001 structural-identity check (the event pills/world-bands render
identically across all three grouping modes — the no-VRT-harness equivalent of
the pixel-diff) and the REQ-009 grep gate (no lib/timeline component reaches for
the raw-HTML directive). Reword the BucketHeaderChip doc to describe the
directive by name so the gate stays literal.

Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:00:16 +02:00
Marcel
5936f3a9ae feat(timeline): wire the grouping toggle into the Zeitstrahl page
Add the grouping $state beside the #780 layer-filter state, render the
GroupingControl stacked above the filter trigger (disabled, but kept in place,
when no loose letters remain), make the meta-line grouping label track the
active mode, and thread groupingMode into TimelineView — filter-then-group,
no refetch.

Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:56:55 +02:00
Marcel
8be4b40e54 feat(timeline): render letter buckets in TimelineView/YearBand
Thread groupingMode through TimelineView → YearBand. TimelineView resolves the
event lookup once over the filtered view (so Ereignis clusters never reference a
filtered-out event). In non-Datum modes YearBand keeps its event pills/world-bands
identical (REQ-001) and replaces the loose letters with per-year LetterBuckets
(REQ-002/003/004); Datum keeps the original card/strip path. The undated bucket is
unchanged in every mode.

Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:52:48 +02:00
Marcel
bc22b2d4c9 feat(timeline): add the Datum·Ereignis·Thema grouping control
An ARIA radiogroup with roving tabindex (#827, REQ-010/011/018): three
arrow-key-navigable ≥44px segments with text labels, full-word aria-labels and
≤360px abbreviations, semantic-token colours that hold contrast in dark mode,
defaulting to Datum. When disabled it stays in place, retains its selection, and
announces a screen-reader reason — deliberately distinct from #780's
aria-pressed layer toggles.

Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:48:36 +02:00
Marcel
f3c2465465 feat(timeline): add the LetterBucket cluster component
Renders one loose-letter cluster for Ereignis/Thema mode (#827): an
"✉ <event> · <n>" header over .lcard.ev cards in Ereignis, a tinted
BucketHeaderChip over chip-suppressed cards in Thema, and a localized
"Weitere Briefe"/"Ohne Thema" header with plain cards for the fallback bucket.

Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:42:45 +02:00
Marcel
fd67a21610 feat(i18n): add grouping + bucket message keys for the Zeitstrahl toggle
New de/en/es keys for #827: the Datum·Ereignis·Thema segment labels and their
≤320px abbreviations, the dynamic meta-line grouping labels
(timeline_grouping_event/_thema), the "Weitere Briefe"/"Ohne Thema" bucket
labels, the radiogroup aria-label, the letters-hidden disabled reason, and the
multi-tag hint. Reuses the existing timeline_grouping_date / timeline_tag_chip_label.

Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:40:12 +02:00
Marcel
0ae4e9a311 feat(timeline): give LetterCard an event variant and chip suppression
Add a `variant="event"` that marks the card `.lcard.ev` for Ereignis-mode event
clusters (#827, REQ-014) and a `suppressTagChip` that hides the per-letter
TagChip inside its own Thema bucket where the header already conveys the topic
(REQ-017). Datum/Ereignis keep the #838 per-letter chip behaviour.

Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:37:54 +02:00
Marcel
99528e6bea feat(timeline): add the tinted Thema bucket-header chip
A fully-tinted root-tag chip for Thema-mode bucket headers (#827, REQ-015):
fill and label both derive from the tag's --c-tag-* token via a color-mix wash
so the label keeps ≥4.5:1 contrast in light and dark mode. A null or unknown
token falls back to a neutral chip with no broken colour. Curator text is
{...}-escaped (REQ-009). Distinct from the neutral per-letter TagChip.

Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:34:29 +02:00
Marcel
4b11d66ca5 feat(timeline): add the client-side letter regroup transform
Pure module powering the #827 Datum·Ereignis·Thema toggle: buildEventLookup
(curated events that survived the #780 layer filter), hasLooseLetters (the
control's enabled state), and bucketLetters (cluster loose letters by
linkedEventId or primary root tag, with a "Weitere Briefe"/"Ohne Thema"
fallback). Filter-then-group, no refetch.

Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:31:03 +02:00
Marcel
0726226c95 chore(api): regenerate types with TimelineEntryDTO.linkedEventId
Regenerated frontend/src/lib/generated/api.ts from the live OpenAPI spec after
adding the nullable linkedEventId field — keeps the CI type-check green for the
#827 grouping UI that consumes it.

Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:27:57 +02:00
Marcel
f57e59b53c style(person): drop the unused gap-1.5 from the add-event link
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m43s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 5m1s
CI / fail2ban Regex (pull_request) Successful in 48s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
SDD Gate / RTM Check (pull_request) Successful in 19s
SDD Gate / Contract Validate (pull_request) Successful in 24s
SDD Gate / Constitution Impact (pull_request) Successful in 19s
CI / Unit & Component Tests (push) Successful in 5m14s
CI / OCR Service Tests (push) Successful in 27s
CI / Backend Unit Tests (push) Successful in 6m27s
CI / fail2ban Regex (push) Successful in 55s
CI / Semgrep Security Scan (push) Successful in 26s
CI / Compose Bucket Idempotency (push) Successful in 1m8s
The person-add-event link wraps a single text label, so the flex gap
never applies — only the sibling edit link (icon + text) needs it.
Removing the dead utility per the UI/UX review nit.

Refs #842
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 09:13:14 +02:00
Marcel
4d4266ba99 docs(rtm): trace #842 timeline-curator-affordances + i18n parity guard
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 5m38s
CI / OCR Service Tests (pull_request) Successful in 26s
CI / Backend Unit Tests (pull_request) Successful in 6m35s
CI / fail2ban Regex (pull_request) Successful in 52s
CI / Semgrep Security Scan (pull_request) Successful in 24s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m9s
SDD Gate / RTM Check (pull_request) Successful in 17s
SDD Gate / Contract Validate (pull_request) Successful in 30s
SDD Gate / Constitution Impact (pull_request) Successful in 16s
Add REQ-001..011 rows for the timeline-curator-affordances feature (all
Done) and a messages.spec parity guard pinning timeline_add_event +
person_add_event across de/en/es. REQ-010/011 cite the existing #781
new-event route + its 403 guard test (no new production code).

Refs #842
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 08:19:28 +02:00
Marcel
446611e3cc feat(person): add a curator "Ereignis für diese Person" link to PersonCard
A curator on a person's page can now seed a timeline event from that
person: a gated link to #781's /zeitstrahl/events/new?personId={id},
which prefills the person and returns to /persons/{id} on save. Hidden
for a Reader. New i18n key person_add_event in de/en/es.

Refs #842
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 08:16:10 +02:00
Marcel
9118a10e4b feat(timeline): add the "Ereignis hinzufügen" CTA to the Zeitstrahl header
Wrap the heading in a flex-wrap header so the gated add-event link drops
below the title at narrow widths instead of overflowing; the CTA targets
#781's /zeitstrahl/events/new and only shows for a WRITE_ALL viewer. Also
thread canWrite into TimelineView so a curator sees the edit pencils on
the real page. New i18n key timeline_add_event in de/en/es.

Refs #842
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 08:12:52 +02:00
Marcel
11bcaf7cdb feat(timeline): add a WRITE_ALL-gated edit pencil to WorldBand
A curated HISTORICAL band had no edit affordance at all. Mirror the
EventPill pencil inline at the end of the band (data-testid="event-edit",
/edit href, aria-hidden glyph + sr-only Bearbeiten), gated on
canWrite && eventId != null. Thread canWrite to WorldBand through both
the year-band path and the undated bucket.

Refs #842
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 08:08:14 +02:00
Marcel
cd238285ae feat(timeline): gate the EventPill edit pencil behind canWrite
Thread a gate-closed canWrite prop through TimelineView -> YearBand ->
EventPill and the undated bucket so a Reader never sees a dead-end edit
link. canEdit now also requires canWrite; the default false keeps an
un-threaded caller closed. The real boundary stays the #781 route guard
plus the backend permission -- this only hides the link.

Refs #842
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 08:03:53 +02:00
Marcel
ec0e4dfa45 fix(timeline): track the meta-line counts to the filtered view
Some checks failed
CI / fail2ban Regex (push) Successful in 46s
nightly / npm-audit (push) Failing after 15s
CI / Unit & Component Tests (push) Successful in 5m21s
CI / OCR Service Tests (push) Successful in 25s
CI / Backend Unit Tests (push) Successful in 5m48s
CI / Semgrep Security Scan (push) Successful in 22s
CI / Compose Bucket Idempotency (push) Successful in 1m7s
nightly / deploy-staging (push) Successful in 2m14s
Renovate / renovate (push) Failing after 21s
The /zeitstrahl header sub-line counted the unfiltered timeline, so a
hidden layer (e.g. Letters off) still showed its entries in the totals
("1 Brief" with no letters on screen) — the documented D1 limitation.
Derive the meta from filteredTimeline so the range and letter/event
counts always match what is actually rendered. hasContent stays on the
full timeline so the filter bar and meta line still appear whenever the
archive has content.

Refs #780

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:11:36 +02:00
Marcel
21b1b3b835 test(timeline): add e2e journey + 375px axe for the layer filter
Playwright spec for /zeitstrahl: the primary journey (hide Letters → letter
cards vanish + trigger reports "1 aktiv" → reset restores) and a 375px axe pass
with the collapsible open in light and dark mode. Not skipped — #779 ships the
route. E2E is not wired into CI, so this runs locally only for now.

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:11:36 +02:00
Marcel
33aff36867 test(timeline): guard the layer filter against navigation and fetch
A static boundary gate (mirroring the project's no-`{@html}` greps) that reads
TimelineFilters.svelte and /zeitstrahl/+page.svelte and fails if either ever
reintroduces goto(, url.searchParams, api.GET, or fetch( — the filter must stay
presentation-only and fully client-side.

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:11:36 +02:00
Marcel
e18282318a feat(timeline): wire the layer filter into /zeitstrahl
The route holds the three layer toggles in $state, binds them into
TimelineFilters, and derives a client-side filtered view of the SSR-loaded
timeline that it passes to TimelineView — no goto, no URL param, no extra
fetch. When the active toggles leave nothing visible it renders a calm
filtered-empty message plus a one-click reset below the still-open filter bar,
never a blank page and never the generic "no events" state. The meta-line keeps
counting the unfiltered timeline (D1 known limitation).

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:11:36 +02:00
Marcel
c6fe61f06b feat(timeline): add TimelineFilters presentation component
A dumb, client-side layer-filter bar for /zeitstrahl: three $bindable layer
toggles (Personal/Historical/Letters) in a fieldset/legend, a sticky
"Filter (N aktiv)" trigger driven by hiddenLayerCount, and a reset text button
shown only when a layer is off. Toggles mirror the SearchFilterBar undated-toggle
markup (aria-pressed, ✓ glyph, 44px touch target, semantic tokens). The
collapsible slide honours prefers-reduced-motion by zeroing its duration. No
goto, no fetch — the route owns the state and the filtered view.

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:11:36 +02:00
Marcel
182d014971 feat(timeline): add layer-filter i18n keys (de/en/es)
Eight Paraglide keys for the /zeitstrahl layer filter — layer labels, the
fieldset legend, the sticky trigger (distinct timeline_filter_trigger and
timeline_filter_trigger_active({count}) so it never reads "Filter (0 aktiv)"),
the reset button, and the filtered-empty message — added to all three locales.
messages.spec asserts their presence and the {count} signature.

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:11:36 +02:00
Marcel
dc9d1d52b3 feat(timeline): add client-side layer-filter helpers
Pure helpers for the /zeitstrahl layer filter: isDefaultState and
hiddenLayerCount drive the "Filter (N active)" trigger, and filterTimeline
derives a client-side view that hides personal/historical/letter layers and
drops year bands left empty. Letters ride the Letters layer, HISTORICAL events
the Historical layer, and curated PERSONAL plus derived life-events the
Personal layer.

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:11:36 +02:00
8558567688 feat(relationship): editable relationships with LocalDate+DatePrecision dates and notes (#841)
All checks were successful
CI / Unit & Component Tests (push) Successful in 5m10s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 5m14s
CI / fail2ban Regex (push) Successful in 50s
CI / Semgrep Security Scan (push) Successful in 27s
CI / Compose Bucket Idempotency (push) Successful in 1m5s
Closes #837

Makes `PersonRelationship` fully editable (type, related person, dates, notes), migrates its dates from `Integer fromYear/toYear` to `LocalDate + DatePrecision` (mirroring the #773 person pattern, ADR-039 / V76), activates the previously-dead `notes` column, and gives the Zeitstrahl's derived **Heirat** events full date precision for free.

Both Open Decisions confirmed as adopted: **no `@Version`** (last-write-wins, single-writer archive) and **`DELETE` ownership-mismatch aligned 403 → 404** (anti-enumeration, matching the new `PUT`).

## What's in it
- **V78** migrates `person_relationships.from_year/to_year` → `from_date`/`to_date` + NOT-NULL `*_date_precision` (default `UNKNOWN`); pre-check abort on corrupt years, `YYYY-01-01`/`YEAR` backfill, 5 named CHECK constraints, year columns dropped.
- **`PUT /api/persons/{id}/relationships/{relId}`** (`@RequirePermission(WRITE_ALL)`) re-runs every create invariant (self / coherence / order / reverse-PARENT_OF / duplicate) and re-flags family membership; orientation preserved per viewpoint.
- New `ErrorCode.INVALID_RELATIONSHIP_DATES` registered in all four sites (§3.6).
- `TimelineEventService` sources the derived marriage date from `SPOUSE_OF.fromDate` + precision.
- Frontend: `RelationshipDateField` (DAY/MONTH/YEAR), upsert-capable `AddRelationshipForm` (pre-fill + notes + in-flight submit lock), `RelationshipChip` Edit affordance, `updateRelationship` server action, read-view date range + notes, `formatRelationshipDateRange` helper. `api.ts` regenerated.
- Docs: ADR-044, db-orm/db-relationships diagrams, DEPLOYMENT §5 deploy note, RTM REQ-001…REQ-019.

## Requirements
All 19 EARS requirements implemented red/green and marked `Done` in `.specify/rtm.md`.

## Test plan
- **Backend** (targeted, green): `RelationshipMigrationTest` (Testcontainers pg16, 8), `RelationshipServiceTest` (22), `RelationshipControllerTest` (15), `RelationshipServiceIntegrationTest` (real DB, 10), `DerivedEventsAssemblyTest` (17), `ArchitectureTest` (14); `clean package` builds.
- **Frontend** (green): `relationshipDates.spec.ts`, `AddRelationshipForm.svelte.spec.ts`, `RelationshipChip.svelte.spec.ts`, `PersonRelationshipsCard.svelte.test.ts`, `page.server.spec.ts`, `messages.spec.ts`. `npm run check` = 798 (below the ~834 baseline); `npm run lint` clean.

## Notes for reviewers
- **Spec deviation:** the edit form was built by making `AddRelationshipForm` upsert-capable rather than a duplicate `EditRelationshipForm` (DRY); RTM rows reference `AddRelationshipForm.svelte.spec.ts`.
- `api.ts` regenerated from the live spec; only relationship-relevant hunks remain (one springdoc `PageableObject` field-reorder pruned).
- **Deploy:** V78 is one-way and not rolling-deploy-safe — stop old JAR → start new JAR (Flyway runs first); targeted `pg_restore -t person_relationships` for rollback. No maintenance window.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Marcel <marcel@familienarchiv>
Reviewed-on: #841
2026-06-14 21:17:36 +02:00
Marcel
1cd6ffd5ca refactor(timeline): de-duplicate the TagChip markup
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 5m4s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 5m26s
CI / fail2ban Regex (pull_request) Successful in 49s
CI / Semgrep Security Scan (pull_request) Successful in 22s
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 28s
SDD Gate / Constitution Impact (pull_request) Successful in 18s
CI / Unit & Component Tests (push) Successful in 4m21s
CI / OCR Service Tests (push) Successful in 25s
CI / Backend Unit Tests (push) Successful in 4m57s
CI / fail2ban Regex (push) Successful in 48s
CI / Semgrep Security Scan (push) Successful in 30s
CI / Compose Bucket Idempotency (push) Successful in 1m8s
Two cleanups flagged in review, both behaviour-preserving:
- collapse the {#if color}/{:else} square-marker branches (identical but for the
  neutral fill) into one element via class:bg-ink-3={!color}; squareStyle is
  already empty when color is null, so no var(--c-tag-) leaks into the neutral
  chip.
- drop the redundant `truncate` class from the name span — the inline
  overflow/ellipsis trio (kept so it applies before the stylesheet loads,
  REQ-008a) already expresses exactly what `truncate` would.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:59:10 +02:00
Marcel
bbf2f96e28 docs(timeline): reword TagChip comment to clear the raw-HTML grep gate
The doc comment described escaping by naming the raw-HTML directive literally,
which trips the lib/timeline grep gate that forbids that token. Reword it the
way LetterCard already does — behavior unchanged.

Refs #835
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:16:51 +02:00
Marcel
8376a520c5 feat(timeline): show the root-tag chip on the letter card
LetterCard now renders a TagChip beneath the sender→receiver/date line
whenever the entry carries a rootTagName, mapping rootTagColor to the chip
(neutral when null). Because the chip lives on LetterCard it shows up wherever
a LetterCard does — the global timeline and the expanded YearLetterStrip — with
no per-surface special-casing; a tagless letter shows no chip. A long name
truncates inline so the card never overflows at 320px.

Refs #835
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:13:21 +02:00
Marcel
c19d4be3fe feat(timeline): add the root-tag chip component
TagChip renders a letter's primary root tag as a small rounded pill — a
decorative aria-hidden colored square (var(--c-tag-{token}), neutral when the
color is null) plus the escaped tag name, prefixed by the sr-only theme label
so color is never the only cue. Truncation is set inline so a long name
ellipsizes without forcing the card into horizontal scroll, and the full name
stays reachable via the chip title. Timeline-local by design — lib/timeline may
not import lib/tag (eslint boundary).

Refs #835
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:08:51 +02:00
Marcel
90e2b4d6c2 feat(i18n): add the timeline tag-chip theme label
timeline_tag_chip_label (de "Thema" / en "Topic" / es "Tema") is the sr-only
prefix the /zeitstrahl letter tag chip reads out so color is never the only
cue. Pinned per locale in messages.spec.ts; the tag name itself is rendered as
data, never translated.

Refs #835
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:08:21 +02:00
Marcel
d33c1e5249 chore(api): regenerate api.ts with the timeline root-tag fields
openapi-typescript pickup of TimelineEntryDTO.rootTagId/rootTagName/
rootTagColor (all optional), so the SvelteKit timeline can read the new
letter chip fields. Regenerated from the live dev spec; only the additive
fields differ from the committed baseline.

Refs #835
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:03:16 +02:00
Marcel
239565ea20 refactor(timeline): single-source the spine X position via --spine-x
All checks were successful
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m9s
SDD Gate / RTM Check (pull_request) Successful in 18s
SDD Gate / Contract Validate (pull_request) Successful in 25s
SDD Gate / Constitution Impact (pull_request) Successful in 19s
CI / Unit & Component Tests (push) Successful in 4m33s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 5m19s
CI / fail2ban Regex (push) Successful in 46s
CI / Semgrep Security Scan (push) Successful in 26s
CI / Compose Bucket Idempotency (push) Successful in 1m8s
CI / Unit & Component Tests (pull_request) Successful in 4m24s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 5m21s
The spine offset (0.5rem phone / 50% desktop) was hard-coded in both
TimelineView's .timeline-axis::before and YearBand's .year-node/.letter-dot,
kept in sync only by a comment. Declare --spine-x once on .timeline-axis
and have the markers consume it by inheritance, so a change to the spine
position moves the markers with it. Add a test that the year-node tracks
the token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 13:51:46 +02:00
Marcel
0a235dc911 refactor(timeline): extract a shared GlyphLabel primitive
The aria-hidden glyph + sr-only label markup was hand-copied in LetterCard
and YearLetterStrip. Extract a small GlyphLabel component and use it at
both sites so the accessibility idiom has a single owner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 13:48:25 +02:00
Marcel
0bd6790b1f refactor(timeline): count timelineMeta totals in a single pass
Replace the flatMap intermediate array plus two filter passes with one
walk over the year bands and the undated bucket. Same counts, no
throwaway allocation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 12:19:40 +02:00
Marcel
84938e1bf3 refactor(timeline): render the WorldBand historical suffix once
The "· historisch" register was emitted in all three date branches, with
the dateless branch dropping the leading separator. Render the span pill
or date as a conditional prefix, then a single trailing "· historisch"
span — one render site, consistent separator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 12:17:39 +02:00
Marcel
398babe584 fix(timeline): pluralize the zeitstrahl meta-line counts
A count of one rendered "1 Briefe" / "1 Ereignisse". Add _singular
companion keys (de/en/es) and select them when the count is exactly one,
following the project's _singular/_plural convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 12:15:11 +02:00
Marcel
9665c9c0fc fix(timeline): drop zero-count segments from the zeitstrahl meta line
A timeline with content in only one category rendered a misleading
"0 Briefe" / "0 Ereignisse" segment. Only push a count segment when its
count is greater than zero; the grouping label and (when present) the
range are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 12:10:37 +02:00
Marcel
f715f9ce9c fix(timeline): show EventPill provenance even when the event has no date
The provenance token (abgeleitet/kuratiert) was nested inside the
{#if dateLabel} block, so an undated or UNKNOWN-precision event — e.g.
one in the undated bucket — rendered no provenance at all. Compose the
subtitle as an optional "{date} · " prefix in front of the always-present
provenance instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 12:08:13 +02:00
Marcel
15836ea9ca refactor(timeline): drop the canvas outer border (REQ-001)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m1s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 5m1s
CI / fail2ban Regex (pull_request) Successful in 43s
CI / Semgrep Security Scan (pull_request) Successful in 23s
SDD Gate / RTM Check (pull_request) Successful in 16s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
SDD Gate / Contract Validate (pull_request) Successful in 23s
SDD Gate / Constitution Impact (pull_request) Successful in 18s
The page is already bg-canvas, so the frame's border was the only thing
making it visible; per review it reads cleaner without it. Keep the padded
bg-canvas surface; the timeline sits on the page without a frame line.

Refs #833
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:39:17 +02:00
Marcel
8029bdec92 fix(timeline): keep the axis spine behind the in-flow content (REQ-006)
The absolutely-positioned spine ::before painted above the in-flow centered
content (density strips, event pills), drawing the line through them. Give
.timeline-axis a stacking context and the spine z-index:-1 so the line is
always background; cards, pills, strips, dots and badges ride on top.

Refs #833
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:38:32 +02:00
Marcel
217508ddb2 fix(timeline): keep the year badge above its node marker (REQ-004)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m23s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 5m37s
CI / fail2ban Regex (pull_request) Successful in 53s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m10s
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 16s
The node marker carried a higher stacking order than the (non-positioned)
badge, so on the centered desktop axis the navy node painted over the white
year digits. Make the badge positioned with the higher z-index; the node now
sits behind the centered pill (which is itself the axis interruption) and
shows only to the badge's left on phone. Guarded by a z-index assertion.

Refs #833
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:30:59 +02:00
Marcel
2ac4aa8f9c test(timeline): type-clean the zeitstrahl page spec data (REQ-001/002)
Pass the full PageData shape (layout auth fields + timeline) through a
pageData() helper so the route spec is svelte-check-clean; the component
still only reads data.timeline.

Refs #833
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:16:07 +02:00
Marcel
bfe66569d7 feat(timeline): paint the axis with a three-stop gradient (REQ-006)
The spine now runs mint → navy → slate, matching the spec's life-thread,
using --palette-mint / --palette-navy / --c-tag-slate (no --palette-slate
token exists). Semantic tokens only — no raw hex.

Refs #833
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:10:49 +02:00
Marcel
18934413bb feat(timeline): center the year badge and add spine markers (REQ-003/004/005)
The year badge now centers on the axis at ≥1024px and hugs the left spine
below that (sticky top:4rem preserved), with a navy node marker so it
visibly interrupts the spine. Each letter row gains a connector dot (white
fill, mint ring) on the spine: centered between card and axis on desktop,
on the left spine clear of the indented card on phone. Spine geometry is
commented to track TimelineView's spine so the markers can't silently desync.

Refs #833
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:10:05 +02:00
Marcel
e4da28d795 feat(timeline): frame /zeitstrahl in a canvas with a meta line (REQ-001/002)
The timeline now sits inside a bordered, rounded bg-canvas sheet. Below the
heading a sub-line composes the year range, the letter and event counts
(from timelineMeta), and the static "Gruppierung: Datum" — joined by " · "
so the range drops out when there are no year bands and the whole line is
absent for an empty timeline. Semantic tokens only; AA-legible text-xs.

Refs #833
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:00:54 +02:00
Marcel
a1e57ff8cf feat(timeline): derive header meta figures from the DTO (REQ-002)
A pure timelineMeta() returns the year range (first/last band, null when
there are no bands) and the letter/event totals across all year bands plus
the undated bucket — the single place these counts are computed.

Refs #833
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:58:25 +02:00
Marcel
e0b096f12c feat(timeline): frame the undated bucket with a dashed border + count (REQ-012)
The "Ohne Datum" section now renders inside a dashed-bordered surface box
whose heading reads "Ohne Datum · {count}", matching the spec's .undated
treatment. The kind/type dispatch (events as pills/bands, letters as cards)
is unchanged; the section stays absent when there are no undated entries.

Refs #833
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:55:37 +02:00
Marcel
6382efa65a feat(timeline): caption the density strip with ✉ + month axis (REQ-010/011)
The dense-year strip count now carries the ✉ glyph (aria-hidden + sr-only
"Brief"), and beneath the sparkline a "Monats-Dichte" caption sits between
the two endpoint month labels (Jan/Dez {year}) at the ≥10px micro-axis
floor, localized via the shared month formatter. The ≥44px keyboard-
focusable "Briefe anzeigen" expand toggle is preserved unchanged.

Refs #833
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:52:55 +02:00