Timeline × Lesereisen: cross-epic synergies — promotion seam + mutual reference (avoid duplicate curation) #785

Open
opened 2026-06-08 12:03:44 +02:00 by marcel · 0 comments
Owner

Milestones: #14 Zeitstrahl — Family Timeline · #13 Lesereisen (Reading Journeys)
Relates to: #774 (TimelineEvent entity), #775 (TimelineEvent CRUD), #781 (curator event forms), #778 (shared dateLabel helper) · #750 (GeschichteType/JourneyItem), #751 (JourneyItem CRUD), #753 (Journey editor)
Status: design-the-seam-now, build-post-MVP. Has open decisions — see "Decisions to resolve".

Why this issue exists

Two in-flight epics both wrap curated documents + narrative + persons:

  • TimelineEvent (#14, new timeline/ domain): a time-anchored cluster of letters — title, type (PERSONAL/HISTORICAL), eventDate + DatePrecision, short description, ManyToMany documents/persons.
  • Lesereise/Journey (#13): a GeschichteType.JOURNEY subtype of Geschichte — an ordered sequence of JourneyItems with interludes (Zwischentexte), drag-reorder, rich narrative.

This issue records the boundary decision (they are not duplicates) and the small seams that make them reinforce each other instead of drifting into two half-overlapping curation tools.

Verdict: complementary primitives — do NOT merge

The timeline design spec already keeps the timeline/ domain "deliberately separate from the in-flight Lesereisen/Geschichte work." That is correct and should stand.

TimelineEvent (#14) Lesereise/Journey (#13)
Entity new timeline/ domain GeschichteType.JOURNEY (extends Geschichte)
Organizing principle time (eventDate + DatePrecision) reading order (JourneyItem.sortOrder)
Documents unordered ManyToMany cluster ordered JourneyItems
Narrative short caption (description TEXT) rich body + interludes between letters
Reader intent explore when / get context read start-to-finish, guided
Permission WRITE_ALL BLOG_WRITE

A TimelineEvent is a dot/span in time; a Lesereise is a path through pages. Merging would force an event to carry ordering + interludes it doesn't want, and a journey to carry a date axis it doesn't have (duplication-by-over-generalization).

Boundary rule (the actual anti-duplication guard)

The overlap hides in the event description. If it grows into rich text with commentary between letters, we've reinvented a Journey inside a TimelineEvent.

Rule: TimelineEvent.description stays a short caption (plain TEXT, no ordering, no interludes). The moment a curator wants ordered reading + interludes, that is a Lesereise — the event links to it rather than growing its own narrative.

This rule is already reflected in #781 (description = optional plain textarea, not rich text). Keep it.

Proposed synergies (scoped)

S1 — Shared UI primitives (already in motion; just don't fork)

Both editors descend from GeschichteEditor. The Journey editor (#753) = GeschichteEditor + ordering + interludes; the TimelineEvent editor (#781) = GeschichteEditor + date/precision, reusing the same DocumentMultiSelect + PersonMultiSelect and the shared DatePrecisionField (#781) / dateLabel helper (#778). Action: no second picker, no second date primitive — verify both editors consume the shared components rather than re-implementing. (Largely covered by #781/#753; this is a cross-check, not new code.)

S2 — Promotion seam: TimelineEvent → "Als Lesereise kuratieren" (primary value)

The timeline naturally discovers clusters ("24 Feldpost 1915"). Add an action on a TimelineEvent (event card and/or /zeitstrahl/events/[id]/edit) → opens the Journey editor pre-filled with the event's documents, ordered by documentDate. The curator then reorders + adds interludes. Funnel: discover-in-time (Zeitstrahl) → narrate-as-path (Lesereise).

  • Reuses the existing ?documentId= prefill idiom (Journey editor / geschichten/new), extended to accept multiple ids (e.g. ?documentIds=a,b,c or a transient seed) in documentDate order.

S3 — Mutual reference (TimelineEvent ↔ Geschichte/Journey)

A nullable reference so the two can point at each other:

  • On a TimelineEvent: "▸ Als Lesereise lesen" when a linked Journey exists.
  • On a Journey detail: "▸ Im Zeitstrahl zeigen" when it originated from / is tied to an event.
  • Modelling options in "Decisions to resolve".

S4 — Lesereise as a timeline marker (discovery) (optional, later)

A Journey's items carry dates; the assembly endpoint (#777) could surface a Journey as a single marker on /zeitstrahl (placed at its earliest item date) → tap → reader. Makes the Zeitstrahl a discovery surface for journeys. Lowest priority of the four.

Decisions to resolve (needs-discussion)

  1. Reference modelling (S3): (a) TimelineEvent.geschichteId nullable FK → Geschichte; (b) reuse Geschichte.documents↔event overlap with no FK (derive the link); (c) a small join table for many-to-many event↔journey. Leaning (a) — simplest, one direction is enough for MVP.
  2. Promotion direction for MVP (S2): event→journey only (recommended), or also journey→event? Event→journey matches the discover→narrate funnel; the reverse is rarer.
  3. Permission alignment: timeline curation is WRITE_ALL, journeys are BLOG_WRITE. If the same people curate both, align the gates (likely both BLOG_WRITE, or document why they differ). Cross-epic decision — owners of #775 and #751 should agree.
  4. Multi-id prefill: extend the Journey editor's ?documentId= to a multi-id seed in date order, vs. a one-shot "create journey from these N documents" server action. Affects #753.
  5. S4 in or out of MVP: recommend out (post-MVP); confirm.

Scope / non-goals

  • In: the boundary rule, S1 cross-check, S2 promotion seam, S3 mutual reference (one direction).
  • Out (explicit non-goals): merging the entities; adding ordering/interludes to TimelineEvent; adding a date axis or DatePrecision to Geschichte; S4 (deferred). Do not generalize a shared "curated collection" super-entity.

Dependencies

Blocked until both sides have entities + editors: #774, #775, #781 (timeline) and #750, #751, #753 (lesereisen). Best picked up after both editors exist so the promotion seam targets real routes.

Acceptance criteria (Given-When-Then)

  • Given a curator on a TimelineEvent with linked documents, when they choose "Als Lesereise kuratieren", then the Journey editor opens pre-filled with those documents ordered by documentDate, and saving creates a JOURNEY-type Geschichte. (S2)
  • Given a TimelineEvent linked to a Journey, when the event renders, then a "Als Lesereise lesen" link to that Journey is shown; given no linked Journey, then no link. (S3)
  • Given TimelineEvent.description, when edited, then it remains plain text (no rich-text/interlude affordances) — the boundary rule holds.
  • Given the two domains, when reviewing the code, then there is exactly one document picker, one person picker, and one date-precision primitive shared by both editors (no forks). (S1)
  • No entity merge: TimelineEvent and Geschichte/JourneyItem remain distinct tables.

Task checklist (after decisions resolved)

  • Resolve Decisions 1–5 (this issue is needs-discussion until then).
  • S1: cross-check both editors share DocumentMultiSelect / PersonMultiSelect / DatePrecisionField / dateLabel — file follow-ups if either forks.
  • S2: "Als Lesereise kuratieren" action on the event surface → Journey editor with multi-document date-ordered prefill (extend ?documentId= handling per Decision 4).
  • S3: add the chosen reference (Decision 1) + reciprocal links on both detail surfaces; regen API types.
  • Permission alignment per Decision 3 (coordinate #775 / #751).
  • i18n keys (de/en/es) for the new actions/links.
  • Tests: promotion action seeds the Journey editor with the right ordered documents; reference link renders only when present; boundary-rule guard (description stays plain).
  • If S3 adds a column/FK: ADR note (cross-domain reference between timeline/ and geschichte/) + DB-diagram update.

Captured from a design discussion (2026-06-08): the conclusion is keep the entities separate, share the primitives, wire a thin promotion seam — so personal events and reading journeys become two ends of one curation pipeline, not two names for one feature.

**Milestones:** #14 Zeitstrahl — Family Timeline · #13 Lesereisen (Reading Journeys) **Relates to:** #774 (TimelineEvent entity), #775 (TimelineEvent CRUD), #781 (curator event forms), #778 (shared dateLabel helper) · #750 (GeschichteType/JourneyItem), #751 (JourneyItem CRUD), #753 (Journey editor) **Status:** design-the-seam-now, build-post-MVP. Has open decisions — see "Decisions to resolve". ## Why this issue exists Two in-flight epics both wrap **curated documents + narrative + persons**: - **TimelineEvent** (#14, new `timeline/` domain): a time-anchored *cluster* of letters — title, type (PERSONAL/HISTORICAL), `eventDate` + `DatePrecision`, short `description`, ManyToMany `documents`/`persons`. - **Lesereise/Journey** (#13): a `GeschichteType.JOURNEY` subtype of `Geschichte` — an **ordered** sequence of `JourneyItem`s with **interludes (Zwischentexte)**, drag-reorder, rich narrative. This issue records the boundary decision (they are **not** duplicates) and the small seams that make them reinforce each other instead of drifting into two half-overlapping curation tools. ## Verdict: complementary primitives — do NOT merge The timeline design spec already keeps the `timeline/` domain *"deliberately separate from the in-flight Lesereisen/Geschichte work."* That is correct and should stand. | | **TimelineEvent** (#14) | **Lesereise/Journey** (#13) | |---|---|---| | Entity | new `timeline/` domain | `GeschichteType.JOURNEY` (extends `Geschichte`) | | Organizing principle | **time** (`eventDate` + `DatePrecision`) | **reading order** (`JourneyItem.sortOrder`) | | Documents | unordered ManyToMany cluster | ordered `JourneyItem`s | | Narrative | short caption (`description TEXT`) | rich body + **interludes** between letters | | Reader intent | explore *when* / get context | *read* start-to-finish, guided | | Permission | `WRITE_ALL` | `BLOG_WRITE` | A TimelineEvent is a **dot/span in time**; a Lesereise is a **path through pages**. Merging would force an event to carry ordering + interludes it doesn't want, and a journey to carry a date axis it doesn't have (duplication-by-over-generalization). ## Boundary rule (the actual anti-duplication guard) The overlap hides in the event `description`. If it grows into rich text with commentary between letters, we've reinvented a Journey inside a TimelineEvent. > **Rule:** `TimelineEvent.description` stays a short caption (plain `TEXT`, no ordering, no interludes). The moment a curator wants *ordered reading + interludes*, that is a Lesereise — the event **links** to it rather than growing its own narrative. This rule is already reflected in #781 (description = optional plain textarea, not rich text). Keep it. ## Proposed synergies (scoped) ### S1 — Shared UI primitives (already in motion; just don't fork) Both editors descend from `GeschichteEditor`. The Journey editor (#753) = GeschichteEditor + ordering + interludes; the TimelineEvent editor (#781) = GeschichteEditor + date/precision, reusing the **same** `DocumentMultiSelect` + `PersonMultiSelect` and the shared `DatePrecisionField` (#781) / `dateLabel` helper (#778). **Action:** no second picker, no second date primitive — verify both editors consume the shared components rather than re-implementing. (Largely covered by #781/#753; this is a cross-check, not new code.) ### S2 — Promotion seam: TimelineEvent → "Als Lesereise kuratieren" *(primary value)* The timeline naturally *discovers* clusters ("24 Feldpost 1915"). Add an action on a TimelineEvent (event card and/or `/zeitstrahl/events/[id]/edit`) → **opens the Journey editor pre-filled with the event's `documents`, ordered by `documentDate`**. The curator then reorders + adds interludes. Funnel: **discover-in-time (Zeitstrahl) → narrate-as-path (Lesereise)**. - Reuses the existing `?documentId=` prefill idiom (Journey editor / `geschichten/new`), extended to accept multiple ids (e.g. `?documentIds=a,b,c` or a transient seed) in `documentDate` order. ### S3 — Mutual reference (TimelineEvent ↔ Geschichte/Journey) A nullable reference so the two can point at each other: - On a TimelineEvent: "▸ Als Lesereise lesen" when a linked Journey exists. - On a Journey detail: "▸ Im Zeitstrahl zeigen" when it originated from / is tied to an event. - Modelling options in "Decisions to resolve". ### S4 — Lesereise as a timeline marker (discovery) *(optional, later)* A Journey's items carry dates; the assembly endpoint (#777) could surface a Journey as a single marker on `/zeitstrahl` (placed at its earliest item date) → tap → reader. Makes the Zeitstrahl a discovery surface for journeys. Lowest priority of the four. ## Decisions to resolve (needs-discussion) 1. **Reference modelling (S3):** (a) `TimelineEvent.geschichteId` nullable FK → Geschichte; (b) reuse `Geschichte.documents`↔event overlap with no FK (derive the link); (c) a small join table for many-to-many event↔journey. Leaning (a) — simplest, one direction is enough for MVP. 2. **Promotion direction for MVP (S2):** event→journey only (recommended), or also journey→event? Event→journey matches the discover→narrate funnel; the reverse is rarer. 3. **Permission alignment:** timeline curation is `WRITE_ALL`, journeys are `BLOG_WRITE`. If the same people curate both, align the gates (likely both `BLOG_WRITE`, or document why they differ). Cross-epic decision — owners of #775 and #751 should agree. 4. **Multi-id prefill:** extend the Journey editor's `?documentId=` to a multi-id seed in date order, vs. a one-shot "create journey from these N documents" server action. Affects #753. 5. **S4 in or out of MVP:** recommend **out** (post-MVP); confirm. ## Scope / non-goals - **In:** the boundary rule, S1 cross-check, S2 promotion seam, S3 mutual reference (one direction). - **Out (explicit non-goals):** merging the entities; adding ordering/interludes to `TimelineEvent`; adding a date axis or `DatePrecision` to `Geschichte`; S4 (deferred). Do **not** generalize a shared "curated collection" super-entity. ## Dependencies Blocked until both sides have entities + editors: #774, #775, #781 (timeline) and #750, #751, #753 (lesereisen). Best picked up after both editors exist so the promotion seam targets real routes. ## Acceptance criteria (Given-When-Then) - *Given* a curator on a TimelineEvent with linked documents, *when* they choose "Als Lesereise kuratieren", *then* the Journey editor opens pre-filled with those documents ordered by `documentDate`, and saving creates a `JOURNEY`-type Geschichte. (S2) - *Given* a TimelineEvent linked to a Journey, *when* the event renders, *then* a "Als Lesereise lesen" link to that Journey is shown; *given* no linked Journey, *then* no link. (S3) - *Given* `TimelineEvent.description`, *when* edited, *then* it remains plain text (no rich-text/interlude affordances) — the boundary rule holds. - *Given* the two domains, *when* reviewing the code, *then* there is exactly one document picker, one person picker, and one date-precision primitive shared by both editors (no forks). (S1) - No entity merge: `TimelineEvent` and `Geschichte`/`JourneyItem` remain distinct tables. ## Task checklist (after decisions resolved) - [ ] Resolve Decisions 1–5 (this issue is `needs-discussion` until then). - [ ] S1: cross-check both editors share `DocumentMultiSelect` / `PersonMultiSelect` / `DatePrecisionField` / `dateLabel` — file follow-ups if either forks. - [ ] S2: "Als Lesereise kuratieren" action on the event surface → Journey editor with multi-document date-ordered prefill (extend `?documentId=` handling per Decision 4). - [ ] S3: add the chosen reference (Decision 1) + reciprocal links on both detail surfaces; regen API types. - [ ] Permission alignment per Decision 3 (coordinate #775 / #751). - [ ] i18n keys (de/en/es) for the new actions/links. - [ ] Tests: promotion action seeds the Journey editor with the right ordered documents; reference link renders only when present; boundary-rule guard (description stays plain). - [ ] If S3 adds a column/FK: ADR note (cross-domain reference between `timeline/` and `geschichte/`) + DB-diagram update. > Captured from a design discussion (2026-06-08): the conclusion is *keep the entities separate, share the primitives, wire a thin promotion seam* — so personal events and reading journeys become two ends of one curation pipeline, not two names for one feature.
marcel added this to the Zeitstrahl — Family Timeline milestone 2026-06-08 12:03:44 +02:00
marcel added the P3-laterfeatureneeds-discussionui labels 2026-06-08 12:03:50 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#785