# Design — Fristenrechner complete UX overhaul (dual-mode + project write-back)

**Task:** t-paliad-322
**Gitea:** m/paliad#146
**Inventor:** cronus (shift-1)
**Date:** 2026-05-26
**Status:** Draft for m's ratification — coder gate held

## 0. Premises verified live (before designing)

Verified against the live youpc Postgres (port 11833, paliad schema) and the live source tree on `mai/cronus/inventor-fristenrechner` @ HEAD.

### 0.1 Rule-and-event corpus today

| Table | Active+published rows | Notes |
|---|---|---|
| `paliad.procedural_events` | 222 (236 total) | The events that anchor a deadline. 4 `event_kind` buckets: `filing`, `hearing`, `decision`, `order`, plus `NULL` for legacy/dpma stragglers. |
| `paliad.sequencing_rules` | 231 | The deadlines themselves, anchored on `procedural_event_id` and (sometimes) `trigger_event_id`. 80 carry a `trigger_event_id`, 4 are `is_spawn=true`, 45 are `is_court_set=true`, 18 carry a `condition_expr`. |
| `paliad.deadline_concepts` | 57 | Hub layer above events (Klageerhebung, Wiedereinsetzung, …). |
| `paliad.proceeding_types` | 46 fristenrechner | 4 jurisdictions: UPC (35), DE (5), EPA (3), DPMA (3). |
| `paliad.event_categories` | 125 (103 leaves) | The current cascade tree — 5 user-bucket roots (`cms-eingang`, `muendl-verhandlung`, `beschluss-entscheidung`, `frist-verpasst`, `ich-moechte-einreichen`) + `sonstiges` leaf. UI hides the forward-workflow root (`HIDDEN_CASCADE_ROOTS` in `client/fristenrechner.ts:2605`). |
| `paliad.deadlines` | 10 (8 with `sequencing_rule_id`) | Demand-side still tiny. The 2 without `sequencing_rule_id` are manual entries. |

Live `primary_party` vocabulary on `sequencing_rules`: `claimant`, `defendant`, `both`, `court`, `NULL`. Live `priority` vocabulary: `mandatory`, `recommended`, `optional` (no `informational` rows yet — Phase 2 reserved the slot but seeding is deferred).

### 0.2 The legacy `deadline_rules` reader is a view

`paliad.deadline_rules_unified` (mig 139, Slice B.3) is a **view** over `sequencing_rules ⋈ procedural_events ⋈ legal_sources`. All Go calculator paths read through it (see `deadline_rule_service.go:70`). The physical `paliad.deadline_rules` table was dropped in mig 140; the view is the canonical legacy-shape reader. Important for this design: there is no "trigger event" table parallel to events — the rule rows themselves are the things the wizard must land on. `trigger_events` (110 rows) is the youpc-parity legacy table used by `/api/tools/event-deadlines` only.

### 0.3 The frontend today (`/tools/fristenrechner`)

Two server-rendered surfaces share the same page (`frontend/src/fristenrechner.tsx`, 657 lines) — the legacy "Procedure mode" (R1 step-list, proceeding picker, trigger date, flag checkboxes) and the **Pathway-B row stack** (`buildRowStack` at `client/fristenrechner.ts:2848`, 4009 lines total). Row stack composes three row kinds via a single `.fristen-row` primitive:

| Row | Source | Filter or qualifier today |
|---|---|---|
| R1 Perspective (Beide / Klägerseite / Beklagtenseite) | `currentPerspective`, prefilled from `project.our_side` | hybrid — narrows party-tagged cascade chips AND is used as a column-bucket hint in the result view |
| R2 Inbox channel (CMS / beA / Postal / Alle) | `currentInboxChannel` | filter — narrows cascade by forum (CMS → upc, beA → de, …) |
| R3..Rn Cascade chain | `event_categories` tree | each step narrows children by `inboxFilterAllowsForums` + `perspectiveAllowsParty` + `cascadeChildAllowsProject` |

The cascade auto-walks single-child branches under a project context and stops at the first branching point. The user picks a leaf; the leaf's slug feeds `/api/tools/fristenrechner/search?event_category_slug=…` which returns concept cards. Each card expands inline to a calc panel (trigger-date input + flags + computed deadline + "in Akte" CTA).

### 0.4 What is broken in this UI (m's verdict, 2026-05-26 21:21)

m's brief in m/paliad#146 enumerates four visible bugs:

1. **"Beide" as default perspective** is incoherent for the headline use case ("file a deadline because something happened" — you ARE one side).
2. **R2 (inbox) does not constrain R3 (cascade)** the way a user expects — picking CMS still leaves "Mündliche Verhandlung" / "Frist verpasst" on the next step. (Cause: those roots have `forums=NULL` in the seed → neutral → visible from every inbox.)
3. **Mixed axes** — the form layers filters (forum, inbox channel) on top of qualifiers (event-kind, perspective, proceeding_type) without making the difference visible. The user can't tell which picks narrow and which define.
4. **Trigger vs follow-up confusion** — the wizard's purpose is to identify the *trigger event*, then surface the *follow-up deadlines*. Today that split is not reflected in the form. After landing on a leaf, the user gets a flat list of concept cards and has to figure out which one is "the thing that happened" vs "the thing I have to file next".

m's verdict: "complete overhaul. Should be easy to use."

### 0.5 Anchor files for the eventual coder

- `frontend/src/client/fristenrechner.ts` (4009 LoC) — page brain. `buildRowStack` @ L2848, `renderRowStack` @ L3112, `runB1Search` (concept-card render) downstream, `expandCardCalc` @ L1337 (inline calc panel), `openSaveModal` @ L290 + `submitSave` @ L374 (project write-back).
- `frontend/src/fristenrechner.tsx` (657 LoC) — server-rendered shell. Contains both the Procedure-mode form **and** the Pathway-B row-stack scaffold. The new design replaces the row-stack scaffold; the procedure-mode form survives.
- `internal/handlers/fristenrechner.go` + `_search.go` + `_event_categories.go` — three handler files. `POST /api/tools/fristenrechner` (procedure-mode calc), `GET /search` (concept cards), `GET /event-categories` (cascade tree).
- `internal/services/fristenrechner.go` (661 LoC) — calculator adapter to `pkg/litigationplanner`. The calculator is **not** touched by this design.
- `internal/handlers/deadlines.go:167` + `services/deadline_service.go:411` (`CreateBulk`) — the project write-back endpoint (`POST /api/projects/{id}/deadlines/bulk`). This survives; the design extends its caller.

### 0.6 Adjacent design docs to read alongside

- `docs/design-determinator-row-cascade-2026-05-13.md` — the row-cascade pillars (Project-driven narrowing / Visual hierarchy overhaul / Persistent row stack). This overhaul **keeps** Pillars 2 and 3 and reworks Pillar 1's contract.
- `docs/design-fristen-phase2-2026-05-15.md` — the unified `sequencing_rules` model the calculator already runs on.
- `docs/audit-fristen-logic-2026-05-13.md` — the trigger-event / Pipeline-A-vs-C distinction.

---

## 1. Vision

**One page, two complementary entry paths, one result surface, one write-back.**

```text
┌───────────────────────── /tools/fristenrechner ─────────────────────────┐
│                                                                          │
│   ╭──────── Akte / kontextfrei ────────╮   (Step 0 — unchanged today)    │
│   │  HL-2024-001  ▼   |   ohne Akte   │                                  │
│   ╰─────────────────────────────────────╯                                │
│                                                                          │
│   ╭────── Entry mode tabs ──────╮                                         │
│   │  [⚡ Direkt suchen]          │  ◀── A: power user, search + chips      │
│   │  [🧭 Geführt]                │  ◀── B: 3-5 question wizard             │
│   ╰─────────────────────────────╯                                         │
│                                                                          │
│   ┌── Mode A: Suche ──────────────┐  ┌── Mode B: Wizard ────────────────┐│
│   │ search-box ▢▢▢▢▢▢▢▢▢▢▢▢▢▢▢   │  │ R1  Was ist passiert?  ✓ filing  ││
│   │ filter chips:                  │  │ R2  Forum?             ✓ UPC     ││
│   │  Forum · Proceeding · Event-   │  │ R3  Verfahren?         ✓ INF     ││
│   │  Kind · Partei                 │  │ R4  Welcher Schritt?    [active] ││
│   │ ┌ Ergebnis-Liste ────────────┐│  │ R5  Welche Seite?      ✓ Kläger  ││
│   │ │  procedural_event hits     ││  │                                   ││
│   │ │  [Trigger einrasten →]     ││  │ (Direkt-suchen ←)                 ││
│   │ └────────────────────────────┘│  └───────────────────────────────────┘│
│   └────────────────────────────────┘                                       │
│                                                                          │
│   ════ shared from here ═══════════════════════════════════════════════  │
│                                                                          │
│   ┌── Trigger event (locked) ──────────────────────────────────────────┐ │
│   │  📥 Klageschrift wurde eingereicht                                  │ │
│   │  upc.inf.cfi · Verletzungsverfahren · Klägerseite                   │ │
│   │  Trigger-Datum: [📅 2026-05-20]   (heute)                            │ │
│   │  ändern ↩                                                            │ │
│   └─────────────────────────────────────────────────────────────────────┘ │
│                                                                          │
│   ┌── Folge-Fristen ────────────────────────────────────────────────────┐ │
│   │  ◉ MANDATORY (auto-checked)                                          │ │
│   │   ☑ Klageerwiderung (3 Monate)  — 20.08.2026 — RoP 23      ✏ Datum  │ │
│   │   ☑ ...                                                               │ │
│   │  ◇ OPTIONAL                                                          │ │
│   │   □ Wiedereinsetzungsantrag (R.320)  — bei Versäumnis                │ │
│   │  ◊ CONDITIONAL                                                       │ │
│   │   □ Erwiderung auf Nichtigkeitswiderklage   nur wenn CCR              │ │
│   │  ⇲ SPAWNED                                                           │ │
│   │   ☑ Berufung gegen Endurteil                            (kein Datum) │ │
│   │  ╭────────────────────────────╮                                      │ │
│   │  │ 4 ausgewählt → in Akte ▶  │                                      │ │
│   │  ╰────────────────────────────╯                                      │ │
│   └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
```

The two modes never compete: they're two front doors into the **same** locked-trigger-event → follow-up-list → write-back flow.

---

## 2. Axis taxonomy — ratified (filters vs qualifiers)

The headline source of today's UX confusion is the unmarked mixing of *filters* (narrowing the question space without committing to an answer) and *qualifiers* (parts of the eventual deadline definition).

| Axis | Role | Source | Constrains | Visual in new UI |
|---|---|---|---|---|
| `forum` | **filter** | derived from `proceeding_types.jurisdiction` (UPC/DE/EPA/DPMA), or from `project.proceeding_type_id`, or user pick | which `proceeding_types` are reachable; which `event_categories` are visible | Mode A: filter chip strip. Mode B: explicit wizard row (R2). Pre-filled + collapsed when there's a project. |
| `proceeding_type` | **qualifier** | `project.proceeding_type_id` (binds via mig 096 codes) OR user pick during wizard | the set of `sequencing_rules` rows that can apply | Mode A: filter chip strip. Mode B: explicit wizard row (R3). Pre-filled + collapsed when there's a project. |
| `event_kind` | **filter** | `procedural_events.event_kind` (filing / hearing / decision / order) | which `procedural_events` are reachable as triggers | Mode A: filter chip strip. Mode B: explicit wizard row (R1 — the headline question). |
| `inbox channel` | **filter** (today) → **out of scope row** (new) | user pick | nothing the user can see (the rule corpus has no "inbox" column; it was only used to recolour the cascade) | Removed from the primary wizard. Pushed into Mode A's secondary chips (off by default). See §3.3. |
| `perspective (our_side)` | **qualifier in file-mode**, **filter in explore-mode** | `project.our_side` OR user pick OR implicit-via-event-kind | `sequencing_rules.primary_party`; result-view column bucketing | Wizard tail (R5) **only when** the trigger event's follow-ups actually differ by side. Pre-filled when project has `our_side`. |
| `instance_level` (first / appeal / cassation) | qualifier | `project.instance_level` (mig 084) — sparse | rare — used to disambiguate APP+DE | Surfaced only when the wizard hits APP+DE-style ambiguity. |

**Rule:** a **filter** narrows the visible options without locking in a deadline answer; it can be cleared and re-applied. A **qualifier** is part of the resulting deadline calculation and is locked the moment it's picked. Filters must propagate forward (Mode A's forum-chip narrows the proceeding-chip's options). Qualifiers are picked once and the answer view reads them.

The "Beide" perspective default (today's bug) is wrong because perspective is a *qualifier* in the headline use case ("file a deadline because something happened — you are one side"), not a *filter*. New default in Mode B: derive from the project's `our_side`, otherwise force a R5 pick (no "Beide"). See Q8 for the explore-mode exception.

---

## 3. Mode taxonomy

### 3.1 Mode A — "⚡ Direkt suchen" (power user)

Two visually distinct strips (per m §11.Q3):

```text
┌── Filter (eingrenzen) ─────────────────────────────────────────────────┐
│  Forum: [UPC] [DE] [EPA] [DPMA] [Alle]                                  │
│  Verfahren: [upc.inf.cfi] [...] [Alle]                                  │
│  Was passierte: [📥 Eingereicht] [🏛️ Termin] [⚖️ Entscheidung] [📜 Verfügung] [Alle] │
│  Partei: [Klägerseite] [Beklagtenseite] [Beide]                          │
├── Suchen ──────────────────────────────────────────────────────────────┤
│  🔎 [_______________________________________________________________]   │
└─────────────────────────────────────────────────────────────────────────┘
┌── Ergebnisse (klicken = als Trigger einrasten) ────────────────────────┐
│  📥 Klageerhebung  ·  upc.inf.cfi · UPC · 3 Folge-Fristen   →           │
│  ...                                                                    │
└─────────────────────────────────────────────────────────────────────────┘
```

Single text input, four filter chip strips above it (Forum · Proceeding · Event-Kind · Partei), and a ranked result list of `procedural_events` underneath. The "Filter" strip is visibly grouped (e.g. light background + "Filter (eingrenzen)" header) so users see at a glance that those picks narrow but don't commit; clicking a result row IS the commit (the qualifier action).

- Search hits `/api/tools/fristenrechner/search` (extended to return events, not just concepts — see §6.1).
- Filter chips compose with the text query (`?forum=upc&pt=upc.inf.cfi&kind=filing&party=defendant&q=Klageerwiderung`).
- Result rows are individual `procedural_events` (not aggregated concept-cards). Each row shows: name (DE/EN), proceeding_type code, jurisdiction badge, event_kind icon, the rule-count it triggers ("3 Folge-Fristen").
- Click a row → "lock as trigger event" → page transitions to the §4 result view.
- Power affordance: a row with multiple linked rules can be locked in **per-rule** ("nur diese Frist") via a kebab menu on the row. (Sane default: lock the whole event; the kebab is for the lawyer who only wants one specific reactive deadline.)

### 3.2 Mode B — "🧭 Geführt" (the wizard)

A 3-5 question row stack that lands on one `procedural_events` row.

**Question order (strawman; m to ratify in Q5):**

1. **R1 — Was ist passiert?** Chips: 📥 Eingereicht (`filing`) · 🏛️ Termin (`hearing`) · ⚖️ Entscheidung (`decision`) · 📜 Verfügung (`order`) · 🕒 Frist versäumt (special bucket, routes to Wiedereinsetzung). One chip = one `event_kind` (or the special). Always asked. ~6 chips, fits one row.
2. **R2 — Vor welchem Gericht / bei welchem Amt?** Chips: UPC · LG/OLG/BGH · EPA · DPMA. Pre-filled from `project.proceeding_type → jurisdiction` (or `project.court` substring). **Skipped if R1 narrows to a single forum** (e.g. "Termin" + project has UPC → R2 is implied).
3. **R3 — In welchem Verfahren?** Chips: every active `proceeding_type` whose jurisdiction matches R2 AND whose event roster contains at least one event with R1's kind. Pre-filled from `project.proceeding_type_id`. **Auto-skipped** when the narrowed scope has only one candidate.
4. **R4 — Welches Schriftstück / Welcher Termin?** This is the wizard's landing question. Chips = `procedural_events` filtered by (R2 forum, R3 proceeding_type, R1 event_kind). Typical scope: 1-12 events. If the user types into this row, the chip layout flips to a search list (same widget as Mode A's result list, narrowed to the wizard's filters).
5. **R5 — Vertreten Sie Kläger- oder Beklagtenseite?** Asked **only when** the selected event's `sequencing_rules` have follow-ups that differ by `primary_party` (a quick "are there both claimant- and defendant-tagged rules among the follow-ups?" check on the catalog). Pre-filled from `project.our_side`. **Skipped otherwise.**

**Row badges** (per m §11.Q3): each wizard row carries a small "Filter" or "Qualifier" tag next to its row-number badge. R1 (event_kind), R2 (forum) → "Filter". R3 (proceeding_type), R4 (procedural_event), R5 (perspective) → "Qualifier". A user can tell at a glance which picks lock in vs which narrow.

Branching policy (locked):

- Pre-fill + collapse a row when the answer is implied by the project (Determinator §4 pattern, unchanged).
- Auto-skip a row when the narrowed scope has exactly one option (the user has effectively no choice). Show the skipped row as a compact `.fristen-row.is-prefilled` line with "(aus Akte)" or "(implizit aus R1)" annotation. *Don't hide the row* — m's "see your selections" pillar from the row-cascade design demands every decision stays visible.
- A user-edited upstream answer **preserves compatible downstream picks** (m §11.Q10): if a re-picked R2 (forum) keeps the existing R1 (event_kind) legal, R1 stays; if it makes R3 (proceeding_type) illegal, R3 resets to active. Rows whose pick was carried across an upstream change render with a one-render "erhalten" annotation so the user notices.
- "Welches Schriftstück?" (R4) is the landing question. Once R4 is answered, the wizard exits and the §4 result view takes over.

### 3.3 The dropped `inbox channel` row

R2-inbox in today's row stack is removed from the primary surface for both modes. Rationale:

- The rule corpus has no `inbox` column. The cascade's `forums=['cms']` etc. tags were a presentation-layer reflection of which forum naturally arrives on which channel — but the rule itself doesn't change based on whether a UPC document arrived via CMS or by post (it can't; only CMS is legal). So the only honest role for "inbox" is to nudge the forum filter on Mode A.
- Mode A keeps inbox as a *secondary* chip strip ("Erweitert" toggle, off by default). Picking CMS auto-sets the forum chip to UPC; picking beA auto-sets it to DE. The user can override.
- Mode B never asks. The wizard derives forum from project context or from R2.

This collapses one bug class entirely (R2-not-constraining-R3) by retiring R2 from the headline path.

---

## 4. Shared result view — "follow-up deadlines"

Once a trigger event is locked (via Mode A click or Mode B R4 pick), the same result view renders.

### 4.1 Trigger card (sticky header)

```text
┌─ Trigger-Ereignis ─────────────────────────────────────────────────────┐
│  📥 Klageerhebung                                                       │
│  upc.inf.cfi · Verletzungsverfahren · UPC                              │
│  ⓘ "Einreichung der Klageschrift gemäß R.13 RoP"                       │
│  Trigger-Datum: 📅 2026-05-20    [ändern ↩]                             │
└─────────────────────────────────────────────────────────────────────────┘
```

`Trigger-Datum` defaults to today. The user can change it inline (date picker). Changing it re-renders the follow-ups with new computed dates.

The "ändern" link drops back to whichever mode the user came from with R1-R4 still answered. (Per Q4: the wizard preserves compatible upstream picks rather than rebooting.)

### 4.2 Follow-up groups

Group `sequencing_rules` rows that have the trigger event as **anchor** (i.e. `sr.procedural_event_id = trigger.id`) into 4 visible groups:

1. **MANDATORY** (`priority='mandatory'`) — pre-checked. The bread-and-butter follow-ups.
2. **RECOMMENDED** (`priority='recommended'`) — pre-checked. Best-practice fillings (R.19 EPÜ Einspruch, replication briefs).
3. **OPTIONAL** (`priority='optional'`) — unchecked. Discretionary actions (R.320 Wiedereinsetzung).
4. **CONDITIONAL** (`condition_expr IS NOT NULL`) — unchecked, with the condition rendered ("nur wenn CCR im Verfahren"). Lawyer ticks if applicable.

Plus a fifth implicit bucket:

5. **SPAWNED / CROSS-PROCEEDING** (`is_spawn=true`, `spawn_proceeding_type_id IS NOT NULL`) — surfaced as a separate sub-section with a clear "leitet ein neues Verfahren ein" annotation. Pre-checked when mandatory.

Recommendation (Q6): **4 visible groups, with SPAWNED inlined into whichever priority bucket it belongs to but tagged with a "⇲ neues Verfahren" badge.** Five groups is too many for a one-page result; folding SPAWNED into its priority keeps the math right (mandatory spawned = mandatory) while still flagging the cross-proceeding implication.

### 4.3 Per-rule row

```text
☑ Klageerwiderung                                                  ✏ Datum
   3 Monate nach Klageerhebung                          20.08.2026
   RoP 23 · Beklagtenseite
   ⓘ Schriftlich, mit Vollmacht. Erstmaliges Bestreiten der Patentverletzung.
```

Columns: checkbox · title (DE/EN) · duration phrase · computed due date · rule citation · party stance · expandable notes.

Inline date editor (✏ Datum) lets the lawyer override the computed date for this rule (same affordance as today's `wireDateEditClicks`). The override flows into the write-back payload.

`is_court_set=true` rules render with the "wird vom Gericht bestimmt" placeholder instead of a date and are unchecked-by-default (matches the current `openSaveModal` behaviour).

### 4.4 Result-view footer (write-back CTA)

```text
┌─ Auswahl ──────────────────────────────────────────────────────────────┐
│  4 Fristen ausgewählt   →   In Akte HL-2024-001 eintragen   ▶          │
│  (oder: 2 mit eigenem Datum, 2 mit Standardberechnung)                  │
└─────────────────────────────────────────────────────────────────────────┘
```

The CTA opens a **confirm-and-edit-dates modal** (per m §11.Q6) where the lawyer can revise each selected deadline's due date one last time, then commits via `POST /api/projects/{id}/deadlines/bulk` (today's endpoint).

**Kontextfrei mode (no Akte)** — per m §11.Q7, the entire write-back footer **does not render** when `project == null`. The result view stays informational. In its place, an inline nudge appears above the deadline groups:

```text
ⓘ Tipp: Wähle oben eine Akte, um diese Fristen einzutragen.
```

The "oben" link focuses the Akte picker. Once a project is picked, the nudge collapses and the footer materialises; no page reload, no result-view rebuild (the trigger event and date persist across the project pick).

Modal payload per deadline (extends today's `CreateDeadlineInput`):

```json
{
  "title": "Klageerwiderung",
  "rule_code": "RoP 23",
  "due_date": "2026-08-20",
  "original_due_date": "2026-08-20",
  "source": "fristenrechner",
  "rule_id": "<sequencing_rules.id>",       /* maps to deadlines.sequencing_rule_id */
  "notes": "..."
}
```

**audit_reason wording (per Q12):** every row inserted via this flow carries an audit-log breadcrumb on the project (matches the deadline `Verlauf` pattern). Default reason string:

> `Aus Fristenrechner — Trigger: {trigger_event_name} ({trigger_date_iso})`

e.g. `Aus Fristenrechner — Trigger: Klageerhebung (2026-05-20)`. Falls into `paliad.project_events` with `kind='deadline_created'` via the existing `DeadlineService.CreateBulk` audit hook; no schema change needed.

---

## 5. URL / state representation

The new flow keeps Pathway-B's URL-as-state contract, simplified:

| Param | Owner | Meaning |
|---|---|---|
| `project` | Step 0 | Active project UUID. Drives the prefills. |
| `mode` | mode tab | `wizard` (default) or `search`. |
| `q` | Mode A | Free text query. |
| `forum` | Mode A | Comma-separated forum codes (`upc,de`). Mode B writes this only when the wizard derives it. |
| `pt` | Mode A | Selected proceeding_type code. |
| `kind` | Mode A | event_kind chip pick. |
| `party` | both | Perspective. Mode A's chip; Mode B's R5. |
| `wizard` | Mode B | Dotted state cursor encoding which row is active and the picks made: `wizard=kind:filing,forum:upc,pt:upc.inf.cfi,active:event`. |
| `event` | both | The locked trigger `procedural_events.code`. Once set, the result view renders. |
| `trigger_date` | result | ISO date. Default = today; URL only carries it when overridden. |
| `selected` | result | Comma-separated `sequencing_rules.id` checkbox state. Only carried when it differs from the priority default. |

Deep links work end-to-end: `?project=…&event=upc.inf.cfi.soc&trigger_date=2026-05-20&selected=…` jumps a colleague straight to the result view with the same picks. (Per Q11 — query string, not pathname.)

`popstate` rebuilds the page from the params alone (same pattern as today). The wizard state cursor lets browser back/forward step the wizard rows instead of dropping back to the page root.

---

## 6. Backend contract changes

### 6.1 Extend `/api/tools/fristenrechner/search`

Today returns concept-cards. Add an alternate response shape (or a `?kind=events` flag) that returns `procedural_events` rows directly:

```json
{
  "query": "Klageerhebung",
  "filters": { "forum": "upc", "pt": null, "kind": "filing", "party": null },
  "events": [
    {
      "id": "<uuid>",
      "code": "upc.inf.cfi.soc",
      "name_de": "Klageerhebung",
      "name_en": "Statement of Claim",
      "event_kind": "filing",
      "proceeding_type": { "code": "upc.inf.cfi", "jurisdiction": "UPC", "name": "..." },
      "follow_up_count": 3,
      "concept_id": "<uuid>",
      "score": 0.92
    }
  ],
  "total": 12
}
```

The concept-card shape stays available for the legacy Pathway-B-filter route (kept as a deep-link compat surface, not user-facing).

### 6.2 New `/api/tools/fristenrechner/follow-ups`

Given a trigger event id + trigger date + optional party qualifier, return the follow-up `sequencing_rules` rows, grouped + with computed dates. Wire shape:

```json
{
  "trigger": { "id": "...", "code": "upc.inf.cfi.soc", "name_de": "Klageerhebung", "event_kind": "filing", "proceeding_type": { "code": "upc.inf.cfi", "name_de": "Verletzungsverfahren", "jurisdiction": "UPC" } },
  "trigger_date": "2026-05-20",
  "party": "claimant",
  "follow_ups": [
    {
      "rule_id": "<uuid>",
      "title_de": "Klageerwiderung",
      "title_en": "Defence",
      "priority": "mandatory",
      "primary_party": "defendant",
      "duration_phrase": "3 Monate",
      "due_date": "2026-08-20",
      "is_court_set": false,
      "is_spawn": false,
      "condition_expr": null,
      "rule_code": "RoP 23",
      "notes_de": "...",
      "spawn_label": null,
      "spawn_proceeding_type": null,
      "appeal_target": null
    }
  ]
}
```

Implementation: `FristenrechnerService.LookupFollowUps(ctx, eventID, triggerDate, party)` — wraps `catalog.LookupEvents(axes={EventID:…, Depth:Next})` (already implemented for the Litigation Planner per `services/fristenrechner.go:251`) and runs the result through `pkg/litigationplanner.Calculate` to fill the dates. The calculator is unchanged.

### 6.3 No schema changes

This design is pure UX + handler shape. The unified `sequencing_rules` model already has every column needed (priority, condition_expr, spawn_*, is_court_set, primary_party, applies_to_target). No migration accompanies this design.

---

## 7. Migration plan — from current row stack to the overhaul

Drop nothing on day one; co-exist for one release. The cutover is by URL flag.

| Phase | What changes | What survives | Branch |
|---|---|---|---|
| **S1 — Backend** | Add `GET /search?kind=events`. Add `GET /follow-ups`. Both feature-flagged behind a request header (off by default). | Existing endpoints. | one PR |
| **S2 — Result view** | New `frontend/src/client/fristenrechner-result.ts` module — given a trigger event + date, render the §4 result view. Mount under a `?overhaul=1` query flag on /tools/fristenrechner. The legacy `renderProcedureResults` stays. | All today's UI. | one PR |
| **S3 — Mode A** | New search-with-filter-chips UI. Mount alongside the row stack under `?overhaul=1`. | Row stack still primary. | one PR |
| **S4 — Mode B (wizard)** | New `frontend/src/client/fristenrechner-wizard.ts` — the 3-5 row stack. Replaces today's `buildRowStack` only when `?overhaul=1`. Project prefill logic from `buildRowStack` ports 1:1. | The legacy row stack stays in place under no flag. | one PR |
| **S5 — Flip the flag** | `?overhaul=1` becomes the default. Legacy row stack and `event_categories`-based cascade rendered with a hard-coded `?legacy=1` for two weeks. | Procedure mode (the upper half of `fristenrechner.tsx`) is unchanged throughout. | one PR |
| **S6 — Cleanup** | Drop the `buildRowStack` function tree and the `event_categories`-served cascade endpoint (the table can stay — it's still semantically a useful taxonomy for future tools, just not the Fristenrechner's UI). Drop the `HIDDEN_CASCADE_ROOTS` constant and the cascade-segment bridge. | None of today's row-stack code. | one PR |

Single project per slice; each PR rebases off main; no shared branches.

The `event_categories` table itself **stays** — `audit-fristen-logic-2026-05-13.md` §2.4 already calls it "a config layer" useful for taxonomy work. The Fristenrechner just no longer reads it. Future tools (the "Ich möchte einreichen" forward-workflow surface m hid in `HIDDEN_CASCADE_ROOTS`) can resurrect it without DB migration.

---

## 8. Worked example — "PA at LG Düsseldorf bekommt einen Hinweisbeschluss via CMS in einer aktiven Akte"

Project: `HL-2024-001`, proceeding_type=`de.inf.lg` (Verletzungsverfahren LG), `our_side='defendant'`, `court='LG Düsseldorf'`.

### 8.1 Wizard path (Mode B, default)

User opens /tools/fristenrechner with that project in Step 0. Mode tab defaults to "🧭 Geführt".

Wizard rows render top-to-bottom, pre-filled where the project implies:

```text
[1] Was ist passiert?            [ active — chips for filing/hearing/decision/order/missed ]
[2] Vor welchem Gericht?         ✓ LG (aus Akte: HL-2024-001)        ← prefilled+collapsed
[3] In welchem Verfahren?        ✓ Verletzungsverfahren (de.inf.lg)  ← prefilled+collapsed
```

User clicks ⚖️ Entscheidung in R1.

Row stack updates:
```text
[1] Was ist passiert?            ✓ Entscheidung                       ← answered
[2] Vor welchem Gericht?         ✓ LG (aus Akte)                      ← prefilled
[3] In welchem Verfahren?        ✓ Verletzungsverfahren (de.inf.lg)   ← prefilled
[4] Welche Entscheidung konkret? [ active — chips: Urteil, Beschluss, Hinweisbeschluss, ... ]
```

R4 chip set is the `procedural_events` whose `proceeding_type_id` = de.inf.lg AND `event_kind` = 'decision'. (Hinweisbeschluss is in this set — `de.inf.lg.hinweisbeschluss` or similar.)

User clicks Hinweisbeschluss. The wizard checks: do the follow-up rules differ by `primary_party`? In this case yes (the Hinweis triggers a reply window for the defendant only). So R5 fires:

```text
[5] Welche Seite vertreten Sie?  ✓ Beklagtenseite (aus Akte)          ← prefilled
```

R5 is pre-filled from `project.our_side='defendant'`. The user could click ändern to override, but doesn't.

Wizard transitions to the §4 result view. Trigger card: "📜 Hinweisbeschluss · de.inf.lg · LG · Beklagtenseite". Trigger date defaults to today.

### 8.2 Result view

Three follow-ups in scope (illustrative):

```text
MANDATORY
  ☑ Stellungnahme zum Hinweisbeschluss (Frist 4 Wochen) — 24.06.2026 — ZPO §139
RECOMMENDED
  ☑ Anpassung der Klageerwiderung — 24.06.2026 — best practice
OPTIONAL
  □ Antrag auf Fristverlängerung (begründet) — auf Antrag
```

User unchecks "Anpassung", changes the Stellungnahme date inline to 2026-06-20 (one weekday earlier), clicks "In Akte HL-2024-001 eintragen ▶".

Modal opens with the 1 selected deadline + the user's date override. User confirms.

### 8.3 Write-back

Server-side: `POST /api/projects/HL-2024-001/deadlines/bulk` with one `CreateDeadlineInput`:

```json
{
  "title": "Stellungnahme zum Hinweisbeschluss",
  "rule_code": "ZPO §139",
  "due_date": "2026-06-20",
  "original_due_date": "2026-06-24",
  "source": "fristenrechner",
  "rule_id": "<sr-uuid>",
  "notes": null
}
```

`DeadlineService.CreateBulk` inserts the row into `paliad.deadlines` (with `sequencing_rule_id` populated from `rule_id`), creates the audit event with the wording "Aus Fristenrechner — Trigger: Hinweisbeschluss (2026-05-26)", and the user is redirected to `/deadlines?project_id=…` with a green success toast.

### 8.4 Mode A path for the same user

User flips the mode tab to "⚡ Direkt suchen". Filter chips auto-load to Forum=DE + Proceeding=de.inf.lg (from project context). User types "Hinweis" — the result list shows `de.inf.lg.hinweisbeschluss` (and maybe `upc.inf.cfi.hinweis` filtered out because Forum=DE narrows it). User clicks. Same result view appears.

Total clicks Mode A: 2 (type + click). Mode B: 2 (R1 chip + R4 chip; R2/R3/R5 prefilled). The wizard wins for trainees who don't know vocabulary; search wins for power users who know "Hinweisbeschluss" and can type 4 chars.

---

## 9. What's NOT in scope

- **Replacing the `sequencing_rules` model.** Phase 3 schema is already what the calculator runs on.
- **Paliadin (LLM) integration into the wizard.** A "Frist-Extraktion aus Dokument" path is filed elsewhere (memory `b6a11b55…`) and stays out of this design. The wizard could later call out to Paliadin for "the user typed something we don't know" — Phase 2 of *this* overhaul, not Phase 1.
- **Calendar / Outlook sync** of created deadlines. Separate t-paliad ticket per project-status.md long-term goals.
- **Editing `sequencing_rules`** from the result view. Read-only here. The admin surface at `/admin/procedural-events` handles editing.
- **The Procedure-mode surface** (upper half of `fristenrechner.tsx`). The proceeding-picker + trigger-date + flag-checkbox UI stays exactly as it is today. That surface answers a different question ("show me the full procedural ablauf for upc.inf.cfi") and is the right tool for that question; the overhaul targets only the Pathway-B / row-stack half of the page.

---

## 10. Open questions for m (12 questions, batched for `AskUserQuestion`)

All 12 questions tracked in m/paliad#146 § "Open design questions". Each gets a recommended option (listed first in the AskUserQuestion call). Bundled into 3 batches of 4.

| # | Topic | Recommended pick |
|---|---|---|
| Q1 | Single page or stepper? | Single page with mode-tabs + collapsible rows. |
| Q2 | Mode switcher placement | Tab pair under Step-0 ("Akte / kontextfrei"). |
| Q3 | Filter-vs-qualifier UX | Qualifiers carry a small "(Pflichtangabe)" tag; filters render in a slimmer pill. |
| Q4 | Cascade tree (keep/replace) | Replace with the 5-question wizard. Drop `event_categories` from the Fristenrechner UI (table stays). |
| Q5 | Result grouping | 4 visible groups (Mandatory / Recommended / Optional / Conditional), SPAWNED folded with badge. |
| Q6 | Project write-back UX | Confirm-and-edit-dates modal (revise each date once before commit). |
| Q7 | No-project mode | CTA disabled with hint ("Wähle eine Akte oben"). Match today's pattern. |
| Q8 | Perspective semantics by mode | Mode B (file): qualifier — required pick. Mode A (search): filter — optional. |
| Q9 | Trigger-date input timing | In the result-view trigger card; default today; inline editable. |
| Q10 | Backward navigation | Preserve compatible downstream picks; reset only those invalidated. |
| Q11 | Deep-link encoding | Query string (`?event=…&trigger_date=…`). |
| Q12 | Audit reason wording | `Aus Fristenrechner — Trigger: {name} ({date})`. |

(Recommendations land as the "first option" in each AskUserQuestion call per the inventor SKILL contract.)

---

## 11. m's decisions (2026-05-26)

All 12 questions answered via `AskUserQuestion` on 2026-05-26 21:30. Recording each pick + the reasoning where it diverges from the inventor's recommendation. Sections of the design that are now load-bearing on these answers carry a "(m §11.Q{n})" cross-reference.

- **Q1 (Page layout): Single page, mode-tabs.** [= recommendation] Both modes share /tools/fristenrechner; the mode-tabs swap the question surface in place. Result view is shared. **Locks §3, §4, §5.**
- **Q2 (Mode switcher): Tab pair under Step-0.** [= recommendation] "⚡ Direkt suchen" / "🧭 Geführt" tabs render directly below the Akte picker. Project context survives the tab flip; compatible filter picks (forum, proceeding) carry across.
- **Q3 (Filter-vs-qualifier UX): Section split — Filter above, Qualifier below.** [≠ recommendation; m picked option 2.] Mode A's filter chips render in a "Filter (eingrenzen)" strip on top; below it, the result list is the qualifier surface (clicking a row locks). Mode B wizard rows carry a small "Filter" / "Qualifier" badge in the row badge area (e.g. R1/R2 = Filter, R3/R5 = Qualifier). The "(Pflichtangabe)" tag from the original recommendation is replaced by this section-level visual hierarchy. **Updates §3.1 (Mode A layout) and §3.2 (wizard row badges).**
- **Q4 (Cascade tree): Replace with wizard, keep table.** [= recommendation] The Fristenrechner UI stops reading `paliad.event_categories`. The table stays for future tools (the hidden "Ich möchte einreichen" forward-workflow). **Locks §3.2 and the cleanup in §7 S6.**
- **Q5 (Result grouping): 4 groups + SPAWNED badge.** [= recommendation] Mandatory / Recommended / Optional / Conditional are the four sub-sections; spawned rules fold into their priority bucket with a `⇲ neues Verfahren` badge. **Locks §4.2.**
- **Q6 (Write-back UX): Confirm-and-edit-dates modal.** [= recommendation] Inline checkbox selection in the result view → "In Akte eintragen ▶" → modal with editable due-date fields per row + Akte picker. **Locks §4.4.**
- **Q7 (No-project mode): Hide the CTA entirely.** [≠ recommendation; m picked option 3.] In kontextfrei mode the result view renders without the write-back footer at all — no disabled-with-hint button. Rationale (inferred from m's pick): the result view is informational by design in explore mode, and a permanently-disabled CTA is visual noise. **Updates §4.4** — the CTA is conditional on `project != null`, not on `disabled`. The hint message moves into the Step-0 picker: when a user is in kontextfrei mode and reaches a result view, a one-line nudge appears above the result groups ("Tipp: Wähle oben eine Akte, um diese Fristen einzutragen") with a link to focus the Akte picker. This preserves the affordance discovery without polluting the footer.
- **Q8 (Perspective semantics): Mode B qualifier, Mode A filter.** [= recommendation] Wizard Mode B's R5 is required and Klagerseite/Beklagtenseite only (no "Beide"); Mode A's perspective chip is a filter with a "Beide" option, off by default. **Locks §2 axis table and §3.2 R5 description.**
- **Q9 (Trigger-date input): In the result-view trigger card.** [= recommendation] The sticky header card on the result view shows the date; default today; inline editable. Changing it re-renders follow-up dates live. **Locks §4.1.**
- **Q10 (Backward navigation): Preserve compatible picks.** [= recommendation] Re-opening any wizard row keeps downstream picks that are still legal under the new upstream value; resets only the picks the new value invalidates. A small chip-strip annotation ("erhalten") appears for one render-cycle on rows whose pick was carried so the user notices. **Updates §3.2 branching policy.**
- **Q11 (Deep-link encoding): Query string.** [= recommendation] `?project=…&mode=…&event=…&trigger_date=…&selected=…&forum=…&pt=…&kind=…&party=…` — every state piece is a query param. `popstate` rebuilds the page from params. **Locks §5.**
- **Q12 (Audit reason wording): `Aus Fristenrechner — Trigger: {name} ({date})`.** [= recommendation] German-locale, includes the trigger event name and its ISO date. Stored as `paliad.project_events.metadata->>'audit_reason'` via the existing `DeadlineService.CreateBulk` audit hook. **Locks §4.4.**

### 11.1 What changed from the strawman as a result

Two follow-on edits flow from m's picks:

1. **§3.1 Mode A layout** — top strip is "Filter (eingrenzen)" with the four filter chip groups (Forum · Proceeding · Event-Kind · Partei); the result list directly below carries the implicit "click here to lock" qualifier action. No "(Pflichtangabe)" tag.
2. **§4.4 Write-back footer** — the footer is rendered conditionally on `project != null`. The kontextfrei-mode informational nudge moves into the result view body above the deadline groups.

These edits don't change the §7 migration plan or the §6 backend contracts.

---

## 12. Synthesis links

- mBrian topic: `topic-fristenrechner` (existing) — file this design as a `[synthesis]` node linked `triggered_by` t-paliad-322 and `related_to` the row-cascade + Phase 2 designs.
- Related memories: row-cascade design `0fbd2c1a-…`, Phase 2 design `a454dc86-…`, audit logic `f6c0c3a2-…`.
