Timeline: TimelineEvent entity + Flyway migration (#774) #816

Open
marcel wants to merge 11 commits from feat/issue-774-timeline-event-entity into main
6 changed files with 90 additions and 2 deletions
Showing only changes of commit 1226bd0a07 - Show all commits

View File

@@ -95,6 +95,7 @@ backend/src/main/java/org/raddatz/familienarchiv/
│ └── relationship/ PersonRelationship sub-domain
├── security/ SecurityConfig, Permission, @RequirePermission, PermissionAspect
├── tag/ Tag domain
├── timeline/ Timeline (Zeitstrahl) domain — TimelineEvent, EventType, TimelineEventRepository
└── user/ User domain — AppUser, UserGroup, UserService
```
@@ -115,6 +116,7 @@ backend/src/main/java/org/raddatz/familienarchiv/
| `UserGroup` | `user_groups` | Has a `Set<String> permissions` |
| `Geschichte` | `geschichten` | `GeschichteType` (`STORY`/`JOURNEY`); ManyToMany `persons` (Person); OneToMany `items` (JourneyItem) |
| `JourneyItem` | `journey_items` | ManyToOne `geschichte` (Geschichte, ON DELETE CASCADE); ManyToOne `document` (Document, ON DELETE SET NULL); `position`, optional `note` |
| `TimelineEvent` | `timeline_events` | `EventType` (`PERSONAL`/`HISTORICAL`); ManyToMany `persons` (Person) + `documents` (Document), both join FKs ON DELETE CASCADE; `DatePrecision` date block; `@Version` + NOT NULL `createdBy`/`updatedBy` audit trail |
**`DocumentStatus` lifecycle:** `PLACEHOLDER → UPLOADED → TRANSCRIBED → REVIEWED → ARCHIVED`

View File

@@ -42,6 +42,7 @@ src/main/java/org/raddatz/familienarchiv/
│ └── relationship/ # PersonRelationship sub-domain
├── security/ # SecurityConfig, Permission, @RequirePermission, PermissionAspect
├── tag/ # Tag domain — Tag, TagService, TagController
├── timeline/ # Timeline (Zeitstrahl) domain — TimelineEvent, EventType, TimelineEventRepository
└── user/ # User domain — AppUser, UserGroup, UserService
```
@@ -67,6 +68,7 @@ For per-domain ownership and public surface, see each domain's `README.md`.
| `Comment` | `document_comments` | Threaded comments with mentions |
| `Notification` | `notifications` | User notification feed |
| `OcrJob` / `OcrJobDocument` | `ocr_jobs`, `ocr_job_documents` | Batch OCR job tracking |
| `TimelineEvent` | `timeline_events` | Curated Zeitstrahl event; ManyToMany persons + documents (join FKs ON DELETE CASCADE); `@Version` + NOT NULL createdBy/updatedBy |
**`DocumentStatus` lifecycle:** `PLACEHOLDER → UPLOADED → TRANSCRIBED → REVIEWED → ARCHIVED`

View File

@@ -164,6 +164,12 @@ _Not to be confused with a document item's optional note_ — a document item's
**Lesereise** `[user-facing]` — a curated reading journey through a sequence of family documents, optionally annotated with editorial notes. Implemented as a `Geschichte` with `type=JOURNEY`. The reader UI (follow-on issue) renders items as a sequential reading experience.
**TimelineEvent** (`TimelineEvent`, table `timeline_events`) `[internal]` — a curated event on the family timeline (*Zeitstrahl*), authored by a curator rather than OCR-derived. Carries the same date block as a `Document` (`eventDate` + `precision` + nullable `eventDateEnd`) so events and letters render through one path, plus a `title`, optional `description`, an `EventType`, and `ManyToMany` links to the `Person`s it involves and the `Document`s that support it (both join FKs `ON DELETE CASCADE`). Diverges from `Document` with an optimistic-lock `@Version` and a NOT NULL `createdBy`/`updatedBy` audit trail (bare UUIDs, no FK to `app_users`) for the multi-curator edit flow. Two DB CHECKs: `event_date_end` is non-null **iff** precision is `RANGE` (a strict biconditional, intentionally tighter than `Document`'s open-ended ranges), and `precision` is never `UNKNOWN` (a curated event always has at least a year; `SEASON`/`APPROX` stay legal). See ADR-040.
**EventType** (`EventType`) `[user-facing]` — the kind of a `TimelineEvent`: `PERSONAL` (a family event, rendered with the family accent) or `HISTORICAL` (world/historical context, rendered with a muted world accent). The string value names are a stable frontend styling contract — renaming requires a coordinated frontend change (ADR-040).
**Zeitstrahl** `[user-facing]` — the family timeline view, rendering curated `TimelineEvent`s (and, in later issues, derived life-events) chronologically. The milestone home of the `timeline` domain.
**Notification** (`Notification`) — an in-app message delivered to an `AppUser`. No email or SMS delivery exists today. Delivered via Server-Sent Events (`SseEmitterRegistry`) and persisted in the `notifications` table.
**Audit log** (`AuditLog`, table `audit_log`) — an append-only event store recording domain-level activity (document edits, user actions, etc.). Append-only by application convention; a `REVOKE UPDATE, DELETE` is attempted at the DB layer (see migrations V46, V47) but is a no-op if the application role is the table owner in PostgreSQL. Do not rely on DB-enforced immutability — the constraint is application-layer only.

View File

@@ -0,0 +1,24 @@
@startuml
!include <C4/C4_Component>
title Component Diagram: API Backend — Timeline (Zeitstrahl)
ContainerDb(db, "PostgreSQL", "PostgreSQL 16")
System_Boundary(backend, "API Backend (Spring Boot)") {
Component(timelineRepo, "TimelineEventRepository", "Spring Data JPA", "Reads and writes TimelineEvent rows and their persons/documents join tables (timeline_event_persons, timeline_event_documents). Issue #774 ships the repository empty; the per-person filter query lands in a later issue.")
Component(timelineSvc, "TimelineEventService", "Spring Service (planned, issue 3)", "Will own curated-event CRUD: assemble TimelineEventView/Summary inside the transaction (lazy ManyToMany + open-in-view=false, per ADR-036/ADR-040), populate createdBy/updatedBy from the session principal, and translate optimistic-lock conflicts to DomainException.conflict.")
Component(timelineCtrl, "TimelineEventController", "Spring MVC (planned, issue 3)", "Will expose /api/timeline reads (READ_ALL) and writes (WRITE_ALL). createdBy/updatedBy are never bound from request bodies (CWE-639).")
}
System_Ext(documentDomain, "Document domain", "Provides DatePrecision (shared value type) and Document references for linked letters")
System_Ext(personDomain, "Person domain", "Provides Person references for who an event involves")
Rel(timelineRepo, db, "SQL queries", "JDBC")
Rel(timelineSvc, timelineRepo, "Reads / writes events (planned)")
Rel(timelineCtrl, timelineSvc, "Delegates to (planned)")
Rel(timelineRepo, personDomain, "References persons via join table")
Rel(timelineRepo, documentDomain, "References documents via join table")
@enduml

View File

@@ -1,6 +1,6 @@
@startuml db-orm
' Schema source: Flyway V1V76 (excl. V37, V43 — intentionally removed)
' Schema as of: V76 (2026-06-12)
' Schema source: Flyway V1V77 (excl. V37, V43 — intentionally removed)
' Schema as of: V77 (2026-06-12)
' ⚠ This is a versioned snapshot. Update when the schema changes significantly.
hide circle
@@ -386,6 +386,39 @@ package "Supporting" {
}
}
' ── Timeline (Zeitstrahl) ──
package "Timeline" {
entity timeline_events {
id : UUID <<PK>>
--
title : VARCHAR(255) NOT NULL
type : VARCHAR(16) NOT NULL
event_date : DATE NOT NULL
date_precision : VARCHAR(16) NOT NULL DEFAULT 'YEAR'
event_date_end : DATE
description : TEXT
created_by : UUID NOT NULL
created_at : TIMESTAMP
updated_by : UUID NOT NULL
updated_at : TIMESTAMP
version : BIGINT
==
CHECK ((date_precision = 'RANGE') = (event_date_end IS NOT NULL))
CHECK (date_precision <> 'UNKNOWN')
}
entity timeline_event_persons {
timeline_event_id : UUID <<FK>>
person_id : UUID <<FK>>
}
entity timeline_event_documents {
timeline_event_id : UUID <<FK>>
document_id : UUID <<FK>>
}
}
' Auth relationships
app_users_groups }o--|| app_users : app_user_id
app_users_groups }o--|| user_groups : group_id
@@ -449,4 +482,10 @@ geschichten_persons }o--|| persons : person_id
journey_items }o--|| geschichten : geschichte_id (ON DELETE CASCADE)
journey_items }o--o| documents : document_id (ON DELETE SET NULL)
' Timeline relationships
timeline_event_persons }o--|| timeline_events : timeline_event_id (ON DELETE CASCADE)
timeline_event_persons }o--|| persons : person_id (ON DELETE CASCADE)
timeline_event_documents }o--|| timeline_events : timeline_event_id (ON DELETE CASCADE)
timeline_event_documents }o--|| documents : document_id (ON DELETE CASCADE)
@enduml

View File

@@ -6,6 +6,7 @@
' precision/attribution fields); no new FK relationships, so this diagram is unchanged.
' Note: V76 swaps persons.birth_year/death_year for birth_date/death_date +
' precision columns; columns only, no new FK relationships, diagram unchanged.
' Note: V77 adds the timeline_events table + two join tables (Timeline package below).
hide circle
skinparam linetype ortho
@@ -71,6 +72,13 @@ package "Supporting" {
entity journey_items
}
' ── Timeline (Zeitstrahl) ──
package "Timeline" {
entity timeline_events
entity timeline_event_persons
entity timeline_event_documents
}
' Auth relationships
app_users_groups }o--|| app_users : app_user_id
app_users_groups }o--|| user_groups : group_id
@@ -136,4 +144,11 @@ journey_items }o--o| documents : document_id (ON DELETE SET NULL)
note right of journey_items : partial UNIQUE (geschichte_id, document_id)\nWHERE document_id IS NOT NULL (V74)
note right of geschichten : CHECK length(body) <= 4000\nfor type = JOURNEY (V75)
' Timeline relationships (V77)
timeline_event_persons }o--|| timeline_events : timeline_event_id (ON DELETE CASCADE)
timeline_event_persons }o--|| persons : person_id (ON DELETE CASCADE)
timeline_event_documents }o--|| timeline_events : timeline_event_id (ON DELETE CASCADE)
timeline_event_documents }o--|| documents : document_id (ON DELETE CASCADE)
note right of timeline_events : CHECK event_date_end non-null IFF RANGE\nCHECK date_precision <> 'UNKNOWN' (V77)
@enduml