# 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": "", /* 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": "", "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": "", "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": "", "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": "", "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-…`.