Files
paliad/docs/design-tools-cleanup-2026-05-12.md
mAi 8e9cde6d52 design(t-paliad-178): Tools surface cleanup — split Fristenrechner / Verfahrensablauf
Inventor pass for t-paliad-178. Two intents (deadline determination vs
abstract procedural shape browse) get two dedicated routes:

- /tools/fristenrechner — keeps deadline-determination, gains Step 0
  ("Abstrakt oder Akte?") above today's Step 1.
- /tools/verfahrensablauf — new dedicated abstract-browse surface with
  variant chips (with_ccr / with_cci / with_amend), consolidated-vs-lane
  view, and side-by-side compare.

§0 premise audit corrects three things the task brief got wrong:
  1. projects.court is free-text, not FK — no silent court_id auto-pick.
  2. projects.proceeding_type_id points at litigation-category rows, not
     fristenrechner-category — a mapping helper (litigation × jurisdiction
     → fristenrechner code) is required.
  3. condition_flag variants only exist on UPC_INF + UPC_REV; every other
     proceeding renders a single canonical timeline. Variant chips honour
     this — no dead chips on DE_INF / EPA_OPP / DPMA_*.

Sliced into 4 independent merges: Slice 1 (route + shell split) is the
structural foundation; Slices 2-4 layer Step 0 / variant chips / compare.

DESIGN ONLY — no implementation. Awaiting m's go/no-go before coder shift.
2026-05-12 14:10:20 +02:00

45 KiB
Raw Permalink Blame History

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: ~700900 LoC. New code on verfahrensablauf.ts (variant chips + lane mode + compare): ~400600 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

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=<uuid> — 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=<id> 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=<uuid>, 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=<uuid> + 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).
  • 7201022px: 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 200300 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 24 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 24 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 ~700900 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.courtcourt_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 24 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 <PWAHead>, <Sidebar>, <Footer>.
  • frontend/src/client/verfahrensablauf.ts — new file. ~150 LoC for Slice 1. Wires the picker → POST /api/tools/fristenrechner → render via shared module.
  • frontend/src/client/views/verfahrensablauf-core.ts — new file. The lifted code: renderTimelineBody, renderColumnsBody, the calculateDeadlines fetch wrapper, court picker, view-toggle. Imported by both client/fristenrechner.ts and client/verfahrensablauf.ts.
  • frontend/src/client/fristenrechner.ts — delete the Step 2 "browse" card handler (lines 2715-2717 today). Remove the ?path=a interpretation as a top-level entry (still keep path="a" as an Akte-mode wizard pathway). Import calc + render from views/verfahrensablauf-core.ts.
  • frontend/src/fristenrechner.tsx — delete the fristen-step2-browse card markup (lines 215-223 today).
  • frontend/src/components/Sidebar.tsx:163-164 — change href from /tools/fristenrechner?path=a to /tools/verfahrensablauf. Adjust the currentPath comparison to match the new pathname.
  • frontend/src/client/sidebar.ts:447 fixVerfahrensablaufActive — delete the function + its call site.

Build:

  • frontend/build.ts — add renderVerfahrensablauf import (line 5-6 area), add client/verfahrensablauf.ts to entrypoints array (line 228 area), add the Bun.write(join(DIST, "verfahrensablauf.html"), renderVerfahrensablauf()) step (line 355 area).

i18n:

  • frontend/src/client/i18n.ts + i18n-keys.ts — add tools.verfahrensablauf.title, tools.verfahrensablauf.subtitle, nav.verfahrensablauf (already exists; re-verify the key still points at the right label).

Tests:

  • Playwright smoke covering: /tools/verfahrensablauf renders, sidebar nav link active class lights up correctly without fixVerfahrensablaufActive, /tools/fristenrechner?path=a 302s, the calc roundtrip works on both routes, build artefacts emit both fristenrechner.html and verfahrensablauf.html.

Out of Slice 1 (deferred to Slices 2-4):

  • Step 0 toggle on /tools/fristenrechner (Slice 2).
  • Akte-driven auto-derivation helper service (Slice 2).
  • Variant chips, lane view (Slice 3).
  • Compare view (Slice 4).

13. Open questions for m

  1. Sidebar label. Keep "Verfahrensablauf" (current) or switch to "Verfahrensabläufe" (plural — reads as catalogue) or something else? Current label is unambiguous; plural risks reading as a list page.

  2. Akte-mode mapping with no proceeding_type_id. 11/11 live projects have NULL proceeding_type_id. Akte-mode silently degrades to "pick proceeding manually". OK? Or should Akte-mode require a proceeding_type_id and force the user to set it on the project first?

  3. Court free-text → FK migration. I'm flagging this as a follow-up but not designing it here. Want me to file a separate ticket so it's tracked, or fold it into Slice 2's scope?

  4. Lane view caveat for v1. The second lane uses the same trigger date as the primary (so dates are nominal-but-wrong for a real-world CCR filed weeks later). UI caveat "Annahme: Widerklage zur gleichen Zeit eingelegt" is honest but adds clutter. Acceptable or do we hold lane view back until trigger-2 input lands?

  5. Compare view max columns. v1 caps at 2. Three+ would be a richer compare ("UPC_INF vs DE_INF vs EPA_OPP for the same patent") but layout-hostile on anything <1280px. Confirm 2 for v1?

  6. Back-compat for ?path=a. I propose a 302 redirect so old bookmarked URLs work. Alternative: 410 Gone (harsh) or 200-with-deprecation-banner (chatty). 302 is the conventional move; confirm?

  7. Drop the "Verfahrensablauf einsehen" card from Step 2 entirely vs keep it as a deep-link shortcut to /tools/verfahrensablauf from inside the Fristenrechner flow? I'm proposing drop; m signals?

  8. DE_INF / EPA_OPP / DPMA variants. Today no condition_flag rules. Future seed-data tickets (out of scope here): with/without expedited, with/without amendment for EPA opposition, etc. Want a follow-up ticket filed for the seed-data work or wait for user feedback?

  9. Pathway B (Determinator) entry point in Abstract mode. I propose a small "Frist aufgrund Ereignis" link in the result panel. Or hide it entirely from abstract mode? Today Pathway B is reachable from anywhere via ?path=b.

  10. Implementer choice. I'd recommend a coder familiar with frontend/src/client/fristenrechner.ts for Slice 1 since the bundle split is the load-bearing risk. Curie (t-paliad-086), cronus (t-paliad-088, t-paliad-110), noether (t-paliad-165) have all touched the file. Head decides.


DESIGN READY FOR REVIEW

Slice 1 is the structural foundation (route split, sidebar cleanup, code lift). Slices 2-4 layer Step 0 / variant chips / compare on top. Awaiting m's go/no-go before coder shift.