# Design — SmartTimeline (Verlauf-tab redesign)

**Author:** lagrange (inventor)
**Date:** 2026-05-08
**Task:** t-paliad-169
**Status:** READY FOR REVIEW — m gates inventor → coder transition.

---

## 0. Premises verified live (before designing)

Before anchoring the design, I checked the live state — CLAUDE.md / memory / issue body can drift, the live system can't.

- **Verlauf today** is `frontend/src/projects-detail.tsx:74-101` — `<ul className="entity-events" id="project-events-list">` rendered from `paliad.project_events` via `loadEvents(id)` at `client/projects-detail.ts:305`. Pure audit log: `event_type` distribution in prod is 100 % administrative — `deadline_completed/updated/created/...`, `note_created`, `appointment_*`, `checklist_*`, `project_type_changed`, `our_side_changed`, `deadlines_imported`. No "future-tense" or "off-script" events surface anywhere on the project page today.
- **Projection logic** lives in `internal/services/fristenrechner.go:Calculate(ctx, proceedingCode, triggerDateStr, opts CalcOptions)` returning a `UIResponse{Deadlines []UIDeadline}` keyed by `rule_code`. `CalcOptions.AnchorOverrides map[string]string` lets callers replace any rule's date and downstream rules re-anchor — already the load-bearing primitive for "actual dates anchor downstream projections" (t-paliad-131 Phase A).
- **`paliad.deadline_rules`** carries 172 active rules across 19 fristenrechner proceeding types (UPC×8, DE×5, EPA×2, EP×1, DPMA×3). `condition_flag text[]` already drives counterclaim cross-flows: `with_ccr` enables 7 UPC_INF cross-flow rules (Defence-to-CCR R.29.a, Application to amend R.30.1, Defence to App-to-amend R.32.1, Reply to Defence-to-CCR R.29.d, Rejoinder R.29.e, +2). `with_amend` / `with_cci` work on UPC_REV.
- **`paliad.projects.our_side`** column exists (added in t-paliad-164) but is **null on every live row today**. The CCR perspective-flip the cascade implements via Determinator B1 (t-paliad-167) is not yet exercised by real data.
- **CCR is not a separate project today.** It's a flag (`with_ccr=true`) on a parent UPC_INF project. m's vision asks us to revisit that.
- **FilterBar** (`frontend/src/client/filter-bar/`, riemann's t-paliad-163 Phase 1) ships with axis stubs `deadline_event_type` + `project_event_kind` already wired into `BarState` and `AxisKey` — Phase 2 is supposed to fill them in. The SmartTimeline's facet set is exactly the kind of thing those stubs were left pending for.
- **Project hierarchy in prod** is the canonical 4-level shape: Client (`Siemens AG`) → Litigation (`Siemens ./. Huawei`) → Patent (`EP3456789`) → Case (`UPC-CFI München — Klage Siemens ./. Huawei`). 11 projects total.
- **t-paliad-168 deliverable 3 is dropped** per task brief — there will be no separate Verfahrensablauf-as-its-own-tab on the project page. The wizard's projection logic is the SmartTimeline's future-skeleton feeder.

---

## 1. Vision + scope

m's vision (verbatim 2026-05-08 23:02):

> The Verlauf tab inside the case should hold past + future events. If we know the proceeding type, there is a timeline. We adapt the Verfahrensablauf logic and fix dates for things when they happened. A smart timeline. If a counterclaim is filed, that is also included. Hold it flexible — add events regardless of whether they fit the normal course.

The **SmartTimeline** is one composed view that answers *"what has happened in this matter, what is happening now, and what is on the standard road from here"*. Three time-zones in one widget:

| Zone | What it shows | Data source |
|---|---|---|
| **Past** | Filings, decisions, appointments, audit milestones — all dated, anchored to reality | `paliad.deadlines` (status=`done`) ∪ `paliad.appointments` (start_at < today) ∪ `paliad.project_events` (selected `timeline_kind`) |
| **Now** | Open deadlines + appointments today | same tables, today-bracket |
| **Future (predicted)** | Standard-course rules from `deadline_rules` projected forward, faded — only those without an actual `paliad.deadlines` row yet | `fristenrechner.Calculate` against project's proceeding type + trigger anchor |
| **Future (off-script)** | User-added events that don't fit the standard tree (counterclaim filed, ad-hoc Anhörung, party amendment) | `paliad.deadlines` with `source='off_script'` ∪ child counterclaim sub-project's actuals ∪ `project_events` with `timeline_kind` |

### What changes

- The `tab=history` panel on `/projects/{id}` becomes a SmartTimeline component that renders all four zones in one column.
- The audit-only Verlauf view does not disappear — it survives as a "Audit-Log" sub-toggle inside the SmartTimeline ("Alle Audit-Events anzeigen") and on the existing `/admin/audit-log` page (t-paliad-071).
- The existing FilterBar primitive grows two facets (`timeline_track`, `timeline_status`) and re-uses three (`time`, `personal_only`, `deadline_event_type`).

### What stays

- Step 2 third-card + sidebar entry from t-paliad-168 are unaffected — the standalone Verfahrensablauf wizard at `/tools/fristenrechner` remains a knowledge-platform tool.
- `paliad.project_events` keeps its full audit-log role for `/admin/audit-log`.
- `paliad.deadlines` + `paliad.appointments` schemas don't migrate (only one optional column added; details in §2).
- The existing "Inkl. Unterprojekte" toggle on the project page stays — the SmartTimeline reads child events through it.

### Out of scope (v1)

- Horizontal-Gantt rendering. We pick a vertical timeline; Gantt is a future shape (t-paliad-144 substrate already supports `shape` switching, so adding a Gantt shape is later, not now).
- Outlook/Exchange sync. CalDAV stays the only sync path.
- Cross-matter timelines (e.g. "everything happening on EP3456789 across Siemens ./. Huawei AND any related opposition"). The patent-level aggregation in §5 is a step in that direction but cross-matter view is a separate task.
- Rendering documents (Schriftsätze) on the timeline. That's the t-paliad-17 Incoming-Submission workflow, separate.

---

## 2. Data model

**Recommendation: virtual view, ONE optional column.** No new top-level table for v1. The four zones above are computed at read time from the existing tables. The single schema change is a nullable `timeline_kind text` column on `paliad.project_events` so a subset of audit rows can opt into surfacing as timeline content.

### 2.1 Why no new `timeline_events` table

A first-instinct design would materialise a new `paliad.timeline_events` table with columns `(project_id, kind, date, title, status, source_track, rule_code?, actual_deadline_id?, …)`. I recommend against it for v1:

1. **Three of the four zones already have authoritative tables.** `paliad.deadlines` is the source-of-truth for legal deadlines (with completion + approval state); `paliad.appointments` for hearings + court dates; `paliad.project_events` for audit. Forcing a copy into `timeline_events` creates a sync problem on every mutation.
2. **The future-projected zone is a function of proceeding-type + trigger date + actual anchors** — not stored data. Materialising it would require invalidation on every `paliad.deadlines` change. Cheaper to recompute per request: 19 proceeding types × at most ~15 rules = ~285 ms with cold pg cache, well under the page-render budget. Re-uses the cached `FristenrechnerService` (already memoised per request via service instantiation).
3. **t-paliad-144 set the precedent** that ViewService composes per request without materialising. The SmartTimeline is a project-scoped instance of the same pattern.

If load testing later shows the projection cost matters, we materialise into a `paliad.projected_timeline_cache` table indexed by (project_id, rule_code) — but design that when load shows it, not now.

### 2.2 The one column added

```sql
-- migration NNN_project_events_timeline_kind.up.sql
ALTER TABLE paliad.project_events
  ADD COLUMN timeline_kind text NULL;

-- nullable + no CHECK — enum lives in code (services/projection_service.go).
-- Value space (v1):
--   'milestone'        — a structural event worth pinning to the timeline
--                        (counterclaim_filed, third_party_intervened,
--                         party_amendment, our_side_changed, scope_change)
--   'custom_milestone' — free-text user-added event
--   NULL               — audit only (default, all existing rows)

CREATE INDEX project_events_timeline_kind_idx
  ON paliad.project_events (project_id, timeline_kind)
  WHERE timeline_kind IS NOT NULL;
```

Existing event types stay `NULL` — they remain audit-only and don't clutter the timeline. New write paths (counterclaim-link, off-script milestone) set the column on insert.

### 2.3 The discriminated `TimelineEvent` shape

Composed in `internal/services/projection_service.go` (new). One Go struct, one TS mirror. Frontend renders without knowing where each row came from:

```go
type TimelineEvent struct {
    Kind       string     // "deadline" | "appointment" | "milestone" | "projected"
    Status     string     // "done" | "open" | "overdue" | "court_set" | "predicted" | "off_script"
    Track      string     // "parent" | "counterclaim" | "child:<project_id>" | "off_script"
    Date       *time.Time // nil = undated (court-set + counterclaim-pending)

    Title      string
    Description string
    RuleCode   string     // empty when not deadline-rule-derived

    // Provenance — exactly one is non-nil for actual rows; both nil for projected.
    DeadlineID    *uuid.UUID
    AppointmentID *uuid.UUID
    ProjectEventID *uuid.UUID

    // For projected rows (Kind=="projected") — the rule it came from, for
    // the click-to-anchor affordance (§6).
    DeadlineRuleID *uuid.UUID
    DeadlineRuleParty string  // 'claimant' | 'defendant' | 'court' | 'both'

    // For child-track rows — the sub-project this event belongs to.
    SubProjectID    *uuid.UUID
    SubProjectTitle string
}
```

### 2.4 Read path

```
GET /api/projects/{id}/timeline?
    from=...&to=...&direct_only=true|false&
    tracks=parent,counterclaim,...&kinds=deadline,appointment,projected,...
```

The handler:
1. Calls `ProjectionService.For(ctx, projectID, opts)` which:
   - Loads the project (proceeding_type_id, our_side, parent chain).
   - Loads child counterclaim sub-projects (if any — see §4).
   - Loads `paliad.deadlines` (project_id IN [self, child counterclaims]) → emits Kind=deadline rows.
   - Loads `paliad.appointments` (same) → emits Kind=appointment rows.
   - Loads `paliad.project_events WHERE timeline_kind IS NOT NULL` → emits Kind=milestone rows.
   - For each (project, child) with a proceeding_type_id, calls `FristenrechnerService.Calculate` with `AnchorOverrides` derived from completed actuals → emits Kind=projected rows for any rule that does **not** have a matching `paliad.deadlines.rule_id` row.
   - Sorts by Date ASC, undated rows last (with secondary sort on rule sequence_order so undated court-set rows preserve the standard course's order).

Visibility is inherited via existing `visibilityPredicate` on each underlying service — no new RLS surface to design.

### 2.5 What does NOT need to change

- `paliad.deadlines` schema — unchanged. (The existing `original_due_date`, `source`, and the AnchorOverrides plumbing already cover "actual date anchors downstream", §6.)
- `paliad.appointments` — unchanged.
- `paliad.deadline_rules` — unchanged. The existing `condition_flag text[]` keeps doing its job.
- `paliad.projects` — unchanged. (See §4 for the counterclaim sub-project shape: it uses existing columns.)

---

## 3. UI mockup — three states

The SmartTimeline replaces the current `<ul className="entity-events">` block (~30 lines of TSX) with a vertically-flowing two-column timeline:

- Left column: date (or "Datum offen" placeholder).
- Right column: stacked card per event with a status icon, title, kind chip, and (for actuals) a deep-link to `/deadlines/{id}` etc. Same `.entity-event` row contract as today (cf. CLAUDE.md whole-card click rule), no `::before` overlay.

A horizontal "**Heute →**" rule separates past from future. Past goes below (most-recent first), future above (chronological). Today's events sit on the rule.

### 3.1 State A — empty / no proceeding type set

```
┌──────────────────────────────────────────────────────────────┐
│ SmartTimeline                          [Filter ▼]  [+ Eintrag] │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   Noch keine Ereignisse erfasst.                             │
│                                                              │
│   Setze einen Verfahrenstyp im Projekt-Header, um den        │
│   Standardverlauf als Vorhersage zu sehen, oder lege         │
│   einen Eintrag manuell an.                                  │
│                                                              │
│   [+ Frist anlegen]   [+ Termin anlegen]   [+ Meilenstein]    │
│                                                              │
└──────────────────────────────────────────────────────────────┘
```

The empty state actively guides toward the two unlocks: setting a proceeding type (enables future-projection) or adding manual events (works without one).

### 3.2 State B — UPC_INF, infringement-only

```
┌──────────────────────────────────────────────────────────────┐
│ SmartTimeline   Verfahrenstyp: UPC-Verletzung   [Filter ▼]   │
├──────────────────────────────────────────────────────────────┤
│  Zukunft (vorhergesagt)                                      │
│  ─────────────────────────────                               │
│   2027-02-20  ░ Hauptverhandlung                             │
│               ░ wird vom Gericht bestimmt   [Datum setzen]   │
│   ─                                                          │
│   2026-12-02  ░ Duplik (RoP.029.c)             [voraussichtlich]│
│   2026-11-02  ░ Replik (RoP.029.b)             [voraussichtlich]│
│   2026-08-31  ░ Klageerwiderung (RoP.023)      [voraussichtlich]│
│                                                              │
│ ━━━━━━━━━━━━━━━━━━━━ Heute (2026-05-08) ━━━━━━━━━━━━━━━━━━━━ │
│                                                              │
│  Vergangenheit                                               │
│  ─────────────────────────────                               │
│   2026-04-29  ✓ Klageschrift zugestellt (Anker)              │
│   2026-04-25  ✓ Akte angelegt (Audit)                        │
└──────────────────────────────────────────────────────────────┘
```

- `░` (faded) = projected, `✓` = done, `!` = overdue (red), `…` = open (amber), `▢` = court-set (dashed border).
- "Datum setzen" on the Hauptverhandlung row is the click-to-anchor affordance (§6).
- "voraussichtlich" pill is the projected-status visual; tooltip explains "Anhand des Standardverlaufs aus dem Fristenrechner berechnet".
- Filter chip selector reveals the FilterBar primitive directly above the list (collapsed by default to reduce noise on first load — same affordance riemann shipped on /inbox).

### 3.3 State C — UPC_INF + Counterclaim (CCR-Subprojekt)

```
┌──────────────────────────────────────────────────────────────────────────────┐
│ SmartTimeline   Verfahrenstyp: UPC-Verletzung   [Track ▼ Beide]   [Filter ▼] │
├──────────────────────────────────────────────────────────────────────────────┤
│ Verletzung (Klägerseite)              ┊  Widerklage (Beklagtenseite, CCR)    │
│ ──────────────────────────────────────┊──────────────────────────────────────│
│  Zukunft (vorhergesagt)                                                      │
│   2027-02-20  ░ Hauptverhandlung      ┊                                      │
│                 [Datum setzen]        ┊                                      │
│   2027-01-29  ░ Rejoinder R.29.e      ┊  2026-12-29  ░ Rejoinder R.32.3      │
│   2026-12-29  ░ Reply to Defence-CCR  ┊                                      │
│   2026-11-29  ░ Defence to App-amend  ┊  2026-11-29  ░ Reply to Defence-amend│
│   2026-10-31  ░ Defence to CCR (R.29a)┊  2026-09-30  ░ Defence to amend      │
│   2026-08-31  ░ Klageerwiderung mit CCR┊                                     │
│                                       ┊                                      │
│ ━━━━━━━━━━━━━━━━━━━━ Heute (2026-05-08) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│                                       ┊                                      │
│  Vergangenheit                        ┊                                      │
│   2026-04-29  ✓ Klageschrift zugestellt┊  ⊕ Widerklage angekündigt           │
│                                       ┊     (off-script, 2026-05-02)         │
│   2026-04-25  ✓ Akte angelegt         ┊                                      │
└──────────────────────────────────────────────────────────────────────────────┘
```

- Two parallel tracks — left is the parent infringement, right is the linked counterclaim sub-project (see §4).
- `[Track ▼]` chip toggles between "Beide" (default when a CCR sub-project exists), "Nur Verletzung", "Nur Widerklage".
- "⊕" marks an off-script milestone (the counterclaim was *announced* before being formally filed — a `project_events` row with `timeline_kind='custom_milestone'`).
- Mobile: stacks vertically with collapsible per-track headers.

---

## 4. Counterclaim shape — sub-project, defended

m's framing offered two shapes. Inventor recommendation: **sub-project**. Trade-off explicit.

### 4.1 The choice

| | **Sub-project (recommended)** | **Same-project, parallel proceeding-overlay** |
|---|---|---|
| Project rows | One per proceeding (parent INF + child CCR) | One project, two proceeding-types attached |
| `our_side` flip | Independent on the child (parent: claimant; child: defendant in CCR-on-validity, claimant on CCR-of-infringement) | Needs a "perspective per proceeding" sub-table |
| Determinator routing (t-paliad-167) | Existing — child gets its own cascade | Needs proceeding-aware routing inside one project |
| Project tree (t-paliad-149) | Naturally appears as a nested node | Same-row, no tree change |
| Dashboard per-project counts | Each gets its own count | Mixing — needs new "by-proceeding" aggregator |
| Visibility / RLS | Inherits `can_see_project` cascade | Same |
| CCR Number from CMS | Stored on child's `case_number` | Stored on parent in a new `case_numbers jsonb` |
| New schema | None (uses existing project + parent_id) | New `project_proceedings` join table |

### 4.2 Why sub-project

- **Cheap.** Zero schema migration. The hierarchy already supports arbitrary nesting (4 types: client / litigation / patent / case — but `parent_id` is type-agnostic).
- **Consistent with the data we just built.** t-paliad-164 our_side, t-paliad-149 project tree, t-paliad-167 Determinator cascade, t-paliad-168 deadline-rule jurisdiction defaults all assume "one project = one proceeding perspective". Counterclaim being a sub-project just means we keep that assumption.
- **CCR Number.** The counterclaim has its own CCR number in the UPC CMS — which means it is in fact a separate proceeding artifact, not just a phase of the parent. Modeling it as a separate project row with its own `case_number` reflects reality. The "case-complex-wise" closeness m asks about is the parent_id link, not collapsing them into one row.
- **Independent timeline math.** UPC R.49(2) puts CCI / app-to-amend "as part of" Defence to revocation — but that just means zero-duration filed-with-parent. The downstream re-anchoring is independent in each tree.

### 4.3 The link

A new optional FK on `paliad.projects`:

```sql
-- migration NNN_projects_counterclaim_of.up.sql
ALTER TABLE paliad.projects
  ADD COLUMN counterclaim_of uuid NULL
    REFERENCES paliad.projects(id) ON DELETE SET NULL;

CREATE INDEX projects_counterclaim_of_idx
  ON paliad.projects (counterclaim_of)
  WHERE counterclaim_of IS NOT NULL;

-- A project can be EITHER a parent (counterclaim_of IS NULL) OR a
-- counterclaim against another project (counterclaim_of points at it),
-- but not both. Enforced by a CHECK on the union of FKs (see §10).
```

`parent_id` keeps the standard hierarchy (the counterclaim child still lives under the same patent / litigation tree). `counterclaim_of` is an *additional* relation expressing "this project is the CCR against project X". The two are both set on a counterclaim sub-project.

### 4.4 Creating a counterclaim from the timeline

The "+ Eintrag" button on the parent's SmartTimeline opens a typed-add modal (§7). Picking type=`Counterclaim` (UPC) creates a child project with:

- `parent_id` = parent's parent (so CCR appears as a sibling under the patent, not a grandchild — debatable; see §11 Q4).
- `counterclaim_of` = parent project id.
- `proceeding_type_id` = `UPC_REV` (CCR-on-validity is the standard case; UPC_CCI is the rarer R.49.2.b path).
- `our_side` = inverted from parent (parent claimant → child defendant, parent defendant → child claimant).
- `title` = `<patent> — Widerklage (CCR)` auto-suggested.

The same flow applies to `case_amend` (UPC R.30 application to amend) — a separate child sub-project. *Whether to model R.30 as a child project or as a flag on the parent is open: amendments are usually just a flag in our existing model. Default v1 = stay as flag, do **not** create a sub-project for application-to-amend; only formal counterclaims (CCR / CCI) get sub-projects.*

### 4.5 What the parent's SmartTimeline shows for the child

When `counterclaim_of` exists pointing at this project, the SmartTimeline renders a parallel right-track with the child's events (limited to `kind IN ('deadline','appointment','milestone')` — child's projected rows are also included). User can collapse/hide the child track via the `[Track ▼]` chip.

The child's own SmartTimeline shows its own events as the primary track plus the parent as a left-side faded-context track (so the lawyer working on the CCR can see what's happening on the main proceeding without leaving the page).

---

## 5. Parent-node aggregation rule

What does the SmartTimeline render at higher levels of the project hierarchy? The four levels we have today:

### 5.1 Per-level rendering

| Level | Default render | Why |
|---|---|---|
| **Case** (UPC-CFI X) | Full SmartTimeline of self + parallel-track for any linked CCR sub-project. All zones, all kinds. | The lawyer working a single proceeding sees everything in one view. |
| **Patent** (EP3456789) | Lanes — one per child case. Each lane shows only `kind IN ('deadline','milestone')` + status `IN ('done','open','overdue')`. Projected rows hidden by default (unfold-per-lane on click). | A patent typically has 1-3 active cases (CFI + CoA + opposition). Showing all projected rows from every case = overwhelming. Showing actuals + structural milestones gives the matter-level view. |
| **Litigation** (Siemens ./. Huawei) | Lanes — one per child patent's primary case (most-recently-active case). Show only `kind='milestone'` + status=`done` + per-case "next due" pill. | Litigation level is portfolio-of-patents-against-this-defendant. Useful to see when each patent's current proceeding is, not the granular deadlines. |
| **Client** (Siemens AG) | Default = matter list (existing project tree). Behind a "Timeline-Ansicht" toggle, lanes = one per litigation. Shows only `kind='milestone'` + status=`done`. | Client level can have 100+ matters. A timeline across all is meaningless. The toggle makes it discoverable for the partner who wants the bird's-eye view. |

### 5.2 The single rule

> Each level removes one tier of detail and adds one tier of grouping. Going up: fewer kinds rendered, fewer statuses surfaced, more lanes.

| Level | Kinds | Statuses | Lanes |
|---|---|---|---|
| Case | all | all | self + CCR child |
| Patent | deadline + milestone | done + open + overdue | one per child case |
| Litigation | milestone | done | one per child patent |
| Client | milestone (toggle) | done | one per child litigation |

This rule is implementable as a single `levelPolicy(projectType)` function in `ProjectionService` returning a `(kinds, statuses, lane_grouping)` triple. All four cases share the same render component; only the input filter varies.

### 5.3 Off-script events at higher levels

Off-script milestones (counterclaim filed, party amendment, scope change) are first-class at every level — they're the events m most cares about seeing at the litigation/patent overview. The "milestone" kind survives the level filter at all levels.

### 5.4 Not in v1

Cross-matter aggregation (e.g. "all my UPC matters, one timeline") is a Custom-View concern (t-paliad-144 substrate). The SmartTimeline is project-scoped; cross-project goes through `/views/{slug}` with a sources=`timeline` ViewSpec. Phase 5+, after t-paliad-163 Phase B lands.

---

## 6. Date-anchoring + reflow semantics

### 6.1 The rule (explicit)

> An actual date — recorded as a `paliad.deadlines.due_date` (status `done`) or `paliad.appointments.start_at` (in the past) or a milestone date — anchors every downstream projected event whose parent rule is the corresponding deadline_rule. The reflow propagates one parent-step at a time, until the next actual takes over or the chain bottoms out.

In other words: the existing `AnchorOverrides` mechanism in `FristenrechnerService.Calculate` is exactly the load-bearing primitive. The SmartTimeline's `ProjectionService` builds the override map at request time:

```go
overrides := map[string]string{}
for _, d := range completedDeadlines {
    if d.RuleCode == "" || d.CompletedAt == nil { continue }
    overrides[d.RuleCode] = d.CompletedAt.Format("2006-01-02")
}
// Court-set rules pick up the actual date too — set when the user enters
// "Hauptverhandlung fand statt am ..." via the inline anchor affordance.
opts := CalcOptions{AnchorOverrides: overrides, Flags: flagsForProject(p)}
result := frist.Calculate(ctx, p.ProceedingCode, p.TriggerDate, opts)
```

### 6.2 The UI affordance

Each projected row carries a `[Datum setzen]` link (or full-row click on tap-targets). Click → inline date input expands inline. On submit:

- If the row corresponds to a `deadline_rules` entry that has a *real* deadline (not court-set), the action creates a `paliad.deadlines` row with `rule_id` set, `due_date=entered`, `original_due_date=projected`, `source='anchor'`, `status='done'`, `completed_at=entered`. (The "anchor" source is new; existing values are `manual`, `rule`, `import`. v1 adds `'anchor'` to the existing CHECK list.) This is the "we just learned the parent fact" path.
- If the row is court-set (decision / hearing / order), the action creates a `paliad.appointments` row with `start_at=entered`, `appointment_type='hearing'|'decision'|'order'` derived from the rule's `event_type`. The appointment links back to `rule_code` via a new optional FK column `paliad.appointments.deadline_rule_id` (nullable; existing rows stay null).
- Either way, the next read recomputes the projection with the new override and downstream rows reflow.

### 6.3 Editing an actual date later

If the user clicks an existing actual row's date, the inline editor PATCHes the underlying record (`/api/deadlines/{id}` or `/api/appointments/{id}`), and the next read re-projects.

### 6.4 What happens to overdue projected rows

A projected row whose date is in the past but no actual exists yet renders as "vorhergesagt — überfällig" (faded amber). Clicking it lets the user either (a) anchor it as actual on a different date, or (b) explicitly mark "ist nicht eingetreten / wurde verschoben" — which writes a `project_events` row with `event_type='rule_skipped'` + `timeline_kind='milestone'` so the audit trail records the decision.

---

## 7. Off-script event UX

The cardinal constraint: "We must hold it flexible — add events regardless of whether they fit the normal course." Off-script events are first-class.

### 7.1 The "+ Eintrag" CTA

Persistent button in the SmartTimeline header. Click → typed-add modal:

```
┌──────────────────────────────────────────────────┐
│  Neuer Eintrag im SmartTimeline                  │
├──────────────────────────────────────────────────┤
│                                                  │
│  Was ist passiert? (oder wird passieren?)        │
│                                                  │
│   ◯ Frist                  → /deadlines/new      │
│   ◯ Termin                 → /appointments/new   │
│   ◯ Widerklage (CCR)       → Anlegen Sub-Akte    │
│   ◯ Anwendung auf Änderung (R.30)  → Flag setzen │
│   ◯ Schriftsatz / Order    → Off-script          │
│   ◯ Eigener Meilenstein    → Off-script (frei)   │
│                                                  │
│  [ Abbrechen ]              [ Weiter ▶ ]         │
└──────────────────────────────────────────────────┘
```

The visible options depend on the project's `proceeding_type_id`. UPC_INF gets the CCR + R.30 routes; UPC_REV gets CCI; DE_INF gets none of these. The "Schriftsatz / Order" + "Eigener Meilenstein" routes are universal.

### 7.2 The off-script branch

For "Schriftsatz / Order" and "Eigener Meilenstein" — a small form:

```
Off-script Meilenstein

  Titel:        [Widerklage angekündigt durch Beklagten         ]
  Datum:        [2026-05-02]
  Beschreibung: [Schreiben des Beklagtenanwalts vom 02.05., …    ]
  Verknüpfung:  ☐ Frist daraus erzeugen   ☐ Termin daraus erzeugen
  Sichtbar in:  ◉ Diese Akte   ◯ Diese Akte + Eltern
                                  ↑ Will it bubble up to higher levels?

  [ Abbrechen ]                                  [ Speichern ]
```

On submit, writes a `paliad.project_events` row with:

- `event_type='off_script_milestone'` (new value in the event_type enum-ish CHECK; today's CHECK is open-ended text — confirm during impl).
- `timeline_kind='custom_milestone'`.
- `event_date=entered`.
- `description=...`.
- `metadata={"track": "parent" | "off_script", "links": [...]}`.

The optional checkboxes "Frist daraus erzeugen / Termin daraus erzeugen" open the standard deadline/appointment-create flow with the milestone's data prefilled and the milestone's id linked via metadata for audit trail.

### 7.3 Curated catalogue per proceeding type (NICE TO HAVE)

A small lookup table `paliad.timeline_event_catalogue (proceeding_type_id, kind, slug, name_de, name_en, primary_party)` could surface in the modal as a "Häufige Ereignisse" section above the universal "Eigener Meilenstein" route. Examples:

- UPC_INF: Counterclaim Filed, Third Party Intervention, Hearing Postponement, Cost Decision Issued
- UPC_REV: Application to Amend Filed, Substantive Decision, Costs Order
- DE_INF: Hinweisbeschluss Issued, Verteidigungsanzeige, Termin Hauptverhandlung, Versäumnisurteil

The catalogue is a v2 nice-to-have. v1 ships with "Eigener Meilenstein" as the universal escape hatch and the few proceeding-specific routes named above (CCR, CCI, R.30) hardcoded on the modal.

---

## 8. Filter facets — first-pass refinement

Refining the task brief's first-pass list against the FilterBar API (riemann's `BarState` / `AxisKey`). Each axis maps to either a universal axis (already shipped), an existing per-source stub (riemann left ready), or a new one.

### 8.1 Reused universal axes (already in BarState)

- **`time`** (universal, chip cluster + custom range) — past 30/90d, next 30/90/any/custom. Default = `any`. Re-used verbatim; no work.
- **`personal_only`** (universal, chip) — re-used. "Nur meine Einträge" — `created_by=me`. Behavior same as on `/events` (t-paliad-128).

### 8.2 New per-source axes (extend `AxisKey`)

```ts
// frontend/src/client/filter-bar/types.ts — additions
export type AxisKey =
    | …existing…
    | "timeline_kind"     // multi-select chip cluster
    | "timeline_status"   // multi-select chip cluster
    | "timeline_track"    // multi-select chip cluster
    ;

export interface BarState {
    …existing…
    timeline_kind?: ("deadline" | "appointment" | "milestone" | "projected")[];
    timeline_status?: ("done" | "open" | "overdue" | "court_set" | "predicted" | "off_script")[];
    timeline_track?: ("parent" | "counterclaim" | string /* child:<projectid> */)[];
}
```

### 8.3 The facet set on the SmartTimeline surface

The surface declares this `axes` array when it mounts the bar:

```ts
mountFilterBar(host, {
  axes: [
    "time",            // universal — past/future filter
    "timeline_kind",   // deadline | appointment | milestone | projected
    "timeline_status", // done | open | overdue | court_set | predicted | off_script
    "timeline_track",  // parent | counterclaim | child:<id>
    "personal_only",   // optional — toggle "nur meine Einträge"
    "deadline_event_type", // existing stub — wired in t-paliad-117 multi-select
    "shape",           // timeline (default) | list | cards
    "sort",            // chronological asc/desc
    "density",         // comfortable | compact
  ],
  surfaceKey: "project-smart-timeline",
  systemViewSlug: "project-timeline",
  …
});
```

### 8.4 Defaults

- `time = any`
- `timeline_kind = [deadline, appointment, milestone]` (projected hidden by default — the user opts in via chip; reduces noise on first load when most projects don't have a proceeding type set)
- `timeline_status = [done, open, overdue, off_script]` (predicted + court_set hidden by default if `projected` kind is hidden — the chip group is one logical "show future" toggle)
- `timeline_track = all available`
- `shape = timeline`
- `sort = date_desc` (most recent first; matches today's Verlauf default)
- `density = comfortable`

### 8.5 The "show future" macro

Most users will only want one toggle: "Zukunft anzeigen". We render that as a primary chip pair next to `time`:

```
[ Vergangenheit | Heute | Zukunft ]   ← primary toggle
```

Internally this maps to `time + timeline_kind` (Vergangenheit hides projected, Zukunft shows projected, Heute is just today). Power users can drill into the granular axes via the bar.

### 8.6 What riemann's port (t-paliad-170) needs to know

Riemann is porting FilterBar onto the Verlauf surface in parallel. Three things they need:

1. **Three new axis keys** (`timeline_kind`, `timeline_status`, `timeline_track`). They render as chip clusters — the same primitive `chipRow + chipBtn` riemann already factored.
2. **`shape: "timeline"`** is a new render shape. Existing shapes are `list | cards | calendar` (t-paliad-144). We pick "timeline" as a 4th shape so the FilterBar's shape switcher lets the user collapse to `list` (compact audit log) or `cards` (chronological card grid) without losing the data. Implementation = new `frontend/src/client/views/shape-timeline.ts` mirroring the other shape files. Out of scope for t-paliad-170 (riemann ports the bar, not the new shape).
3. **The `timeline_track` axis options are dynamic** — they depend on whether the project has a counterclaim child. The bar already supports lazy axes (the `project` axis pattern in `axes.ts:30` — `"populated lazily"`). `timeline_track` follows the same shape: surface fetches available tracks at mount, passes them to the bar.

---

## 9. Verfahrensablauf-logic sharing — extract, don't import

**Recommendation: extract into a shared module first.**

### 9.1 The decision

The wizard's projection logic is currently in two places:

1. `internal/services/fristenrechner.go:Calculate(...)` — the canonical Go implementation. Already returns a `UIResponse{Deadlines []UIDeadline}` keyed by rule_code, supports `AnchorOverrides`. ~1000 lines, tested.
2. `frontend/src/client/fristenrechner.ts:calculate()` — the frontend wrapper that POSTs `/api/tools/fristenrechner` and handles flags + overrides. ~3500 lines including the wizard UI, but the projection-relevant slice is small (call + render).

The SmartTimeline's `ProjectionService.For(projectID)` needs the *Go calculator*, not the frontend code path. So the question is really: *do we add a new Go service that wraps `FristenrechnerService.Calculate` for projects?*

Yes — a thin adapter, not a parallel implementation.

### 9.2 The adapter

```go
// internal/services/projection_service.go (new, ~200 LoC)

type ProjectionService struct {
    db          *sqlx.DB
    fristen     *FristenrechnerService
    deadlines   *DeadlineService
    appointments *AppointmentService
    projects    *ProjectService
    courts      *CourtService
}

// For builds a SmartTimeline for one project (and its CCR child if any).
// Composes the four zones described in §1; returns sorted TimelineEvent[].
func (s *ProjectionService) For(ctx context.Context, projectID uuid.UUID, opts ProjectionOpts) ([]TimelineEvent, error) {
    p, err := s.projects.GetVisible(ctx, projectID, opts.ViewerID)
    // ...
    children := s.projects.LoadCounterclaimChildrenVisible(ctx, projectID, opts.ViewerID)

    actuals := s.collectActuals(ctx, []uuid.UUID{p.ID, children...}) // dl + appt + milestones
    overrides := buildAnchorOverrides(actuals)

    var projected []TimelineEvent
    if p.ProceedingTypeCode != "" && p.TriggerDate != nil {
        proj := s.fristen.Calculate(ctx, p.ProceedingTypeCode, p.TriggerDate.Format("2006-01-02"),
            CalcOptions{AnchorOverrides: overrides, Flags: flagsFor(p), CourtID: p.CourtID})
        projected = projectionToTimeline(proj, p, actuals)
    }
    // (same for each child counterclaim)

    return mergeAndSort(actuals, projected, opts.LevelPolicy), nil
}
```

The adapter does not duplicate the calculator — it calls `FristenrechnerService.Calculate` exactly once per (project, child). Same code path as `/api/tools/fristenrechner` uses today; same tests cover both.

### 9.3 What the standalone wizard keeps

`/tools/fristenrechner` continues to use `FristenrechnerService.Calculate` directly — it's a knowledge-platform tool, not a project-scoped view. It does not gain anchoring affordances or off-script events. The projection there is hypothetical ("if you start a UPC_INF on date X, here's the timeline"), not project-actual.

`ProjectionService` is a project-scoped composition layer; it lives one level above `FristenrechnerService` in the dependency graph.

### 9.4 The test split

- `fristenrechner_test.go` keeps testing the calculator (duration math, AnchorOverrides, CourtID resolution).
- `projection_service_test.go` (new) tests the composition: mixing actuals + projected, level policy, counterclaim child merging, sort order.

---

## 10. Phasing — 4 sequential slices

Each slice is independently shippable and reviewable. m's go/no-go gate after each.

### Slice 1 — SmartTimeline skeleton (no projection yet)

What lands:

- New `internal/services/projection_service.go` with `For()` returning only actuals (deadlines + appointments + opted-in `project_events`). No `fristenrechner` call yet.
- Migration `NNN_project_events_timeline_kind.up.sql` adds the optional column + partial index (§2.2).
- New endpoint `GET /api/projects/{id}/timeline?…` returning `[]TimelineEvent`.
- `frontend/src/client/projects-detail.ts:loadEvents` rewritten to call `/timeline` instead of `/events`. The current Verlauf list is replaced by the new vertical timeline component (`client/views/shape-timeline.ts` — new file, ~300 LoC).
- "+ Eintrag" CTA in the timeline header (modal partially implemented — only "Eigener Meilenstein" route lit; CCR / R.30 / Frist / Termin routes are link buttons to existing flows).
- "Audit-Log anzeigen" toggle that switches to the legacy chronological list rendering (`paliad.project_events` ALL — not just `timeline_kind IS NOT NULL`).

What it gives m: a working SmartTimeline showing past actuals + open/upcoming deadlines + appointments + off-script milestones, with the audit log surviving as a toggle. No future-projection yet.

### Slice 2 — Future-projection + click-to-anchor

What lands:

- `ProjectionService.For` calls `FristenrechnerService.Calculate` and emits projected rows.
- Click-to-anchor inline date editor (§6.2). New endpoint `POST /api/projects/{id}/timeline/anchor` taking `{rule_code, actual_date, kind?}` and writing the appropriate `paliad.deadlines` (`source='anchor'`) or `paliad.appointments` (`deadline_rule_id` FK new) row.
- Migration `NNN_appointments_deadline_rule_id.up.sql` adds the optional FK on appointments + extends `paliad.deadlines.source` CHECK to include `'anchor'`.
- "voraussichtlich" / "Datum vom Gericht" status pills + projected-row CSS (faded + dashed border for court-set).
- New "Zukunft anzeigen" macro chip pair (§8.5).
- `event_type='rule_skipped'` write path for the "ist nicht eingetreten" decision (§6.4).

What it gives m: predicted future course based on standard timeline; click to fix any date when something happens; downstream reflows automatically.

### Slice 3 — Counterclaim sub-project

What lands:

- Migration `NNN_projects_counterclaim_of.up.sql` — the new `counterclaim_of` FK + index + the CHECK (a project either has counterclaim_of OR is parent — not both — to keep the invariant clean).
- "+ Eintrag → Widerklage (CCR)" route in the modal (§7.1) — creates child project with auto-suggested `our_side` flip, `proceeding_type_id`, and title, then navigates to it for the user to fill in `case_number`.
- `ProjectionService` loads CCR children + emits parallel-track rows.
- `[Track ▼]` chip in the header — reads `available_tracks` from the timeline response.
- The two-column rendering on State C (§3.3).
- `paliad.project_events` audit row written on counterclaim creation (`event_type='counterclaim_created'`, `timeline_kind='milestone'`).

What it gives m: counterclaims as proper sub-projects, parallel timelines, CCR perspective-flip works end-to-end.

### Slice 4 — Parent-node aggregation

What lands:

- `levelPolicy(projectType)` in `ProjectionService` — kinds/statuses/lane filter per level (§5.1).
- Lane-grouped rendering at Patent / Litigation / Client levels.
- "Timeline-Ansicht" toggle on Client-level project page (default off; lanes-of-litigations when on).
- Off-script milestones bubble up to higher levels via the `metadata.bubble_up: true` flag (§7.2 form's "Sichtbar in: Diese Akte + Eltern" checkbox).

What it gives m: portfolio-level timelines without overload — the bird's-eye view he asked about.

### What's NOT in any slice

- Curated per-proceeding event catalogue (§7.3) — v2 nice-to-have.
- Gantt rendering — separate `shape: "gantt"` follow-up.
- Cross-matter timeline — Custom Views path.
- Outlook integration — out of scope.

---

## 11. Open questions for m

Listed with my (inventor) pick where I have one — m decides.

**Q1 — Counterclaim sub-project vs proceeding-overlay (§4).** I recommend sub-project. Confirm before Slice 3 design lock.

**Q2 — Should `our_side` flip automatically on counterclaim sub-project creation?** My pick: yes, default-flip with a "Stimmt nicht?" toggle on the create modal. The R.49.2.b CCI is the edge case (parent claimant → child claimant in CCI of the *separate* infringement claim), but the standard CCR-on-validity always inverts. Default-flip + toggle handles both.

**Q3 — Should `paliad.deadlines.source` gain `'anchor'` or should we re-use `'manual'`?** My pick: new `'anchor'` value — separates "user-typed-it-in" from "user-recorded-an-actual-after-projection-fired" for analytics + future automated import (Outlook event → anchor).

**Q4 — Counterclaim sub-project's `parent_id` — under the patent (sibling to parent case) or under the parent case (grandchild)?** My pick: under the patent (sibling). The CCR is its own proceeding with its own case_number; modeling it as a sibling to the parent infringement, both under the patent, mirrors how UPC CMS sees them. Grandchild placement would imply CCR is "part of" the parent case which it structurally isn't.

**Q5 — Off-script milestone bubble-up default.** My pick: default-on for `event_type IN ('counterclaim_created', 'third_party_intervention', 'scope_change')`; default-off for `event_type='custom_milestone'`. Form has the override checkbox in either case.

**Q6 — Should `/tools/fristenrechner` keep its standalone existence?** Brief says yes — knowledge tool, separate from project context. My pick: yes, agree. It stays.

**Q7 — Application-to-amend (UPC R.30) as sub-project or flag?** My pick: stay as flag (`with_amend`). Amendments are not a separate proceeding artifact in the CMS — they ride on the parent's record. The cross-flow rules already activate via `condition_flag`.

**Q8 — On the parent's SmartTimeline, do CCR rows mix into one column or stay in a parallel right-track?** My pick: parallel right-track when both are populated; collapses into one column on mobile (vertical stacking with sub-headers per track). The `[Track ▼]` chip lets desktop users opt into single-column mode.

**Q9 — Court-set anchor (Hauptverhandlung) creates a `paliad.appointments` row or a `paliad.deadlines` row?** My pick: `paliad.appointments` — it's an appointment, not a deadline. The new `appointments.deadline_rule_id` FK preserves the link back to the rule for downstream re-anchoring.

**Q10 — Is `timeline_kind` the right column name?** Alternatives: `is_timeline_milestone bool`, `surface_on_timeline bool`. My pick: keep `timeline_kind text NULL` because it lets us distinguish `milestone` (structural) from `custom_milestone` (free-form) without a second column.

**Q11 — Should the SmartTimeline be the only view of the project's events?** Or do we keep a "klassisch (chronologisch)" sidebar tab? My pick: SmartTimeline as the only Verlauf tab; "Audit-Log anzeigen" toggle inside the timeline reveals the chronological rendering. m uses `/admin/audit-log` (t-paliad-071) for the cross-project audit query.

**Q12 — Patent-level "matter list vs lane timeline" default.** My pick: lanes by default at Patent + Litigation; matter list by default at Client. The Litigation level has 1-3 child patents typically → 1-3 lanes is fine. Client can have 100+ → lanes are a toggle.

---

## 12. Files implementer will touch (Slice 1 only)

Aggregated for the coder shift kickoff:

**Backend (Go):**
- `internal/services/projection_service.go` — new, ~250 LoC.
- `internal/handlers/projection.go` — new, GET /api/projects/{id}/timeline, ~80 LoC.
- `internal/handlers/handlers.go` — register the new route.
- `internal/db/migrations/NNN_project_events_timeline_kind.{up,down}.sql` — new.

**Frontend (TS / TSX):**
- `frontend/src/client/views/shape-timeline.ts` — new render shape, ~300 LoC.
- `frontend/src/client/projects-detail.ts:loadEvents` — replace with timeline fetch.
- `frontend/src/projects-detail.tsx:74-101` — replace Verlauf markup with `<div id="project-smart-timeline">`.
- `frontend/src/styles/global.css` — `.smart-timeline-*` styles, ~150 LoC.
- `frontend/src/client/i18n.ts` — ~30 keys under `projects.detail.smarttimeline.*`.

**Tests:**
- `internal/services/projection_service_test.go` — new (live-DB integration test, skipped without `TEST_DATABASE_URL`).
- `internal/services/projection_service_unit_test.go` — pure-function tests (sort, level policy, override-build).

Slices 2-4 are scoped in §10; coder picks them up after m's gate.

---

## 13. Trade-offs flagged

- **Per-request projection cost.** Recomputing on every Verlauf load is fine for a single project. If m navigates to a Client-level lane view with 50 child litigations × 3 cases each, that's 150 calculator invocations. Mitigation: lane-rendering at Litigation+Client levels excludes `kind='projected'` by default (§5), so the calculator is only called on the leaf rendering. Watch in production; add per-(project, hash(overrides)) cache if needed.
- **Migration order across active workers.** riemann is on t-paliad-170 (FilterBar Verlauf port) in parallel. Slice 1 must merge **after** their port because Slice 1 mounts the bar with new axis keys. Coordinate via head before Slice 1 PR opens.
- **Sub-project counterclaim adds a tier.** The project tree gets deeper (Patent → Case + Patent → CCR-Sub-Case as siblings). Existing tree visualisation in t-paliad-149 handles arbitrary depth, but the per-card "in 3 children" badge needs to count the CCR child correctly — verify in Slice 3.
- **`appointments.deadline_rule_id`** is a backward-pointing FK that doesn't exist yet. Adding it in Slice 2 is clean (nullable, no backfill needed). Just flagging that this ties appointments to deadline_rules where they previously had no link.
- **Anchor write path can race.** Two users clicking "Datum setzen" on the same row simultaneously could both write `paliad.deadlines` rows. Mitigation: server-side check `WHERE NOT EXISTS (SELECT 1 FROM paliad.deadlines WHERE project_id=... AND rule_id=...)` before insert, otherwise PATCH the existing row. Standard pattern.
- **What if the proceeding type changes mid-flight?** The user changes `paliad.projects.proceeding_type_id` after deadlines have been calculated. Existing actuals stay (they have `rule_id` FK pointing to the OLD rule tree). Projected rows recompute against the NEW rule tree; rule_codes that don't exist in the new tree drop out. This is the same behaviour today — flagging because the SmartTimeline makes it more visible.

---

## 14. Recommendation for implementer

Pattern-fluent Sonnet coder. Slice 1 is largely boilerplate (new service + handler + render shape). Slice 2 needs the calculator integration which is well-trodden (t-paliad-131 Phase A shipped overrides). Slice 3 needs the sub-project FK design (one careful migration) and the parallel-track CSS. Slice 4 is render-policy logic, low-risk.

Lagrange (this worktree) parks. NOT pre-emptively flipping to coder — m gates.

---

**DESIGN READY FOR REVIEW**
