diff --git a/docs/design-tools-cleanup-2026-05-12.md b/docs/design-tools-cleanup-2026-05-12.md new file mode 100644 index 0000000..cec041d --- /dev/null +++ b/docs/design-tools-cleanup-2026-05-12.md @@ -0,0 +1,569 @@ +# Design — Tools surface cleanup (Fristenrechner vs Verfahrensablauf split) + +**Author:** kelvin (inventor) +**Date:** 2026-05-12 +**Task:** t-paliad-178 +**Status:** READY FOR REVIEW — m gates inventor → coder transition. + +--- + +## 0. Premises verified live (before designing) + +CLAUDE.md / memory / the task brief can all drift. Each anchor below is verified against the live codebase or DB on `mai/kelvin/inventor-tools-surface` (baseline commit `54b227c`). + +- **One route + one TSX serve both nav entries today.** `/tools/fristenrechner` is the only registered page route (`internal/handlers/handlers.go:162`). Both sidebar entries (Fristenrechner + Verfahrensablauf) target the same Bun-built `dist/fristenrechner.html` and disambiguate purely through `?path=a` and a client-side active-class fix-up (`frontend/src/client/sidebar.ts:447 fixVerfahrensablaufActive`). Confirmed: the live HTML pulled from paliad.de (auth-gated → 302 to login, served-bytes match) is the shell rendered by `frontend/src/fristenrechner.tsx:87 renderFristenrechner`. +- **The client runtime is 3 559 lines, not the 2 700+ quoted in the task brief.** `frontend/src/client/fristenrechner.ts` carries Step 1 / Step 2 / Step 3a / Pathway A wizard / Pathway B cascade + filter / search + cascade engines / column + timeline result-card renderers in one IIFE bundle (`Pathway` type at line 2315, `showPathway()` at line 2370, `showBMode()` at line 2406). Any "separate route" path must either lift code out of this bundle into a shared module or accept a larger duplicated bundle on the new page. +- **Sidebar deep-link `?path=a` lands on Pathway A directly, NOT on the Akte picker.** I traced `initPathwayFork → readPathwayFromURL → showPathway("a")`: it sets `step1.style.display = "none"`, `step2.hidden = true`, `step3a.hidden = true`, `pathway-a.hidden = false`. The user sees the wizard's "Verfahrensart wählen" tile picker first. The task brief's phrasing — "still drops users at Step 1 (Akte-Picker)" — is the perceived UX from the wizard's own internal "wizard-step-1" labelled "Verfahrensart wählen". Mental model: two surfaces with the same nav label "Step 1" muddy intent; the fix m wants is structural (a dedicated route), not a JS bug fix. +- **`paliad.projects.court` is a free-text column, NOT an FK to `paliad.courts`.** Confirmed in `information_schema.columns`. Live values: `LG München I` (1 row), `UPC` (2), `UPC CoA` (1). The task brief's "project has a court FK" is **wrong**; only `proceeding_type_id` is a real FK. The design must NOT silently auto-pick a `paliad.courts.id` from `projects.court` — fuzzy mapping is best-effort + always overridable, never silent. +- **`paliad.projects.proceeding_type_id` points at `category='litigation'` rows (7 codes: INF, REV, CCR, APM, APP, AMD, ZPO_CIVIL).** The Fristenrechner wizard accepts `category='fristenrechner'` codes (20 codes: UPC_INF, DE_INF, EPA_OPP, …). These overlap conceptually (`INF` is the abstract noun behind both `UPC_INF` and `DE_INF`) but are different rows. Auto-derivation needs a small mapping: `litigation_code × jurisdiction → fristenrechner_code`. Example: `INF + UPC → UPC_INF`. `INF + DE → DE_INF` (first instance). The instance dimension (LG / OLG / BGH) is **not** on `paliad.projects` today, so DE_INF_OLG / DE_INF_BGH cannot be inferred — only the first-instance code can be. +- **`paliad.projects` carries no `priority_date` or `trigger_date` column.** It does have `filing_date` and `grant_date`. Only EP_GRANT.ep_grant.publish (Art. 93 EPÜ) is anchored on `priority_date` today (via `anchor_alt`). For Akte-driven prefill, `priority_date` stays blank by default and the user fills it. +- **`paliad.projects.our_side` and `paliad.projects.counterclaim_of` exist** (already exploited by t-paliad-164 perspective-chip predefine and the parent-counterclaim link respectively). These two columns are the actual hooks for "consolidated timeline" vs "side-by-side lanes" — see §6. +- **`deadline_rules.condition_flag` is a real text[] column with exactly 4 distinct value-sets in production:** `[with_amend]` (4 rows), `[with_cci]` (4), `[with_ccr]` (5), `[with_ccr, with_amend]` (4). Only `UPC_INF` (proceeding_type_id=8) and `UPC_REV` (proceeding_type_id=9) carry variant-flagged rules. Every other proceeding type renders a single canonical timeline today. **This is the hard data bound on the variant-chip design** — chips beyond these three flags would have no rules to flip and must be marked "future". +- **Court-specific rule overrides do not exist as a mechanism.** `CourtID` in `CalcOptions` (`internal/services/fristenrechner.go:107`) only switches the holiday calendar (via `courts.CountryRegime`). There is no per-court rule branch. "UPC LD Mü vs LD Düsseldorf" overrides are NOT a thing — they'd need a new column on `deadline_rules`. +- **Expedited-vs-standard distinctions do not exist** either. No `condition_flag` row matches an expedited concept. Adding one is a schema-and-seed change, out of scope here. +- **Result rendering today** lives in `renderTimelineBody` and `renderColumnsBody` (`frontend/src/client/fristenrechner.ts:637 / :664`). The user toggles between the two with a radio (`#fristen-view-toggle`). Both renderers take a single `DeadlineResponse` and emit DOM strings; neither knows about "two timelines side by side". A consolidated-vs-lane view (§5–§6) is a renderer-level change, not a backend one. +- **The Step 1/Step 2/Step 3a/Pathway A/B layout shipped under t-paliad-133 + t-paliad-168.** The "Verfahrensablauf einsehen" card (Step 2 third option, lines 215-223 of fristenrechner.tsx) was added in t-paliad-168 specifically to give the abstract-browse case a discoverable entry. If Verfahrensablauf moves to its own route, the third card becomes redundant (§9). + +If any of these conflict with what the task brief asserts, **the live state wins** and the brief is the bug — flagged in §13 for m. + +--- + +## 1. Vision + scope + +m's framing (verbatim from the task brief): + +> Users want to **either** (1) determine a deadline — possibly Akte-scoped, possibly abstract — **or** (2) browse a typical Verfahrensablauf abstractly with variant options. + +The two intents are **fundamentally different**: + +- **Determine a deadline** ends with a save (or a print, or a manual transcription) of a *specific* date attached to *something* — a project, or a sticky-note in the user's head. +- **Browse a Verfahrensablauf** ends with the user understanding the *shape* of a proceeding — no date binding required. + +Today both intents collapse onto one URL because the wizard infrastructure is shared. The cost: two sidebar entries pointing at the same shell, an active-class fix-up script (`fixVerfahrensablaufActive`), and a Step 1 ("Welche Akte?") frame that doesn't match the abstract-browse intent. + +### Scope of this design + +1. **Page surface split** — separate routes per intent. `/tools/fristenrechner` keeps the deadline-determination intent (Akte-scoped *or* abstract). `/tools/verfahrensablauf` becomes the dedicated abstract-browse surface with variant chips + side-by-side compare. +2. **Step 0 "Abstrakt oder Akte?"** as the FIRST affordance on `/tools/fristenrechner`. Pick → narrows downstream inputs. +3. **Akte-driven auto-derivation** — map project columns to wizard inputs and flag the gaps. +4. **Variant chips + consolidated-vs-lane view** for `/tools/verfahrensablauf`. +5. **Side-by-side compare** on `/tools/verfahrensablauf` (max 2 timelines for v1). +6. **Sidebar labels + URL conventions** post-split. +7. **Mobile responsive** plan. +8. **What gets dropped** (Step 2 browse card, sidebar fix-up script). + +### Explicitly out of scope (per task brief) + +- Deadline-rule data-model changes (court-specific overrides, expedited-flag, new condition_flag values). Audited in §0, propose nothing here. +- t-paliad-166 Determinator B1 cascade redesign — separate ticket, on-hold. Pathway B continues to exist inside `/tools/fristenrechner`; we note interplay in §11 but do not pre-empt. +- t-paliad-157 Fristenrechner interactive-UX pair session — on-hold. The cleanup here may inform it, but we don't dictate it. +- Project Verlauf tab (`/projects/{id}` → Verlauf). Stays as-is. SmartTimeline renders concrete-per-case via `internal/services/projection_service.go`; no Tool-side mirror. +- New backend services. The split runs on the existing `POST /api/tools/fristenrechner` + `POST /api/tools/event-deadlines` endpoints; we add at most one helper for Akte → fristenrechner-code mapping. +- Backend rule changes — touch the substrate only enough to verify what the design needs is already there. + +--- + +## 2. Page surfaces + route split + +m has already chosen **Option A** in the task brief: split by intent, separate URLs. The design here implements that choice. For honesty I also note the alternatives I considered and why A still wins after audit. + +### 2.1 Three options weighed + +| Option | URL shape | Trade-off | Verdict | +|---|---|---|---| +| **A — Two routes** | `/tools/fristenrechner` + `/tools/verfahrensablauf` | Clean mental model. Sidebar entries map 1:1 to URLs. `fixVerfahrensablaufActive` dies. Two HTML files; shared client code lifted into a module. | **Picked.** Aligns with intent split. | +| **B — One route, `?mode=` fork** | `/tools/fristenrechner?mode=calc` vs `?mode=browse` | Single HTML bundle, no shared-module lift. But: sidebar entries still alias the same page; muddled intent stays in the user's head; we'd still need a Step 0 inside the calc mode. | Rejected by m. Verifies on second look: it just moves `?path=a` to `?mode=browse`, doesn't fix the problem. | +| **C — Move into Patentglossar** | Verfahrensablauf renders inline on glossary pages | Discoverability shrinks. Glossary entries are concept-bounded; Verfahrensablauf is procedure-bounded. The two indexes don't map. | Rejected by m. | + +### 2.2 Code-reuse strategy under Option A + +The honest cost of splitting routes is shared-client-code duplication. Today `client/fristenrechner.ts` (3 559 LoC) bundles everything. The Verfahrensablauf-only surface needs: + +- The proceeding-type tile picker (`UPC_TYPES`, `DE_TYPES`, `EPA_TYPES`, `DPMA_TYPES` arrays in `fristenrechner.tsx`). +- The timeline + columns result renderers (`renderTimelineBody`, `renderColumnsBody`). +- The `POST /api/tools/fristenrechner` calc invocation. +- Court picker + holiday-calendar pickup (read-only). +- DE/EN i18n for the timeline rows. + +It does NOT need: + +- Step 1 Akte picker / ad-hoc chip / Step 1 summary. +- Step 2 file/happened/browse cards. +- Step 3a outgoing-intent chooser. +- Pathway B cascade + filter + perspective + inbox chips (~1 200 LoC). +- Save-to-Akte modal. +- Trigger-event mode (`mode-event-panel`). + +**Plan:** lift the deadline-timeline core (proceeding picker + calc + render) into `frontend/src/client/views/verfahrensablauf-core.ts`. Both pages import it. Pathway B + Save modal + Step machinery stay in `client/fristenrechner.ts`. Estimated lifted surface: ~700–900 LoC. New code on `verfahrensablauf.ts` (variant chips + lane mode + compare): ~400–600 LoC. + +This keeps the IIFE per-page bundle pattern intact (one entry per route in `frontend/build.ts:228`). No runtime npm dep added. + +### 2.3 The two pages in one sentence each + +- **`/tools/fristenrechner`** — Deadline determination. Optional Akte scope. Ends in "save / print / done". +- **`/tools/verfahrensablauf`** — Procedural shape browser. No Akte. Ends in "now I understand the shape". + +### 2.4 Sidebar + +```text +Werkzeuge + Fristenrechner → /tools/fristenrechner + Verfahrensablauf → /tools/verfahrensablauf + Kostenrechner → /tools/kostenrechner + … +``` + +`fixVerfahrensablaufActive` deletes; the SSR-time `navItem` helper handles both active classes natively because the hrefs differ on pathname. + +--- + +## 3. Step 0 "Abstrakt oder Akte?" on `/tools/fristenrechner` + +m's lock-in: Step 0 comes FIRST. Today's Step 1 (Akte picker) forces the user to either commit to an Akte or escape via ad-hoc chips before anything else moves. Step 0 makes the binary choice explicit. + +### 3.1 Affordance — three sketches considered + +**Sketch A — Radio toggle (Recommended).** +A pair-of-toggle at the top of the page, wide on desktop, stacked on mobile. The currently-active half expands into its full picker; the inactive half collapses to a slim header that the user can click to flip. + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Schritt 0 — Wie wollen Sie die Frist bestimmen? │ +│ │ +│ ◉ Mit Akte verknüpfen ○ Abstrakt — ohne Akte │ +│ ────────────────────────────────────────────────────────────│ +│ │ +│ 🔍 Akte suchen… │ +│ [Akte 1 · CLI-2024 — Foo GmbH vs Bar Ltd. — UPC LD Mü] │ +│ [Akte 2 · …] │ +│ ──── │ +│ + Neue Akte anlegen │ +│ │ +└──────────────────────────────────────────────────────────────┘ +``` + +When the user picks "Abstrakt": + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Schritt 0 — Wie wollen Sie die Frist bestimmen? │ +│ │ +│ ○ Mit Akte verknüpfen ◉ Abstrakt — ohne Akte │ +│ ────────────────────────────────────────────────────────────│ +│ │ +│ Verfahrensart wählen: │ +│ [UPC] [DE] [EPA] [DPMA] ← jurisdiction picker (4 tabs) │ +│ (then proceeding-type tiles within the chosen tab) │ +│ │ +└──────────────────────────────────────────────────────────────┘ +``` + +**Why I'd recommend this:** the toggle is a single decision, declared up-front, with the consequence visible inline. No modal dismissal cost. Keyboard navigation natural. On mobile it stacks to two stacked rows where the active row expands and the inactive row stays a touch-target. + +**Sketch B — Two big cards.** Like today's Step 2 cards but at the very top. Pro: pretty + tappable. Con: click-and-commit feels heavier than a toggle; "going back" reads as undoing a choice instead of flipping it. + +**Sketch C — Modal-before-render.** Most decisive, also most annoying — the user can't even see the page before the dialog clears. Reject. (Modals interrupt; we want the user oriented before they're asked.) + +### 3.2 URL state + +Step 0 binds to `?mode=akte|abstract` in the URL. + +- `?mode=akte&project=` — Akte selected. Court / proceeding-type / our_side auto-derived (§4). +- `?mode=abstract&forum=upc|de|epa|dpma` — abstract. Jurisdiction tab selected; proceeding-type tiles below. +- `?mode=` absent — render Step 0 with no preselection. + +Deep-link from `/projects/{id}` → "Frist berechnen" button passes `?mode=akte&project=` and lands on Step 0 with Akte branch already filled. + +`localStorage["paliad.fristen.mode"]` remembers the user's last choice for soft re-entry (the `PATHWAY_STORAGE_KEY` pattern already exists). + +### 3.3 Removal of today's Step 2 fork (file / happened / browse) + +With Step 0 making the intent binary, the file-vs-happened branching collapses into one wizard with two anchor sources: + +- **Akte mode** — wizard pre-filled. After calc, the save CTA is "An Akte hängen". `?path=` machinery shrinks because Pathway A vs Pathway B becomes a wizard *step* (incoming-event vs outgoing-event), not a top-level path. +- **Abstract mode** — wizard takes proceeding-type + date as today. After calc, save CTA disabled (no Akte to save against); `Drucken` remains. + +The "Verfahrensablauf einsehen" card is gone from `/tools/fristenrechner` (its purpose lives on `/tools/verfahrensablauf` now — §9). + +Pathway B (the cascade) is **kept** as a separate entry-flow inside Akte-mode for "Etwas ist passiert" — the t-paliad-166 redesign is on-hold and we don't pre-empt it. In abstract mode Pathway B is reachable via a "Frist aufgrund Ereignis (Determinator)" link in the result panel; the cascade itself unchanged. + +--- + +## 4. Akte-driven auto-derivation + +When `mode=akte&project=`, the wizard prefills as much as it honestly can from `paliad.projects`. The rest stays empty + visible. + +### 4.1 Mapping table + +| Wizard input | Project source | Confidence | Behaviour | +|---|---|---|---| +| **proceeding_type_code** (UPC_INF, DE_INF, …) | `proceeding_types.code` via `projects.proceeding_type_id` + jurisdiction disambiguation | medium-high | Best-effort pick + the proceeding-tile picker stays visible with the picked tile pre-selected. User can flip. | +| **trigger_date** | None today | low | Always empty. User fills. | +| **priority_date** (EP_GRANT only) | `projects.grant_date` or `projects.filing_date` (parent patent project's filing) | low-medium | Pre-fill only when the chosen proceeding is `EP_GRANT`. Field stays visible + editable. | +| **court_id** | `projects.court` (free text) — fuzzy match against `paliad.courts.code` | low | Pre-select if string-match is exact-or-trivial-canon (e.g. `"UPC"` → `upc-cd-...`? **No** — too ambiguous; leave blank); else leave blank. Picker visible + required for UPC where holiday calendar differs. | +| **our_side** (perspective chip) | `projects.our_side` | high | Already wired (t-paliad-164). Predefine + show "vorgegeben durch Akte" hint. | +| **condition_flag** (with_ccr, with_cci, with_amend) | None today | low | Stays user-driven. Flag checkboxes appear conditionally on UPC_INF/UPC_REV. | +| **counterclaim sibling info** | `projects.counterclaim_of` | medium | If set, the result panel shows a small "Verbundenes Verfahren: " line with a deep-link to the parent's Verlauf tab. Informational only — doesn't change calc. | + +### 4.2 Litigation → fristenrechner code mapping + +`projects.proceeding_type_id` points to `category='litigation'` rows. The wizard wants `category='fristenrechner'`. The mapping is multi-key: + +| litigation code | jurisdiction | resolved fristenrechner code | +|---|---|---| +| `INF` | UPC | `UPC_INF` (id 8) | +| `INF` | DE | `DE_INF` (id 12) — first instance only; OLG/BGH not derivable | +| `REV` | UPC | `UPC_REV` (id 9) | +| `REV` | DE | `DE_NULL` (id 13) | +| `CCR` | UPC | `UPC_REV` (id 9) + `with_cci` flag suggested | +| `APM` | UPC | `UPC_PI` (id 10) | +| `APP` | UPC | `UPC_APP` (id 11) | +| `AMD` | UPC | (no direct fristenrechner code; suggest UPC_INF with `with_amend`) | +| `ZPO_CIVIL` | DE | `DE_INF` (id 12) — fallback | + +The jurisdiction comes from `proceeding_types.jurisdiction` (UPC / DE / EPA / DPMA) on the project's own proceeding_type row, not from `projects.country` directly (which is a different axis — country of patent, not of forum). + +Implementation: a helper `services.ResolveFristenrechnerCodeForProject(projectID)` returning `(code, confidence, reason)` so the UI can render "Vorgeschlagen: UPC_INF (aus Akte abgeleitet — Sie können umstellen)". Where confidence is `low`, no preselect — user picks. + +### 4.3 Court free-text — no silent FK promotion + +`projects.court` is a free-text field. Live values include `"UPC"` (ambiguous: which division?), `"UPC CoA"` (matches `upc-coa-luxembourg`), `"LG München I"` (matches `de-lg-muenchen1`). I deliberately do NOT auto-pick a `paliad.courts.id` from this string in v1: the cost of a wrong silent pick (a holiday-calendar mismatch invalidating a calculated date) is high; the benefit of saving one click is low. The Court picker stays visible and **required** for UPC proceedings (already today's behaviour via the `isCourtDeterminedRule` check in `internal/services/fristenrechner.go:779`). + +If the free-text value matches a canonical `paliad.courts.code` exactly (case-insensitive), we *highlight* the matching option but do not auto-select. The user clicks to confirm. + +Follow-up ticket worth filing (out of scope here): migrate `projects.court` from text to `court_id` FK. That'd land a real auto-derivation. Until then, this design treats it as a hint. + +### 4.4 Edge case — Akte without a proceeding_type_id + +11 of 11 live projects today have no `proceeding_type_id` set yet. Behaviour: the wizard renders with all proceeding-type tiles selectable, no preselect, no hint. Functionally identical to abstract mode but with the Akte locked for save-CTA. No error state — silent graceful degradation. + +--- + +## 5. Variant chips on `/tools/verfahrensablauf` + +The new dedicated route renders proceeding-shape with the user toggling "what variant am I looking at?". Variants are the live `condition_flag` mechanism. + +### 5.1 Variants that exist today (audited live) + +Only **UPC_INF** (id 8) and **UPC_REV** (id 9) carry `condition_flag` rules. The flags themselves: + +- `with_ccr` — Klägerseite, infringement claim met with revocation counterclaim. Adds `inf.def_to_ccr`, `inf.reply`, `inf.reply_def_ccr`, `inf.rejoin`, `inf.rejoin_reply_ccr` (5 rules) to UPC_INF. +- `with_cci` — Beklagtenseite on revocation answered with infringement counterclaim. Adds `rev.cc_inf`, `rev.def_cci`, `rev.reply_def_cci`, `rev.rejoin_cci` (4 rules) to UPC_REV. +- `with_amend` — Patent amendment proposed. Adds `inf.app_to_amend`, `inf.def_to_amend`, `inf.reply_def_amd`, `inf.rejoin_amd` to UPC_INF; `rev.app_to_amend`, `rev.def_to_amend`, `rev.reply_def_amd`, `rev.rejoin_amd` to UPC_REV. Composes with `with_ccr` / `with_cci`. + +Every other proceeding type (DE_INF, DE_NULL, EPA_OPP, EPA_APP, EP_GRANT, DPMA_*, UPC_APP, UPC_PI, UPC_DAMAGES, UPC_DISCOVERY, UPC_COST_APPEAL, UPC_APP_ORDERS) has zero `condition_flag` rules — only one canonical timeline. + +### 5.2 Chip set per proceeding + +Chips are conditionally rendered based on which flags exist on the selected proceeding's `condition_flag` rule rows. + +``` +UPC_INF: [Standard] [+ Widerklage Nichtigkeit (with_ccr)] [+ Patentänderung (with_amend)] +UPC_REV: [Standard] [+ Verletzungs-Widerklage (with_cci)] [+ Patentänderung (with_amend)] +DE_INF, DE_NULL, EPA_OPP, …: (no chips, single timeline) +``` + +Chips are **toggleable** (multi-select), not radio. Each chip toggles its flag on/off; the timeline reflows. Composite combinations (`with_ccr + with_amend`) render the union of rules. Toggling all chips off renders the base proceeding (no `condition_flag` rules). + +Future flags (court-specific, expedited) — chips are **disabled and dimmed** with a tooltip "wird noch nicht unterstützt" when the proceeding has nothing to offer. We do NOT pre-render dead chips for proceedings without variants. + +### 5.3 Consolidated vs lane view — the toggle m asked for + +m's example: an infringement action triggers a counterclaim for revocation. Two ways to render: + +**Consolidated** — One timeline. CCR-related events (the `with_ccr` flag) interleave with base UPC_INF events along the same vertical timeline. Colour-coded by `primary_party` (claimant / defendant / court). This is the current behaviour when `?flags=with_ccr` is set. + +**Lane** — Two parallel columns. Column 1 = UPC_INF base timeline. Column 2 = UPC_REV timeline (the counterclaim's own proceeding). Rules anchored on shared trigger dates align horizontally. + +Toggle UI sits beside the variant chips: + +``` +[Standard] [+ Widerklage] | View: ◉ Konsolidiert ○ Spalten +``` + +In v1, the lane view is only available when the user has selected a variant that implies a *second proceeding* — i.e., `UPC_INF + with_ccr` shows UPC_INF || UPC_REV side-by-side, `UPC_REV + with_cci` shows UPC_REV || UPC_INF. Same backend data, different paint. + +For variants that DON'T imply a second proceeding (`with_amend` alone), the lane toggle is hidden — there's only one timeline. + +### 5.4 URL state + +`/tools/verfahrensablauf?proceeding=UPC_INF&flags=with_ccr,with_amend&view=lane&trigger_date=2026-05-12` + +Trigger date is optional — without it, the timeline renders with relative offsets ("+3 Monate", "+6 Wochen") instead of absolute dates. This is the "browse shape" mode. With a trigger date the timeline becomes concrete. + +`view=consolidated` (default) or `view=lane` toggles paint. + +--- + +## 6. Side-by-side compare + +The second variant axis. m wants to compare *two different proceeding types* OR *two variants of the same proceeding* side-by-side. + +### 6.1 Affordance + +A "Vergleichen" button next to the variant chips. Click → second proceeding picker slides in, second variant-chip row appears, two timelines render side-by-side. + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Verfahren A: [UPC_INF ▾] Flags: [✓ with_ccr] [ with_amend]│ +│ Verfahren B: [UPC_REV ▾] Flags: [✓ with_cci] [ with_amend]│ +│ Trigger A: [2026-05-12] Trigger B: [synced ✓] │ +│ ────────────────────────────────────────────────────────────│ +│ │ +│ Timeline A ║ Timeline B │ +│ ┌─ Klageerhebung ║ ┌─ Nichtigkeitsklage │ +│ │ 2026-05-12 ║ │ 2026-05-12 │ +│ ├─ Klageerwiderung ║ ├─ Klageerwiderung │ +│ │ 2026-08-12 (3M) ║ │ 2026-08-12 (3M) │ +│ … │ +└──────────────────────────────────────────────────────────────┘ +``` + +### 6.2 Decisions + +- **Max 2 timelines for v1.** Three+ would push the layout below mobile readability and add picker friction. The `counterclaim_of` example always pairs two proceedings; that's the common case. +- **Synchronised date axis** by default (Trigger A = Trigger B). Toggle "Unabhängige Trigger-Daten" reveals a second date input. Synced is the right default because the most common compare is "what happens in both proceedings starting from the same Klageerhebung date". +- **Independent variant chips per timeline.** Variant A's flags don't affect Variant B. The chips render per-column. +- **Wide-screen primary.** Lane and compare views require ≥720px to be readable. Below that, stack vertically (Timeline A above Timeline B, full-width each). The synced-trigger constraint stays; users on small screens still get the compare, just stacked. +- **Permalink-shareable.** `?compare=1&a_proceeding=UPC_INF&a_flags=with_ccr&b_proceeding=UPC_REV&b_flags=with_cci&trigger=2026-05-12&synced=true` — every chip + variant + trigger captured in URL. Copy-paste produces an identical render. + +### 6.3 Lane view vs Compare view — are they the same thing? + +Conceptually similar (two columns), but UX-distinct: + +- **Lane view** is "one variant that implies two proceedings rendered together". The two columns are *logically linked* (e.g., `UPC_INF + with_ccr` always shows the same UPC_REV alongside). +- **Compare view** is "the user picked two arbitrary proceedings + variants to look at together". The two columns are *independently chosen*. + +In renderer terms they share the same DOM layout (CSS grid with 2 columns). The state differs: lane view's second proceeding is computed from the variant flag; compare view's second proceeding is user-picked. We implement them as one renderer with two state-entry points. + +--- + +## 7. Sidebar nav labels + URL conventions + +### 7.1 Labels (post-cleanup) + +Today: **Fristenrechner** + **Verfahrensablauf**. + +Recommendation: keep the labels as-is. m's brief suggested alternatives ("Frist berechnen" / "Verfahrensabläufe") — I think the current labels are tighter: + +- "Fristenrechner" is a known brand-term in the firm vocabulary (per the German-tool-names-as-brands convention in CLAUDE.md). +- "Verfahrensablauf" reads as a noun "the procedural flow", which matches the abstract-browse intent better than the plural "Verfahrensabläufe" (which reads as "the catalogue of all flows"). + +But I flag this for m in §13 — the call is brand-strategic, not technical. + +### 7.2 URL conventions + +| Route | Key params | Purpose | +|---|---|---| +| `/tools/fristenrechner` | `mode=akte\|abstract` | Pick branch | +| `/tools/fristenrechner?mode=akte&project=` | + `path=outgoing\|happened` | Akte deadline determination | +| `/tools/fristenrechner?mode=abstract&forum=upc&proceeding=UPC_INF&trigger_date=…` | + `flags=…` | Abstract deadline determination | +| `/tools/verfahrensablauf` | `proceeding=…&flags=…&view=…&trigger_date=…` | Browse one proceeding-shape | +| `/tools/verfahrensablauf?compare=1&a_proceeding=…&b_proceeding=…&…` | (per §6.2) | Compare two | + +The `?path=a` query param dies entirely. The `fixVerfahrensablaufActive` function deletes. The localStorage key `paliad.fristen.pathway` is preserved (still used by Akte-mode Pathway A/B inside `/tools/fristenrechner`); it gets a sibling `paliad.fristen.mode`. + +### 7.3 Bookmarkability + share + +Both pages produce permalinks. Copy URL → paste in another browser → identical view (with same auth gate). The compare-view URL is particularly load-bearing for the "send your colleague a precomputed timeline" use case — it's how a PA quickly shows a counterpart "this is the shape we're looking at". + +--- + +## 8. Mobile + responsive + +Existing breakpoints in the codebase: 640px / 720px / 768px / 1023px (`frontend/src/styles/global.css`). + +### 8.1 `/tools/fristenrechner` + +- **≥720px:** Step 0 toggle horizontal. Akte search results in a list. +- **<720px:** Step 0 toggle stacks (radio rows top-to-bottom). Akte list full-width. +- **<480px:** Proceeding-tile picker (UPC / DE / EPA / DPMA tabs + tiles) wraps tiles to one column. + +### 8.2 `/tools/verfahrensablauf` + +- **≥1023px:** Lane view + compare view render side-by-side (CSS grid 2-col). +- **720–1022px:** Lane view side-by-side; compare view stacks (Timeline A above Timeline B, full-width). +- **<720px:** Both lane and compare stack vertically. Variant chips wrap to 2-3 rows. +- **<480px:** Single-column always. Compare-view "Vergleichen" button still works but stacks the result rows. + +### 8.3 Variant chips on mobile + +Chips wrap with `flex-wrap`. Maximum 3 chips per row on a 360px viewport (each chip ≤ 110px wide); composite proceedings (UPC_INF, UPC_REV) fit 3 chips so this works. + +### 8.4 What does NOT collapse on mobile + +- The trigger-date input. Stays a single date picker (browser-native; iOS / Android already render their own UI). +- The proceeding picker. Stays tiled (large tap targets). +- The result rows (column + timeline views). Render unchanged from today; mobile already handles them. + +--- + +## 9. What gets dropped + +| Today | Post-cleanup | +|---|---| +| **Step 2 "Verfahrensablauf einsehen" card** | Deleted. The abstract-browse case has its own route. | +| **Sidebar `?path=a` deep-link** | Deleted. `/tools/verfahrensablauf` replaces it. | +| **`fixVerfahrensablaufActive()` function** | Deleted. Both sidebar entries map 1:1 to URLs; native SSR active-class works. | +| **`localStorage["paliad.fristen.pathway"]`** | Preserved as-is. Still used inside Akte-mode Pathway A/B. | +| **The Step 1/Step 2 fork on `/tools/fristenrechner`** | Replaced by Step 0 (Akte vs Abstract). Step 2's "file vs happened vs browse" becomes a wizard-internal branch, not a top-level page state. | +| **Step 3a "outgoing-intent chooser" (File / Draft / Enter)** | Kept inside Akte-mode. The Draft option (`fristen-step3a-draft`) stays disabled as today (placeholder). | + +The deletions sum to maybe 200–300 LoC out of `client/fristenrechner.ts`. The lift of `verfahrensablauf-core.ts` is the bigger reshape; net LoC churn around +500 / -300. + +--- + +## 10. Slicing for the coder pass + +Four slices, each independently mergeable. Slice 1 ships the structural split; Slices 2–4 layer features. + +### Slice 1 — Route + shell split (foundation) + +- New route `/tools/verfahrensablauf` registered in `internal/handlers/handlers.go`. +- New handler `handleVerfahrensablaufPage` serves `dist/verfahrensablauf.html`. +- New TSX `frontend/src/verfahrensablauf.tsx` — renders the proceeding-tile picker + result panel. No variant chips yet; no compare yet. Just the abstract-browse case factored out. +- New client `frontend/src/client/verfahrensablauf.ts` — minimal: picker → calc → render. Imports from a new shared module `client/views/verfahrensablauf-core.ts`. +- Sidebar `Sidebar.tsx:163-164` updated: second nav entry's href flips from `/tools/fristenrechner?path=a` to `/tools/verfahrensablauf`. +- `client/sidebar.ts:447 fixVerfahrensablaufActive` deleted (and its call site at the bottom of `initSidebar`). +- Step 2 "Verfahrensablauf einsehen" card markup in `frontend/src/fristenrechner.tsx` + its handler in `client/fristenrechner.ts` deleted. +- Step 2's "browse" event handler at `fristen-step2-browse` removed; the path="a" branch in `showPathway` still exists for Akte-mode wizard re-use. +- DE/EN i18n keys: `tools.verfahrensablauf.title`, `tools.verfahrensablauf.subtitle`, plus all the proceeding-tile labels (already exist — reused). +- Build: add `renderVerfahrensablauf` import and `bun:write` step in `frontend/build.ts`. +- Tests: Playwright smoke — `/tools/verfahrensablauf` renders, sidebar nav links work, no 404s, the old `?path=a` URL 302s to `/tools/verfahrensablauf` (back-compat for any bookmarked links). + +**What does NOT change in Slice 1:** the existing `/tools/fristenrechner` page works exactly as today (Step 1 / Step 2 / Step 3a / Pathway A / Pathway B). Step 0 is Slice 2. + +### Slice 2 — Step 0 on `/tools/fristenrechner` + +- New Step 0 toggle component in `fristenrechner.tsx` (above today's Step 1). +- `?mode=akte|abstract` URL param + `paliad.fristen.mode` localStorage hook. +- "Abstract" branch reveals a new compact proceeding-tile picker inside the Step 0 frame (or scrolls to today's wizard-step-1). +- "Akte" branch renders today's Step 1 (Akte search + ad-hoc chips). +- Akte-driven auto-derivation (§4): a new service `ResolveFristenrechnerCodeForProject(projectID)` and frontend hook that preselects the proceeding tile + `our_side` chip + Court hint (highlight only, not pre-select). +- Tests: Playwright smoke for the four state transitions (akte → abstract, abstract → akte, akte+project → akte-no-project, deep-link `?mode=abstract&forum=upc`). + +### Slice 3 — Variant chips + consolidated/lane view + +- Variant-chip strip on `/tools/verfahrensablauf` (`with_ccr`, `with_cci`, `with_amend` conditional on proceeding). +- `?flags=` URL param. +- Lane-vs-consolidated toggle. Lane view auto-enables when the variant implies a second proceeding (UPC_INF+with_ccr → UPC_REV; UPC_REV+with_cci → UPC_INF). +- Lane renderer in `views/verfahrensablauf-core.ts` (CSS grid 2-col, shared trigger-date axis). +- Tests: Playwright smoke for variant toggles + lane render + lane on mobile (stack). + +### Slice 4 — Side-by-side compare + +- "Vergleichen" button + second-proceeding picker. +- `?compare=1&a_proceeding=…&b_proceeding=…&…` URL state. +- Synced-trigger toggle; independent-trigger fallback. +- Permalink test (copy URL → fresh tab → same render). +- Mobile fallback (stacked). +- Tests: Playwright smoke for compare entry, both timelines render, permalink roundtrip. + +Each slice merges to main independently. Slice 1 is the bottleneck; once it's in, Slices 2–4 can ship in any order (Slice 2 only touches `/tools/fristenrechner`, Slices 3+4 only touch `/tools/verfahrensablauf`). + +--- + +## 11. Tradeoffs flagged + +### 11.1 Code duplication vs route clarity + +The split forces ~700–900 LoC of client code into a shared module (`views/verfahrensablauf-core.ts`). That's lift work without user-visible benefit. The alternative (one big page with `?mode=`) saves the lift but keeps the muddled mental model that triggered this redesign in the first place. **Decision: pay the lift cost.** It's a one-time refactor; the navigation clarity is durable. + +### 11.2 Step 0 vs Step 1 — perceived "extra step" + +Today's flow: Akte picker (Step 1) → choose-intent cards (Step 2) → wizard. Tomorrow's flow: mode toggle (Step 0) → Akte picker OR abstract picker → wizard. Same number of clicks for the Akte case. One *fewer* click for the abstract case (you go straight to proceeding tiles instead of clicking "Verfahrensablauf einsehen" first). Net win. + +### 11.3 Court free-text means imperfect auto-derivation + +We can't reliably auto-pick `court_id` from `projects.court` until that column becomes an FK. The design leans on "highlight matching options" rather than silent preselect. The cost is one extra click. **File a follow-up ticket** to migrate `projects.court` → `court_id` FK; until then, no silent FK promotion. + +### 11.4 Pathway B (Determinator cascade) stays inside Akte-mode + +t-paliad-166 will redesign Pathway B as a row-by-row cascade. We don't pre-empt that. Pathway B remains reachable from Akte-mode's "Etwas ist passiert" card. In Abstract mode it's reachable through a "Frist aufgrund Ereignis" link in the result panel. Both paths stay; only the entry surface changes. + +### 11.5 Variant chips disabled for non-UPC proceedings + +Only UPC_INF and UPC_REV have `condition_flag` rules today. DE_INF, DE_NULL, EPA_OPP, etc. show no chips. This is honest — the data isn't there. If users ask for German "with/without counterclaim" variants, that's a `condition_flag` seed-data ticket, not a UX redesign. + +### 11.6 Lane view assumes the second proceeding exists + +`UPC_INF + with_ccr` lanes to `UPC_REV`. But `UPC_REV` itself is a full proceeding with its own deadlines anchored on a *separate* trigger date (the CCR filing date, not the SoC date). For v1 we render the second lane with the *same trigger date* as the primary — which is wrong-but-useful: the user sees the *shape* of the counterclaim's flow but the dates are nominal. A future iteration adds a "second trigger date" input for the lane. **Document this in the UI** with a small caveat: "Annahme: Widerklage zur gleichen Zeit eingelegt". + +### 11.7 No state preserved across the route boundary + +If a user is mid-calc on `/tools/fristenrechner` and clicks the sidebar's `/tools/verfahrensablauf`, their wizard state is lost. We don't try to bridge the two — they're different intents. The URL captures everything important; the user can pop back via the browser back button. + +### 11.8 Print mode is the only export + +No PDF, no SVG, no CSV export in this design. The existing `#fristen-print-btn` + `@media print` stylesheet handles it. m's broader chart-export design (`docs/design-project-chart-2026-05-09.md`) covers the export ambition for the project-level chart; this Tool-level surface keeps it simple. + +--- + +## 12. Files implementer will touch (Slice 1 only) + +This is the bottleneck slice. Slices 2–4 each add their own scope but Slice 1 defines the structural change. + +**Backend (Go):** + +- `internal/handlers/handlers.go:162` — add `protected.HandleFunc("GET /tools/verfahrensablauf", handleVerfahrensablaufPage)`. +- `internal/handlers/fristenrechner.go` — add `handleVerfahrensablaufPage` (1-liner, serves `dist/verfahrensablauf.html`). Or split into its own file `internal/handlers/verfahrensablauf.go` for tidiness. +- `internal/handlers/handlers.go` — add back-compat 302: `/tools/fristenrechner?path=a` → `/tools/verfahrensablauf` (preserves bookmarked links). A small middleware or an `init` redirect handler suffices. + +**Frontend (TSX + TS):** + +- `frontend/src/verfahrensablauf.tsx` — new file. ~250 LoC. Renders header + jurisdiction-tab picker + proceeding-tile picker + result panel container. No variant chips, no compare yet (those are Slices 3+4). Reuses ``, ``, `