feat(t-paliad-173): SmartTimeline Slice 2 backend — projection + anchor + skip + sequence guard

Slice 2 of the SmartTimeline (docs/design-smart-timeline-2026-05-08.md
§6 + §9 + §10) bundled with m/paliad#31's layered requirements:

Migration 076:
- appointments.deadline_rule_id nullable FK to deadline_rules + partial idx
- deadlines.source CHECK widened to include 'anchor' (alongside existing
  'manual','fristenrechner','rule','import').

ProjectionService (extended):
- Wires FristenrechnerService + DeadlineRuleService.
- For() now emits Kind="projected" rows for any rule lacking a matching
  paliad.deadlines.rule_id / appointments.deadline_rule_id row, with
  Status in {predicted | predicted_overdue | court_set}.
- Lookahead cap (default 7, override via ?lookahead=N, max 50): future
  predicted rows beyond N are dropped; predicted_overdue + court_set
  rows are exempt from the cap (#31 layer 1).
- Dependency annotations DependsOnRuleCode/Date/Name on every row that
  carries a DeadlineRuleID, walked from the rule's parent_id chain
  (#31 layer 2). Date prefers actuals over projections.
- AnchorOverrides built from completed deadlines (completed_at /
  status='completed') + appointments tied via deadline_rule_id.
- triggerDate derives from the proceeding's root rule's anchor when
  present, else today() as placeholder.

Anchor write path (POST /api/projects/{id}/timeline/anchor):
- Sequence guard: if rule.parent_id has no anchored actual, return
  409 predecessor_missing with the missing rule's code/name DE+EN +
  pre-formatted bilingual messages so the frontend can render an
  inline error with a "Stattdessen <predecessor> erfassen" link
  (#31 layer 3, no confirm-and-write override in v1).
- kind dispatch: rules with event_type IN ('hearing','decision','order')
  write paliad.appointments with deadline_rule_id; everything else
  writes paliad.deadlines with source='anchor', status='completed',
  completed_at=actual_date.
- Idempotent: existing (project_id, rule_id) row PATCHes instead of
  inserting (race-safe per design §13).

Skip write path (POST /api/projects/{id}/timeline/skip):
- Writes paliad.project_events with event_type='rule_skipped' +
  metadata.rule_code; subsequent reads drop the matching projected
  row from the cascade (§6.4).

Handlers expose projection meta via X-Projection-{Has,Total,Shown,Overdue,Lookahead}
headers so the wire shape stays []TimelineEvent (frozen since Slice 1).
This commit is contained in:
m
2026-05-09 15:33:20 +02:00
parent 335be29b23
commit 85d7dd497c
7 changed files with 1289 additions and 103 deletions

View File

@@ -216,11 +216,15 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc
protected.HandleFunc("PATCH /api/projects/{id}", handleUpdateProject)
protected.HandleFunc("DELETE /api/projects/{id}", handleDeleteProject)
protected.HandleFunc("GET /api/projects/{id}/events", handleListProjectEvents)
// t-paliad-171 — SmartTimeline (Verlauf-tab redesign).
// /timeline returns the merged timeline (Slice 1 = actuals only).
// t-paliad-171 / t-paliad-173 — SmartTimeline (Verlauf-tab redesign).
// /timeline returns the merged timeline (actuals + Slice 2 projections).
// /timeline/milestone is the "Eigener Meilenstein" write path.
// /timeline/anchor is the click-to-anchor write (Slice 2).
// /timeline/skip is the "ist nicht eingetreten" decision (§6.4).
protected.HandleFunc("GET /api/projects/{id}/timeline", handleGetProjectTimeline)
protected.HandleFunc("POST /api/projects/{id}/timeline/milestone", handleCreateProjectTimelineMilestone)
protected.HandleFunc("POST /api/projects/{id}/timeline/anchor", handleProjectTimelineAnchor)
protected.HandleFunc("POST /api/projects/{id}/timeline/skip", handleProjectTimelineSkip)
protected.HandleFunc("GET /api/projects/{id}/children", handleListProjectChildren)
protected.HandleFunc("GET /api/projects/{id}/tree", handleGetProjectTree)
protected.HandleFunc("POST /api/projects/{id}/pin", handlePinProject)