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.
45 KiB
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/fristenrechneris the only registered page route (internal/handlers/handlers.go:162). Both sidebar entries (Fristenrechner + Verfahrensablauf) target the same Bun-builtdist/fristenrechner.htmland disambiguate purely through?path=aand 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 byfrontend/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.tscarries 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 (Pathwaytype 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=alands on Pathway A directly, NOT on the Akte picker. I tracedinitPathwayFork → readPathwayFromURL → showPathway("a"): it setsstep1.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.courtis a free-text column, NOT an FK topaliad.courts. Confirmed ininformation_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; onlyproceeding_type_idis a real FK. The design must NOT silently auto-pick apaliad.courts.idfromprojects.court— fuzzy mapping is best-effort + always overridable, never silent.paliad.projects.proceeding_type_idpoints atcategory='litigation'rows (7 codes: INF, REV, CCR, APM, APP, AMD, ZPO_CIVIL). The Fristenrechner wizard acceptscategory='fristenrechner'codes (20 codes: UPC_INF, DE_INF, EPA_OPP, …). These overlap conceptually (INFis the abstract noun behind bothUPC_INFandDE_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 onpaliad.projectstoday, so DE_INF_OLG / DE_INF_BGH cannot be inferred — only the first-instance code can be.paliad.projectscarries nopriority_dateortrigger_datecolumn. It does havefiling_dateandgrant_date. Only EP_GRANT.ep_grant.publish (Art. 93 EPÜ) is anchored onpriority_datetoday (viaanchor_alt). For Akte-driven prefill,priority_datestays blank by default and the user fills it.paliad.projects.our_sideandpaliad.projects.counterclaim_ofexist (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_flagis 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). OnlyUPC_INF(proceeding_type_id=8) andUPC_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.
CourtIDinCalcOptions(internal/services/fristenrechner.go:107) only switches the holiday calendar (viacourts.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 ondeadline_rules. - Expedited-vs-standard distinctions do not exist either. No
condition_flagrow matches an expedited concept. Adding one is a schema-and-seed change, out of scope here. - Result rendering today lives in
renderTimelineBodyandrenderColumnsBody(frontend/src/client/fristenrechner.ts:637 / :664). The user toggles between the two with a radio (#fristen-view-toggle). Both renderers take a singleDeadlineResponseand 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
- Page surface split — separate routes per intent.
/tools/fristenrechnerkeeps the deadline-determination intent (Akte-scoped or abstract)./tools/verfahrensablaufbecomes the dedicated abstract-browse surface with variant chips + side-by-side compare. - Step 0 "Abstrakt oder Akte?" as the FIRST affordance on
/tools/fristenrechner. Pick → narrows downstream inputs. - Akte-driven auto-derivation — map project columns to wizard inputs and flag the gaps.
- Variant chips + consolidated-vs-lane view for
/tools/verfahrensablauf. - Side-by-side compare on
/tools/verfahrensablauf(max 2 timelines for v1). - Sidebar labels + URL conventions post-split.
- Mobile responsive plan.
- 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 viainternal/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-deadlinesendpoints; 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_TYPESarrays infristenrechner.tsx). - The timeline + columns result renderers (
renderTimelineBody,renderColumnsBody). - The
POST /api/tools/fristenrechnercalc 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
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);
Druckenremains.
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. Addsinf.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. Addsrev.cc_inf,rev.def_cci,rev.reply_def_cci,rev.rejoin_cci(4 rules) to UPC_REV.with_amend— Patent amendment proposed. Addsinf.app_to_amend,inf.def_to_amend,inf.reply_def_amd,inf.rejoin_amdto UPC_INF;rev.app_to_amend,rev.def_to_amend,rev.reply_def_amd,rev.rejoin_amdto UPC_REV. Composes withwith_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_ofexample 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_ccralways 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).
- 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/verfahrensablaufregistered ininternal/handlers/handlers.go. - New handler
handleVerfahrensablaufPageservesdist/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 moduleclient/views/verfahrensablauf-core.ts. - Sidebar
Sidebar.tsx:163-164updated: second nav entry's href flips from/tools/fristenrechner?path=ato/tools/verfahrensablauf. client/sidebar.ts:447 fixVerfahrensablaufActivedeleted (and its call site at the bottom ofinitSidebar).- Step 2 "Verfahrensablauf einsehen" card markup in
frontend/src/fristenrechner.tsx+ its handler inclient/fristenrechner.tsdeleted. - Step 2's "browse" event handler at
fristen-step2-browseremoved; the path="a" branch inshowPathwaystill 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
renderVerfahrensablaufimport andbun:writestep infrontend/build.ts. - Tests: Playwright smoke —
/tools/verfahrensablaufrenders, sidebar nav links work, no 404s, the old?path=aURL 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|abstractURL param +paliad.fristen.modelocalStorage 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_sidechip + 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_amendconditional 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— addprotected.HandleFunc("GET /tools/verfahrensablauf", handleVerfahrensablaufPage).internal/handlers/fristenrechner.go— addhandleVerfahrensablaufPage(1-liner, servesdist/verfahrensablauf.html). Or split into its own fileinternal/handlers/verfahrensablauf.gofor tidiness.internal/handlers/handlers.go— add back-compat 302:/tools/fristenrechner?path=a→/tools/verfahrensablauf(preserves bookmarked links). A small middleware or aninitredirect 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, thecalculateDeadlinesfetch wrapper, court picker, view-toggle. Imported by bothclient/fristenrechner.tsandclient/verfahrensablauf.ts.frontend/src/client/fristenrechner.ts— delete the Step 2 "browse" card handler (lines 2715-2717 today). Remove the?path=ainterpretation as a top-level entry (still keeppath="a"as an Akte-mode wizard pathway). Import calc + render fromviews/verfahrensablauf-core.ts.frontend/src/fristenrechner.tsx— delete thefristen-step2-browsecard markup (lines 215-223 today).frontend/src/components/Sidebar.tsx:163-164— change href from/tools/fristenrechner?path=ato/tools/verfahrensablauf. Adjust thecurrentPathcomparison to match the new pathname.frontend/src/client/sidebar.ts:447 fixVerfahrensablaufActive— delete the function + its call site.
Build:
frontend/build.ts— addrenderVerfahrensablaufimport (line 5-6 area), addclient/verfahrensablauf.tstoentrypointsarray (line 228 area), add theBun.write(join(DIST, "verfahrensablauf.html"), renderVerfahrensablauf())step (line 355 area).
i18n:
frontend/src/client/i18n.ts+i18n-keys.ts— addtools.verfahrensablauf.title,tools.verfahrensablauf.subtitle,nav.verfahrensablauf(already exists; re-verify the key still points at the right label).
Tests:
- Playwright smoke covering:
/tools/verfahrensablaufrenders, sidebar nav link active class lights up correctly withoutfixVerfahrensablaufActive,/tools/fristenrechner?path=a302s, the calc roundtrip works on both routes, build artefacts emit bothfristenrechner.htmlandverfahrensablauf.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
-
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.
-
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? -
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?
-
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?
-
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?
-
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? -
Drop the "Verfahrensablauf einsehen" card from Step 2 entirely vs keep it as a deep-link shortcut to
/tools/verfahrensablauffrom inside the Fristenrechner flow? I'm proposing drop; m signals? -
DE_INF / EPA_OPP / DPMA variants. Today no
condition_flagrules. 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? -
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. -
Implementer choice. I'd recommend a coder familiar with
frontend/src/client/fristenrechner.tsfor 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.