- §0 premises verified live: 4-layer Pathway B mess (radio + 2 chip-strips + breadcrumb-cascade), 91/103 leaves carry forum tag, 16 leaves carry party tag, 11/11 live projects have NULL proceeding_type_id (graceful degrade), 4 distinct condition_flag value-sets on UPC_INF + UPC_REV only, project.court is free-text not FK, verfahrensablauf-core.ts carries zero cascade leakage post-t-paliad-179 Slice 1. - §1 three intertwined pillars: project-driven narrowing / visual hierarchy overhaul / row-by-row persistent cascade. - §2-3 single .fristen-row primitive (active / answered / prefilled / hidden) replaces radio + chip-strips + breadcrumb-cards. - §4 data mapping: forum derivation already shipped; new litigation_code x jurisdiction -> fristenrechner_code helper (shared with t-paliad-178 Slice 2). - §5 per-row pre-fill / hide / skipped-but-shown matrix across UPC / DE / EPA / DPMA / ad-hoc / zero-context flows with two compact ASCII diagrams. - §6 Filter / Suche mode = escape-hatch icon-button (inventor's pick). - §7-9 mobile breakpoints, three reset flavours, search affordance placement. - §10 three slices: visual-only (Slice 1), narrowing depth + proceeding_mapping.go helper (Slice 2), mobile + search polish (Slice 3). - §11 seven trade-offs flagged (row-stack height, aus-Akte noise, auto-walk magic, radio removal, NULL proceeding_type_id reality, mapping ambiguities, ändern descendant invalidation). - §12 file-touch map for Slice 1 only. - §13 fifteen open questions for m to call before coder shift. NOT self-merged. Awaiting m's go/no-go.
55 KiB
Design — Determinator B1 row-by-row cascade (replaces breadcrumb drilldown)
Author: pauli (inventor) Date: 2026-05-13 Task: t-paliad-166 Status: READY FOR REVIEW — m gates inventor → coder transition. Gitea: m/paliad#25 (re-opened by m's 2026-05-13 11:17 comment).
0. Premises verified live (before designing)
CLAUDE.md, mai-memory and the task brief can all be stale by days. Every anchor below is verified against the live codebase or live DB on mai/pauli/determinator-b1-row-by (baseline adf377c — main as of Slice 1 of t-paliad-179 merge).
0.1 The Pathway B markup today
frontend/src/fristenrechner.tsx:227-310 is the Pathway B shell. Four functionally different layers are stacked with four visually different treatments. Live, in source order:
| Layer | Element | Affordance | Visual |
|---|---|---|---|
| L1 Mode | .fristen-mode-toggle |
role=radiogroup with two <input type="radio"> |
Radio buttons. Tree vs Filter. |
| L2 Perspective | .fristen-perspective-bar |
Three <button> chips |
Pill chips. Kläger / Beklagter / Beide. |
| L3 Inbox | .fristen-inbox-bar |
Four <button> chips |
Pill chips. CMS / beA / Posteingang / Alle. |
| L4 Cascade | .fristen-b1-cascade |
Breadcrumb + question + button-grid (drill-down) | Cards in a grid, breadcrumb above. |
Below L4 sits .fristen-b1-results — the concept-card list that narrows as the cascade descends. That's content, not a decision layer.
m's critique is exact: L1/L2/L3/L4 are all "narrow the deadline-rule space" steps with the same conceptual weight, but the user sees a radio, two pill strips, and a card grid. The cascade itself (L4) hides previous steps behind a breadcrumb — so when you've drilled three levels deep you can no longer see "I picked CMS → vom Gericht → Hinweisbeschluss" in one glance unless you read tiny breadcrumb crumbs.
0.2 The cascade engine today
frontend/src/client/fristenrechner.ts:2405-2574 (renderB1Cascade). For a given ?b1=<slug>:
- Build
trail = buildBreadcrumb(roots, currentSlug). The trail is the ancestors of the current node. - Render
<nav class="fristen-b1-breadcrumb">= root-reset +›-separated crumb buttons. - Render
<p class="fristen-b1-question">= the current node'sstep_question_de(or"Was ist passiert?"at root). - Render
<div class="fristen-b1-buttons">= child nodes as button cards (icon + label,--leafmodifier on terminal nodes). - Render
<button class="fristen-b1-step-back">= "← Eine Stufe zurück".
Drilling = navigateB1(child.slug) = pushState + renderB1Cascade(child.slug). The previous question disappears; only the breadcrumb crumb survives as text. There is no "row of answered decisions."
0.3 Where narrowing happens today
fristenrechner.ts:2509-2522 filters cascade children by two predicates before rendering:
inboxFilterAllowsForums(c.forums)— hides nodes whoseforumstag doesn't matchactiveForumOnPage(). The active forum is resolved atfristenrechner.ts:2960-2970with a three-input precedence chain:- Inbox chip (
cms→upc,bea/posteingang→de). User override beats everything. - Ad-hoc chip from Step 1's explore-mode bypass (
upc/de/epa/dpma). - Project context (
project.proceeding_type_id→proceeding_types.code→ prefix →upc/de/epa/dpma).
- Inbox chip (
perspectiveAllowsParty(c.party)— hides leaves whosepartytag contradicts the perspective chip. t-paliad-164 already auto-fills the chip fromproject.our_side.
So project-driven narrowing for the FORUM axis is shipped. What m is asking for in this task is (a) generalize the pattern so MORE rows get pre-answered, (b) make the answered-state visible in the same row format, (c) hide rows whose answer is fully implied (UPC project + L3 Inbox).
0.4 The taxonomy and rule corpus
Live data, paliad.event_categories (recursive tree, t-paliad-133):
- 6 root buckets under
(root):cms-eingang("Von wem ist das Schriftstück?"),muendl-verhandlung("Mündliche Verhandlung"),beschluss-entscheidung("Beschluss / Entscheidung"),frist-verpasst("Frist verpasst"),ich-moechte-einreichen("Ich möchte etwas einreichen"),sonstiges(terminal leaf). - 103 leaves total. 91 carry a
forumstag (upc/de/epa/dpma); 12 are neutral. 16 leaves carry apartytag — all underich-moechte-einreichen.*(claimant / defendant) — the perspective filter touches outgoing filings only, never incoming Gegenseiten-Schriftstücke (which are symmetric: you receive what the other side sent regardless of who you are). - Cascade depth varies 2–4 levels. Slug encodes the path with dots, e.g.
cms-eingang.gegenseite.upc-inf.klageschriftis 4 segments deep.
paliad.proceeding_types:
- 20
category='fristenrechner'codes (the wizard / B1 cascade vocabulary):UPC_INF,UPC_REV,UPC_APP,UPC_APP_ORDERS,UPC_COST_APPEAL,UPC_DAMAGES,UPC_DISCOVERY,UPC_PI,DE_INF,DE_INF_OLG,DE_INF_BGH,DE_NULL,DE_NULL_BGH,DPMA_OPP,DPMA_BPATG_BESCHWERDE,DPMA_BGH_RB,EPA_OPP,EPA_APP,EP_GRANT. - 7
category='litigation'codes (the project model's vocabulary):INF,REV,CCR,APM,APP,AMD,ZPO_CIVIL. Alljurisdiction='UPC'exceptZPO_CIVIL. - The two vocabularies overlap conceptually but not row-wise. Mapping
litigation_code × jurisdiction → fristenrechner_codeis required for Akte-derived narrowing beyond the 4-letter forum prefix. The brief lists this mapping; the live data confirms it's the only path.
paliad.deadline_rules.condition_flag — 4 distinct flag-sets live in production: [with_amend], [with_cci], [with_ccr], [with_ccr, with_amend]. Only on UPC_INF and UPC_REV. This is a Determinator-style variant axis the cascade does not surface today; out of scope for this design.
0.5 Live state of paliad.projects
| Column | Live data shape | Used by today's cascade? |
|---|---|---|
court |
Free-text. 4 non-null values across 4 rows: LG München I (1), UPC (2), UPC CoA (1). 7 rows NULL. |
No. |
proceeding_type_id |
FK → proceeding_types.id. 11/11 live rows are NULL. |
Yes — forumFromProject reads it, but it never fires in production today. |
our_side |
enum claimant / defendant / both / court / NULL. |
Yes — t-paliad-164 perspective chip predefine. |
counterclaim_of |
uuid FK self-reference. | No (relevant for SmartTimeline, not Determinator). |
filing_date / grant_date |
dates. | No (relevant to Verfahrensablauf wizard). |
Critical caveat: 11/11 live projects have NULL proceeding_type_id. Until that's backfilled (a separate cleanup), Akte-driven narrowing degrades to "no opinion" for every existing project. The design honours this — silent degrade, no failed-load toast, the cascade simply doesn't narrow. m locked this v1 behaviour with kelvin on 2026-05-13.
0.6 Anchor files for the implementer
frontend/src/fristenrechner.tsx:227-310— Pathway B markup (the four-layer mess).frontend/src/client/fristenrechner.ts:2405-2574—renderB1Cascade.frontend/src/client/fristenrechner.ts:2914-3081— forum + perspective narrowing engine (activeForumOnPage,inboxFilterAllowsForums,perspectiveAllowsParty,applyOurSidePredefine).frontend/src/styles/global.css:1636-1822—.fristen-pathway-shell,.fristen-mode-toggle,.fristen-b1-breadcrumb,.fristen-b1-question,.fristen-b1-buttons,.fristen-b1-button,.fristen-b1-step-back(the visuals this design overhauls).frontend/src/styles/global.css:1965-2065—.fristen-inbox-bar,.fristen-perspective-bar,.fristen-inbox-chip(the chip strip rules).frontend/src/client/views/verfahrensablauf-core.ts(t-paliad-179) — pure-functional core, verified to carry zero Pathway B / cascade code. The lift is clean; this design is independent of it.
0.7 Adjacent design docs
docs/design-tools-cleanup-2026-05-12.md(kelvin, t-paliad-178). Slice 1 of that shipped today; Slice 2 (Step 0 toggle + Akte auto-derivation on/tools/fristenrechner) is adjacent and will share thelitigation_code × jurisdiction → fristenrechner_codemapping with this design.docs/research-determinator-coverage-2026-05-08.md(curie, t-paliad-167). Identified leaves missing from the cascade. Out of scope here — this design is the UX shell that any future coverage additions will land into.
If any of these conflict with what the task brief or memory asserts, the live state wins and the brief is the bug — flagged in §13 for m.
1. Vision + the three pillars
m's framing (2026-05-13 11:17):
When I select a project, it should already narrow down the options (at least if it is a court proceeding). If it is a UPC proceeding, there is no need to show "non-UPC options"; this starts with the "how did you receive it?" which - for the UPC - will always be the UPC CMS.
Not only is the different format for the levels of the questions weird (this needs an overhaul!), also there is no narrowing at all. I already described before that I want each decision on the tree to remain visible (one row per decision, it may be more compact than the active question was) and then go through things until there are only the least possible options left.
Three pillars, intertwined:
Pillar 1 — Project-driven narrowing
Pre-fill or hide decision rows whose answer is implied by the project. UPC project → "Wo kam es an?" is implied (CMS). Project with our_side → perspective implied. Project with proceeding_type_id → cascade root narrows to the matching forum (and deeper, if mappable).
Pillar 2 — Visual hierarchy overhaul
All decision layers are the same primitive: a row with a question label, an answer-area, and an inline "ändern" affordance. Whether the layer is mode-toggle, perspective, inbox, or a cascade level, the visual shape is identical. The active layer expands inside its row; inactive (answered) layers compact to a single line.
Pillar 3 — Row-by-row persistent cascade
Replace breadcrumb drilldown with stacked rows. Each answered decision stays visible as a compact row. The active question is the only row that expands. The cascade builds top-to-bottom; the user sees every choice they made in one glance, and the answered rows act as their own affordances for "ändern".
The pillars interact:
- Pillar 3 (row layout) needs to know what to skip (Pillar 1 narrowing). A skipped row can render as a compact "(aus Akte) UPC CMS" pseudo-row, or be absent. We pick per row in §5.
- Pillar 2 (visual hierarchy) defines how answered vs active vs skipped-but-shown rows look. The four-different-treatments mess gets resolved by a single
.fristen-rowprimitive. - Pillar 1 (narrowing) also affects initial state: in Akte-mode, several rows may render as already-answered on page load. The cascade jumps to the first un-answered row.
2. The row primitive
The whole new layout is built from one element shape. Call it .fristen-row (the existing .fristen-b1-* class names get retired or rebased).
┌─ .fristen-row ──────────────────────────────────────────────────────┐
│ .fristen-row-num .fristen-row-label .fristen-row-edit │
│ [1] Wie suchen? [ändern] │
│ .fristen-row-body │
│ ✓ Schritt-für-Schritt │
└──────────────────────────────────────────────────────────────────────┘
Three states:
2.1 state="active" — the user is answering this row
┌─ .fristen-row.is-active ────────────────────────────────────────────┐
│ [3] Von wem ist das Schriftstück? │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ⚖️ │ │ 🏛️ │ │ ✉️ │ │
│ │ Vom Gericht │ │ Von der │ │ Vom Patent- │ │
│ │ │ │ Gegenseite │ │ amt │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ← zurück │
└──────────────────────────────────────────────────────────────────────┘
Same chip-style buttons regardless of which row it is. Mode pick = two big chips. Perspective = three chips. Inbox = four chips. Cascade step = N chips, one per child node. Leaf cascade chips get a subtle modifier (.fristen-row-chip--leaf) so the user can see "this one ends the cascade".
2.2 state="answered" — the user has picked, but the answer is below
┌─ .fristen-row.is-answered ──────────────────────────────────────────┐
│ [1] Wie suchen? ✓ Schritt-für-Schritt │
│ [ändern] │
└──────────────────────────────────────────────────────────────────────┘
Single line. The label, the picked answer, an "ändern" affordance. Click anywhere on the row (or the explicit ändern link) re-opens the row as active and drops every row below it. (This matches the existing breadcrumb-click semantic: jumping back to an ancestor invalidates descendants.)
2.3 state="prefilled" — derived from the project (or other auto-source)
┌─ .fristen-row.is-answered.is-prefilled ─────────────────────────────┐
│ [2] Ich vertrete ✓ Klägerseite │
│ aus Akte: HL-2024-001 [ändern] │
└──────────────────────────────────────────────────────────────────────┘
Visually identical to is-answered but carries a small "aus Akte: " tag and a slightly muted background. Clicking ändern flips it to active (and drops the prefilled marker — the user has now made an explicit choice).
This generalises t-paliad-164's perspective predefine: same shape, same hint, same override-by-click semantics. The hint becomes a row-level token rather than a one-off <span> next to the chip strip.
2.4 state="hidden" — row is implied by an earlier pre-fill
A row that adds no information given upstream rows can be omitted entirely. e.g. UPC project → forum is upc → inbox row's only valid answer is "CMS" → the row simply doesn't render. We do not render a is-hidden placeholder; the absence is the affordance. (This is m's "no need to show non-UPC options".)
The first user-actionable row floats up under the prefilled stack.
2.5 Why one primitive
The current four-layer mess works against m because each layer looks like a different kind of question. The row primitive collapses that: every decision row carries the same "label + answer + ändern" anatomy. The user reads top-to-bottom; the answered rows stack as a paper trail; the active row is the only thing that demands interaction.
This also implicitly solves the row-count tax of m's "see your selections" ask: the rows compact to ~28px each when answered, so even a deep cascade keeps the active question in the upper third of the viewport.
3. Answered / active / prefilled / hidden — visual treatment
Concrete CSS sketch (Slice 1 will tune; this is the contract):
| Token | Active | Answered | Prefilled | Hidden |
|---|---|---|---|---|
min-height |
auto (chips wrap) | 28px |
28px |
0 (not rendered) |
background |
var(--surface-card) |
transparent |
color-mix(var(--color-accent) 4%, transparent) |
n/a |
border-left |
4px solid var(--color-accent) |
none | 4px solid var(--color-accent-faded) |
n/a |
font-weight (label) |
600 | 500 | 500 | n/a |
font-weight (answer) |
n/a | 600 | 600 | n/a |
cursor |
default | pointer (whole row) | pointer (whole row) | n/a |
ändern affordance |
hidden | shown on hover + always on focus-within | always shown | n/a |
| Row number badge | accent-filled | outlined | outlined (faded) | n/a |
No ::before { inset: 0 } overlay tricks. The whole-row click is wired via a JS handler that calls reopenRow(idx) and skips clicks on <a> / <button> inside the row body — same pattern as .entity-table and the project-detail Verlauf items (CLAUDE.md anchor under "Whole-card / whole-row click").
Active vs answered transition: when the user picks an answer in an active row, the row collapses to is-answered and the next un-prefilled row materialises as active. The DOM is preserved across the transition (row stack is one container with data-state attribute switched on each row); the chip set inside the answered row replaces with the single ✓-prefixed answer span.
For the prefilled state's "aus Akte: " tag — reference comes from project.reference (e.g. HL-2024-001), falling back to the first 8 chars of project.id if no reference. Click on the reference tag is a navigation shortcut to the project (open in new tab — keeps the Fristenrechner state intact).
4. Project-driven narrowing — data mapping
What can we derive from a selected project, and where does each derivation land?
4.1 Mapping table
| Derivation | Source column(s) | Maps to | Pre-fills row | Hides row? |
|---|---|---|---|---|
| Forum (upc / de / epa / dpma) | proceeding_type_id → proceeding_types.code prefix. Fallback: court free-text contains UPC/LG/OLG/BGH/BPatG/EPA/DPMA. |
Cascade filter (existing inboxFilterAllowsForums). |
"Wo kam es an?" if forum=UPC (→ CMS). DE: prefills nothing (beA vs Posteingang is a Postal Realität, not on the project). | UPC: yes. DE/EPA/DPMA: no. |
| Perspective | project.our_side ∈ {claimant, defendant} |
Cascade filter (existing perspectiveAllowsParty). |
"Ich vertrete" → Klägerseite / Beklagtenseite. both / court / NULL: no prefill. |
No — even when prefilled, the row stays visible (the user needs to see "ah yes, I'm the Beklagte here"). |
| Proceeding type | proceeding_type_id + jurisdiction → fristenrechner code via mapLitigationToFristenrechner() (new helper, shared with t-paliad-178 Slice 2) |
Cascade depth: prunes root buckets that don't apply, and prunes inner buckets to those matching the proceeding code. e.g. UPC + INF → only cms-eingang.gegenseite.upc-inf.*, cms-eingang.gericht.urteil-upc-cfi, etc. |
Pre-collapses cascade sub-branches; surfaces deeper-leaf rows directly when only one path applies. | Hides intermediate cascade rows whose only child matches the derived code. |
| Counterclaim | counterclaim_of IS NOT NULL |
Implies with_ccr / with_cci condition flag context. |
Not a cascade row today — surfaces as a condition_flag chip on the wizard. Out of scope for this design; flagged in §13 Q6. |
n/a |
| Filing / grant dates | filing_date, grant_date |
Wizard anchor pre-fill. | Not a cascade row. Out of scope. | n/a |
4.2 Detail: the litigation → fristenrechner mapping
t-paliad-178 §0 and the task brief both call out: project.proceeding_type_id points at the 7 litigation codes (INF, REV, CCR, APM, APP, AMD, ZPO_CIVIL). The cascade speaks fristenrechner codes (UPC_INF, DE_INF, ...). A small mapping is needed:
INF + UPC → UPC_INF
INF + DE → DE_INF (first instance; OLG/BGH not derivable from project)
REV + UPC → UPC_REV
REV + DE → DE_NULL
CCR + UPC → UPC_INF + condition_flag=[with_ccr] (linked via parent's proceeding)
CCR + DE → DE_NULL (German Nichtigkeit IS the counterclaim equivalent)
APP + UPC → UPC_APP
APP + DE → DE_INF_OLG | DE_NULL_BGH (ambiguous — needs court or instance hint; degrade)
APM + UPC → UPC_PI
AMD + UPC → UPC_INF + condition_flag=[with_amend]
ZPO_CIVIL + DE → ZPO civil only; ignore for cascade (no fristenrechner code)
The mapping lives in one place — a new internal/services/proceeding_mapping.go (or the same shared helper t-paliad-178 Slice 2 introduces). The frontend gets the resolved fristenrechner code plus condition_flag array as part of the project payload (ProjectOption.derived_fristenrechner_code + .derived_condition_flags).
Honest about degrade: the mapping isn't always 1:1. APP+DE is ambiguous, ZPO_CIVIL has no analogue, and projects without proceeding_type_id (all 11 live ones today) get no derivation at all. The cascade falls back to forum-only narrowing in every ambiguous case. Never silent FK promotion.
4.3 Detail: court free-text fallback
When proceeding_type_id is NULL but court has a recognisable substring:
court contains "UPC" → forum=upc
court contains "BPatG" → forum=de (Nichtigkeit / DPMA-Beschwerde)
court contains "BGH" → forum=de
court contains "OLG" → forum=de
court contains "LG" → forum=de
court contains "EPA" / "EPO" → forum=epa
court contains "DPMA" → forum=dpma
otherwise → no narrowing
This is a UX nicety, not a correctness mechanism. The fuzzy match always loses to a real proceeding_type_id if both are set. Surfaces as the prefilled-row reference tag: "Forum: UPC (aus Gericht: UPC CoA)".
4.4 What the cascade hides given a forum
event_categories.forums is the live signal:
- 91/103 leaves carry a forum tag.
- 12 are neutral (cross-cutting:
frist-verpasst,sonstiges, some Mündl-Verhandlung leaves, court actions).
With forum=upc active, ~73 leaves drop from the cascade. The user sees the same root buckets (cms-eingang / muendl / beschluss / frist-verpasst / ich-moechte-einreichen / sonstiges) but each bucket's children list collapses to the upc-relevant subset. This is already wired today; the design doesn't change the filter, only its visual presentation.
The new contribution: when a non-leaf bucket reduces to a single descendant chain (e.g. UPC project → cms-eingang → gegenseite → upc-inf is the only chain), the cascade should optionally auto-walk the chain and surface the leaf parent's siblings directly. §5 below.
4.5 What the cascade hides given perspective
Currently only the 16 ich-moechte-einreichen.* leaves carry party tags. So perspective filters outgoing-filing nodes only. Incoming cms-eingang.gegenseite.* nodes don't have party tags — receiving from the opposing side is symmetric (you receive what they sent, regardless of who you are). This is correct and doesn't need fixing.
Design implication: the perspective row is always visible (rows can never be is-hidden based on perspective alone), even when prefilled, because its filter affects user-write decisions that the user might still want to override. Match t-paliad-164.
5. What gets pre-answered, hidden, or skipped-but-shown
A concrete matrix per row, given live data + the rules above:
| Row | Question | Pre-fill source | UPC project | DE project | EPA / DPMA project | No project (ad-hoc) | No project (zero ctx) |
|---|---|---|---|---|---|---|---|
| R0 Mode | Wie suchen? | none | active | active | active | active | active |
| R1 Perspective | Ich vertrete | project.our_side |
prefilled iff our_side ∈ {claimant, defendant}; else active |
same | same (rare for EPA/DPMA — usually only court or NULL) |
active | active |
| R2 Inbox | Wo kam es an? | forum derivation | hidden (forum=upc ⇒ CMS implied) | active (beA vs Posteingang) | active | active | active |
| R3 Bucket | Was ist passiert? | none — user always picks the bucket | active | active | active | active | active |
| R4..Rn Cascade | per-node step_question_de |
proceeding-code derivation can pre-walk a single-child chain | optionally auto-walks single-child chains | same | same | active | active |
Notes:
-
R0 Mode: kept active in all cases. The user always picks Tree vs Filter (or skips R0 entirely if we ditch the mode toggle — see §6). The mode pick is meta and not derivable from the project.
-
R1 Perspective: a project with
our_side='both'is rare but legitimate; it lands as active.'court'is even rarer (m's project model includes a "we are the court" perspective for hypothetical training scenarios). For now:court→ active row. -
R2 Inbox: m's literal ask. UPC → hidden. DE → active (because beA vs Posteingang is meaningful for downstream Phase-0 manual workflows even if the cascade filter doesn't care). EPA/DPMA → active (e.g. EPA online filing vs Post). The "Alle" chip stays for "I don't know yet".
-
R3 Bucket: the 6 root buckets are always shown. Even with a derived proceeding code, the user still has to say "I'm here because I received something / mündl. Verhandlung / Urteil / etc." This is too coarse to derive.
-
R4..Rn Cascade auto-walk: when a derived proceeding code reduces a bucket's children to a single chain, the cascade should pre-walk that chain. e.g. UPC + INF +
cms-eingangbucket → onlygegenseite.upc-inf.*chain survives → R4gegenseiteis pre-answered (with the "aus Akte" badge), R5 jumps directly toupc-inf(also pre-answered), and R6 is the active question "Welcher Schriftsatz?". The user sees four R-rows (R0, R1 prefilled, R3 picked, R4 prefilled, R5 prefilled, R6 active) — clean paper trail of inference + one active question.Important constraint: auto-walk is descendants-of-the-picked-bucket only. R3 (bucket) is always active because the bucket is the user's intent. We never auto-pick the bucket. So a UPC project doesn't pre-pick "cms-eingang" for you; it just makes the sub-cascade efficient once you've said "cms-eingang".
5.1 Compact summary diagram — UPC INF project drilling into a cms-eingang opposing-side schriftsatz
┌─ Step 1: Akte (Step 1 surface, above Pathway B) ────────────────────┐
│ Akte: HL-2024-001 — Acme v. Globex (UPC INF) [Andere Akte] │
└─────────────────────────────────────────────────────────────────────┘
┌─ [1] Wie suchen? ✓ Schritt-für-Schritt [ändern]┐
└─────────────────────────────────────────────────────────────────────┘
┌─ [2] Ich vertrete ✓ Klägerseite [ändern]┐
│ aus Akte: HL-2024-001│
└─────────────────────────────────────────────────────────────────────┘
⨯ Row R2 (Inbox) hidden — UPC implies CMS
┌─ [3] Was ist passiert? ✓ CMS-Eingang [ändern]┐
└─────────────────────────────────────────────────────────────────────┘
┌─ [4] Von wem ist das Schriftstück? ✓ Von der Gegenseite [ändern]┐
│ aus Akte (UPC INF impliziert)│
└─────────────────────────────────────────────────────────────────────┘
┌─ [5] Welches Verfahren? ✓ UPC Verletzungsverfahren │
│ aus Akte: HL-2024-001 │
└─────────────────────────────────────────────────────────────────────┘
┌─ [6] Welcher Schriftsatz wurde eingereicht? (active, awaiting pick)│
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Klageschrift │ │ Klageerwiderung │ │ Replik │ │
│ │ (R.13) │ │ + Widerklagen │ │ │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
│ ... (rest of UPC_INF Schriftsätze) │
│ │
│ ← zurück │
└─────────────────────────────────────────────────────────────────────┘
Six rows. Three user picks (mode, bucket, leaf). Three Akte-derived prefills. One R2 absent. The user sees their full decision path at a glance.
For comparison, today's UI: the user clicks four times into the cascade, the top of the page is two chip strips and a radio they didn't touch, the breadcrumb at the top of .fristen-b1-cascade shows three crumb buttons in 12pt text, and there's no inline indication that the cascade is narrower than the full taxonomy. m's "no narrowing at all" is the literal reading of what's visible.
5.2 Compact summary diagram — DE project drilling into the same
┌─ [1] Wie suchen? ✓ Schritt-für-Schritt [ändern]┐
└─────────────────────────────────────────────────────────────────────┘
┌─ [2] Ich vertrete ✓ Klägerseite [ändern]┐
│ aus Akte: HL-2024-002│
└─────────────────────────────────────────────────────────────────────┘
┌─ [3] Wo kam es an? (active, awaiting pick)┐
│ │
│ ┌──────┐ ┌──────────────┐ ┌──────┐ │
│ │ beA │ │ Posteingang │ │ Alle │ │
│ └──────┘ └──────────────┘ └──────┘ │
└─────────────────────────────────────────────────────────────────────┘
... and the cascade continues below once R3 is answered.
R2 (Inbox) is active because beA vs Posteingang is a real distinction for German projects. The forum is already known (de), so the cascade below R3 will be DE-only — but the user still tells us how the document arrived.
5.3 Compact summary diagram — abstract / no-Akte mode
┌─ [1] Wie suchen? (active, awaiting pick)┐
│ │
│ ┌────────────────────────┐ ┌─────────────────────┐ │
│ │ Schritt-für-Schritt │ │ Filter / Suche │ │
│ │ (Entscheidungsbaum) │ │ │ │
│ └────────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
No prefills, no hidden rows. Every row is asked. Full taxonomy.
6. Filter / Suche mode — coexistence with the cascade
Today's mode toggle (radio) is a UX wart: it's the only radio on the page, it looks unlike everything else, and it sits at the top of Pathway B as if it were a primary axis.
Two options to fold it into the row model:
Option A — Mode is R0, a row like any other
The mode toggle becomes the first row in the stack. Two chips. Pick determines what populates below: tree picker → R3 + cascade. Filter picker → R3 collapses into a search input + result list. The row stays visible (you can switch mid-flow via ändern), but the chrome is consistent.
Pros: simple, every decision is a row, the page reads top-to-bottom. Cons: adds one always-active row to every flow including the "I know what I'm doing, just give me search" use case.
Option B — Mode is an escape hatch, not a row
Filter is positioned as "ich weiß schon, wonach ich suche" — a small link / icon at the top of Pathway B that toggles between cascade and search. No R0 row. Default = cascade. Click → search replaces the row stack.
Pros: fewer rows, less for the common case to scan past. Cons: more discoverable than the current radio? unclear. "Where did the radio go?" is a question.
Option C — Filter as a bottom-of-stack affordance
Cascade is the only top-down flow. Below the cascade results, a "Sie wissen schon den Namen? → direkt suchen" link / row appears. Search is a graceful fallback, not a peer mode.
Pros: gives cascade the primary surface, search becomes a tool for "wait, I know better". Cons: discoverability of search is reduced for power users who DO know.
Inventor's pick: Option B. The radio is dead weight, and the search use case is "I know the name; let me skip the cascade" — that's an escape hatch, not a peer axis. Visually: a small 🔍 icon-button at the top-right of Pathway B titled "Direkt suchen". Click expands a search input that replaces the row stack; result list appears below; "← Zurück zum Entscheidungsbaum" returns to the row stack with prior state preserved.
But this is design-question territory — m's call. §13 Q1.
7. Mobile + responsive
The row primitive is naturally responsive: rows stack vertically by default. Width concerns only the chip set inside an active row.
7.1 Breakpoints
paliad already uses 640 / 768 / 1023 px breakpoints. The rows live inside .fristen-pathway-shell which is already a column-flex.
| Width | Row chrome | Chip layout (active row) |
|---|---|---|
| ≥ 1024px | full label + answer + ändern on one line, badge left | chips in a 3-column grid (or auto-fill min 220px) |
| 768–1023px | same | chips in a 2-column grid |
| 640–767px | label + answer on line 1, ändern on line 2 right-aligned | chips in a 1-column stack |
| < 640px | label on line 1, answer on line 2, ändern as › icon right-aligned |
chips full-width, single column |
7.2 Active-row collapse on tap (mobile-only)
On < 768px, the row stack scrolls; the active row's chip set can be long (e.g. 9 Schriftsatz children). When the user picks an answer, the page autoscrolls so the next active row is at the top of the viewport. This is the same pattern as the Akte picker (Step 1) and existing form flows.
7.3 What we don't do on mobile
- No drawer / modal for the cascade. The whole point of the row stack is being able to see history at a glance; collapsing into a separate surface defeats it.
- No fly-out for ändern. Tap on an answered row's ändern affordance simply re-activates the row in place.
- No "next" button. Picking a chip advances automatically; mobile doesn't need an extra tap to confirm.
8. "Neu starten" / Reset semantics
Three flavours of reset, all need a home:
8.1 Reset the whole cascade (every row to empty)
Today: clicking the breadcrumb's "Pfad zurücksetzen" root crumb. In the new layout: a small ↺ Pfad zurücksetzen link at the top of the row stack, right of the heading. Clicking it:
- Drops every cascade row (R3+).
- Leaves R0 (Mode), R1 (Perspective prefilled), R2 (Inbox if visible) as they are — those are "context", not "the user's investigation".
- Re-activates R3.
Optional behaviour (per Q9): a confirm-dialog if the user has drilled ≥ 3 cascade levels deep. Probably overkill; current breadcrumb root-click is destructive without confirm. Match existing semantic.
8.2 Drop just one decision (ändern semantic)
Built into every answered row's [ändern] affordance and clicking on the row body. Effect: that row reverts to active; every row below it drops; URL ?b1= shortens to that row's prefix.
This is the workhorse of the row stack — m's "you can see your selections" UX implies "you can also rewind to any of them at any time". Built-in.
8.3 Drop the Akte-derived prefills
Trickier: if the user clicks ändern on a is-prefilled row, the prefill is overridden. But what about "I want to ignore my Akte entirely for this exercise"? The Akte itself is bound at the Step 1 surface, above Pathway B. Clicking "Andere Akte" at the Step 1 summary unbinds the Akte and drops all is-prefilled markers. The cascade rows that were is-answered because they were prefilled now revert to is-active (or, if the user had already explicitly overridden via ändern, stay answered with no is-prefilled flag).
This semantic already half-exists for t-paliad-164's perspective predefine; we generalise it to every prefilled row. Implementation: hold a prefillSources: Map<rowID, "akte" | "user"> and re-derive on Akte unbind / change.
8.4 The "Neu starten" button at the bottom
A second affordance at the bottom of the results area, after the user has reached a leaf and is reading concept-cards. "Andere Frist nachschlagen?" → reset to R3. Optional but discoverable; today's UI lacks an equivalent, so this is a small UX win.
9. Search affordance integration
Tied to §6's mode-toggle question. Two integration points:
9.1 Search panel placement (Option B from §6)
The 🔍 Direkt suchen button lives at the top-right of .fristen-pathway-shell. Click → animates the row stack out (or simply replaces it), shows a search input row with a single text field + result list below. ESC or "← Zurück zum Entscheidungsbaum" returns; row stack restores via URL state.
The search is the existing ?q= + B2 chips flow — we don't rebuild it, just relocate its entry point. Existing forum-filter chip row stays inside the search panel.
9.2 Inline search on each cascade row (rejected)
An alternative: each cascade row's chip list gets a tiny "filter chips" input at the top. Reject. Adds chrome to every active row for a feature most users don't need.
9.3 "I searched but want to see the path" round-trip
When the user lands on a leaf via search, optionally show "Im Entscheidungsbaum öffnen → " — clicking restores the row stack with all ancestor rows pre-answered (which is what the cascade's slug already encodes). This is a small extra: lets a search-first user verify "yes, this is the leaf I thought, here's the proceeding context I missed".
10. Slicing for the coder pass
Three slices, each independently shippable, mergeable in order:
Slice 1 — Visual hierarchy + row-by-row layout (no narrowing change)
Replaces the four-layer mess with the row primitive. No backend or DB changes. The narrowing engine stays the same (existing forum + perspective filters fire); the visual presentation moves from breadcrumb + chip strips + radio → row stack.
In scope:
- New
.fristen-rowCSS primitive (with.is-active,.is-answered,.is-prefilledmodifiers). - Refactor
renderB1Cascadeinto a row-stack renderer (renderRowStack(rows: RowSpec[])). - Migrate L1 (mode) / L2 (perspective) / L3 (inbox) / L4..n (cascade) all to row instances.
- "ändern" semantic = re-activate row, drop rows below, push history state.
- Reset link at top of stack.
- i18n keys for row labels.
Out of scope for Slice 1:
- Project-derived proceeding-code narrowing (the
mapLitigationToFristenrechnerhelper). - Auto-walk single-child cascade chains.
- Hide-R2-on-UPC behaviour (Slice 2 — needs the proceeding mapping helper anyway).
- Search affordance relocation (Slice 3).
Outcome: same data, same narrowing, vastly better visual narrative. The user can finally see their decision path. m's pillar 2 + 3 are addressed.
Slice 2 — Project-driven narrowing depth
Adds the litigation_code × jurisdiction → fristenrechner_code mapping and uses it to:
- Pre-fill the proceeding-type sub-cascade rows (R5 in the §5.1 diagram).
- Hide R2 (Inbox) when project is UPC.
- Auto-walk single-child chains.
- Add the "aus Akte: " tag on prefilled rows.
This is where Pillar 1 fully lands. Depends on Slice 1's row primitive.
Includes a small backend helper (shared with t-paliad-178 Slice 2 if both ship in parallel): internal/services/proceeding_mapping.go exposes MapLitigationToFristenrechner(litCode string, jurisdiction string) (fristenCode string, conditionFlags []string, ok bool).
Outcome: an Akte-bound user starts the cascade with three rows already answered, and only one or two active questions remain to drill to the leaf.
Slice 3 — Search affordance + mobile polish
Relocates the mode-toggle / search affordance per §6 Option B. Adds the responsive breakpoints from §7. Polishes the autoscroll-to-active behaviour on mobile.
Mobile-only fixes ride here so Slices 1+2 can be reviewed by m at desktop width first.
Why this order
- Slice 1 is purely visual. m can see the row stack and validate the layout BEFORE we change any narrowing semantic. If m hates the row primitive, we revert one PR. (We won't — but the option matters.)
- Slice 2 is the heavy correctness lift. It depends on the mapping helper, on Akte payload extensions, and on careful Test_DATABASE_URL integration tests.
- Slice 3 is final polish. Independently mergeable, lowest risk.
Each slice is roughly:
- Slice 1: 1 frontend PR (~700 LoC TSX + CSS + client). No backend, no migrations.
- Slice 2: 1 mixed PR (~150 LoC Go + 300 LoC client). No migrations.
- Slice 3: 1 frontend PR (~150 LoC).
11. Tradeoffs flagged
11.1 Row stack is taller than the current shell
A deep cascade (4 levels) plus 3 prefilled rows + R0 = 8 rows. Each ~28px compact + the active row's chip body (200–400px depending on chip count) + spacing → ~600–800px tall. The current shell is ~400px tall in the same scenario. Mitigation: rows are compact (28px), active-row autoscrolling keeps the chip set in view on mobile, and the visual narrative wins. m's ask explicitly trades vertical space for visibility.
11.2 "Aus Akte" tags are slightly noisy
Three rows showing "aus Akte: HL-2024-001" reads a bit redundant. Mitigation: only the first prefilled row shows the reference; subsequent rows show "(aus Akte)" without the reference. Saves vertical noise, keeps the source visible once.
11.3 Auto-walk single-child chains can confuse
The user picks "cms-eingang" → suddenly two rows materialise pre-answered. Looks magical. Mitigation: the two rows are clearly is-prefilled with an "aus Akte (UPC INF impliziert)" tag, and ändern is available on each. After the user has done it twice, the inference becomes a feature; before, a tooltip on first-render ("Diese Schritte ergeben sich aus Ihrer Akte") could help (deferred for v2 — see Q11).
11.4 Removing the radio mode-toggle is a behavioural change
Existing power users may know the radio. Mitigation: the new 🔍 Direkt suchen icon-button at the top of Pathway B is a visible affordance; URL ?mode=filter still works as deep-link. Soft transition.
11.5 11/11 live projects have NULL proceeding_type_id
Slice 2's narrowing literally doesn't fire in production today. We're building UX that requires data nobody has yet. Mitigation: graceful degrade (forum-only narrowing via court free-text fuzzy match — already a feature today). Backfill of proceeding_type_id is a separate follow-up (see Q13).
11.6 The mapping table in §4.2 has ambiguities
APP+DE → ambiguous; ZPO_CIVIL → no analogue; CCR ↔ counterclaim modeling is fragile. Mitigation: every ambiguous case degrades to "no narrowing" — the row stays active rather than incorrectly pre-filled. Better silent than wrong.
11.7 ändern-on-an-ancestor invalidates descendants
Same as today's breadcrumb-click semantic — clicking a non-current crumb drops cascade depth. No data is lost (you can re-walk the cascade), but if the user was reading concept-cards at a leaf, those cards disappear. Mitigation: when ändern is clicked on an answered row, before dropping descendants, brief inline confirmation? Or just match today's behaviour (drop immediately). Inventor recommends match-today; Q12.
11.8 The row primitive may be over-engineered
A single visual primitive for four functionally different layers is a strong opinion. If a future cascade layer (e.g. variant chips for condition_flag) doesn't fit the primitive shape, we have to either extend the primitive or break the consistency. Mitigation: the primitive is shape (label + answer-area + ändern), not behaviour — variant chips fit because they're also "pick one (or several)". The contract is loose enough.
12. Files the implementer will touch (Slice 1 only)
12.1 Frontend
frontend/src/fristenrechner.tsx:227-310— Pathway B markup. Replace.fristen-mode-toggle+.fristen-perspective-bar+.fristen-inbox-bar+.fristen-b1-cascadewith a single.fristen-row-stackcontainer. Add minimal scaffolding rows for mode / perspective / inbox / cascade-host. Keep.fristen-b1-resultsbelow — unchanged.frontend/src/client/fristenrechner.ts:2405-2574— RefactorrenderB1CascadeintorenderRowStack(rows). The row spec is a discriminated union:{kind: "mode" | "perspective" | "inbox" | "cascade", state: "active" | "answered" | "prefilled", question, options[], picked?}. Rendering is one function per state; one switch onkindfor the options builder.frontend/src/client/fristenrechner.ts:2914-3081—inboxFilterAllowsForums+perspectiveAllowsPartyunchanged (Slice 1 is visual-only).frontend/src/client/fristenrechner.ts:initInboxFilter+ perspective init — same handlers, new DOM targets.frontend/src/client/i18n.ts— ~20 new keys underdeadlines.row.*(row labels, ändern affordance, prefilled tag, reset link, "next active" autoscroll-target announce).frontend/src/styles/global.css:1636-1822+:1965-2065— Retire.fristen-mode-toggle,.fristen-perspective-bar,.fristen-inbox-bar,.fristen-b1-breadcrumb,.fristen-b1-question,.fristen-b1-buttons,.fristen-b1-button*. Add.fristen-row-stack,.fristen-row,.fristen-row-num,.fristen-row-label,.fristen-row-answer,.fristen-row-edit,.fristen-row-body,.fristen-row-chip,.fristen-row-chip--leaf,.is-active,.is-answered,.is-prefilled.
12.2 Backend
No backend changes for Slice 1. The existing /api/tools/fristenrechner/event-categories and /api/tools/fristenrechner/search endpoints are unchanged.
12.3 Tests
- Pure-TS unit tests for
buildRowStack(currentState)if extracted (table-driven: given URL state + Akte payload, output the RowSpec[]). - Playwright smoke (post-deploy): land on Pathway B with
?path=b&project=<uuid>, verify R1 prefilled with "aus Akte", R2 hidden for UPC project, ändern on R1 reopens, ändern on bucket drops cascade depth.
12.4 Anchoring back
t-paliad-164 perspective predefine code is the precedent. Re-read it before implementing — same hint mechanism, same override semantics, generalised.
t-paliad-178 Slice 2 (Step 0 toggle + Akte auto-derivation) is parallel; coordinate on the shared proceeding_mapping.go helper file (Slice 2 of this task introduces it; t-paliad-178 Slice 2 can adopt or vice versa, depending on which lands first).
13. Open questions for m
These are inventor's calls flagged for m's gate. Picking is on m, not the coder.
Q1 — Mode-toggle disposition. Three options in §6: (A) R0 row, (B) escape-hatch icon-button [inventor's pick], (C) bottom-of-stack affordance. Pick one or specify another.
Q2 — UPC project: hide R2 entirely or show as compact prefilled?
- Hide entirely (inventor's pick — matches m's "no need to show non-UPC options").
- Show as compact
[2] Wo kam es an? ✓ UPC CMS [ändern] aus Akterow — verbose but explicit.
Q3 — Auto-walk single-child cascade chains?
- Yes, materialise R4..Rn-1 as prefilled (inventor's pick — strong UX, but feels magical first time).
- No, the user always picks their way down even when only one child applies (slower, more predictable).
- Yes-but-only-when-≥-2-rows-collapse (tradeoff).
Q4 — "ändern" affordance shape on an answered row.
- Hover-revealed link "ändern" (inventor's pick — keeps row clean by default).
- Always-visible pencil icon (more discoverable but more chrome).
- Whole-row click is the only handle (cleanest, but no visible affordance — newcomers won't discover it).
Q5 — Drop confirmation when ändern invalidates descendants?
- No (match today's breadcrumb-click — inventor's pick).
- Yes, when ≥ 3 cascade levels would be dropped.
- Always — even a one-row drop confirms.
Q6 — Counterclaim awareness in the cascade.
project.counterclaim_of IS NOT NULL implies [with_ccr] or [with_cci] condition flag depending on the parent's proceeding code. Should this surface as a prefilled row (e.g. "Variante: with_ccr"), or only as a backend filter on the result concept cards (silent)?
- Surface as a prefilled row (transparency — user sees the variant is active).
- Silent backend filter (no row tax, but mystery narrowing).
- Out of scope for this design — handle in a separate variant-chip task.
Q7 — R0 mode-pick deep link.
If a user lands on ?path=b without ?mode=, do we default to tree or to "no R0 picked yet"?
- Default to tree, R0 prefilled (today's behaviour — silent).
- R0 active until the user picks (more explicit, but adds one extra click for the common case).
Q8 — Prefilled-row override permanence. After the user clicks ändern on a prefilled R1 (perspective) and explicitly picks "Beklagter" instead of the Akte's "Kläger", does this override persist if they re-bind the same Akte?
- No, re-bind re-applies (today's behaviour — clean, but overrides feel ephemeral).
- Yes, store override per-Akte in localStorage (sticky overrides — UX-friendly, but new state).
Q9 — Reset confirm. A "Pfad zurücksetzen" link at the top of the row stack — confirm dialog?
- No confirm — match today's breadcrumb root-click (inventor's pick).
- Confirm if cascade depth ≥ 3.
- Always confirm.
Q10 — Search escape-hatch position.
Per §6 / §9, the 🔍 Direkt suchen button sits at the top-right of Pathway B.
- Top-right (inventor's pick — discoverable, doesn't push down the row stack).
- Below the row stack, after results.
- As a permanent row at the bottom of the stack.
Q11 — First-visit tooltip on auto-walked rows. "Diese Schritte ergeben sich aus Ihrer Akte" tooltip on the first prefilled-from-mapping row, dismissed forever on first close?
- Yes (helps onboarding).
- No (extra chrome; the "aus Akte" tag is enough).
- Inline help-icon (?) link to a docs page (longer-form).
Q12 — Concept cards live below the row stack today. Should they collapse / hide when the user reopens an ancestor row (ändern)?
- Collapse/hide on ändern, repopulate when the cascade reaches a leaf again (inventor's pick — matches the "no orphan content" rule).
- Keep visible as last-known until cascade resolves to a new leaf.
Q13 — Backfill paliad.projects.proceeding_type_id?
11/11 live rows are NULL. Slice 2's narrowing depends on this. Should the Slice 2 PR also include a one-off Akte-edit nudge ("Projekt-Setup vervollständigen: Verfahrensart fehlt"), or do we wait until m manually fills them in over time?
- Inline "Verfahrensart ergänzen" link on Akten with NULL proceeding_type_id.
- Backfill script (inferring from
courtfree-text where unambiguous). - Defer entirely; live with degraded narrowing until users fill it organically.
Q14 — Reorder rows so prefilled stack at top, user-picked at bottom? The §5.1 diagram orders rows R0..Rn in their natural cascade sequence (mode → perspective → inbox → bucket → cascade depth). The prefilled rows happen to be R1, R4, R5 (not contiguous). Alternative: visually float all prefilled rows to a single "aus Akte" group at the top, with user-picked rows below. Tradeoff: cleaner separation vs. losing the temporal narrative of the decision path.
- Keep natural order (inventor's pick — narrative wins).
- Group prefilled at top.
Q15 — Should Filter / Suche mode also see Akte prefills?
If the user enters search mode with a project bound, do we silently scope results to the project's forum, or show the full taxonomy?
- Scope (consistent with cascade narrowing — inventor's pick).
- Don't scope (search is a "I know what I'm looking for" mode; the project is incidental).
- Scope with a visible toggle "Auch andere Foren anzeigen".
DESIGN READY FOR REVIEW
Awaiting m's go/no-go on the questions in §13 before the coder shift starts. Inventor (pauli) parks after this commit — no implementation kickoff, no other-skill autoload, head gates the transition.
Recommended implementer: pattern-fluent Sonnet coder. The row primitive is straightforward CSS + a small state machine refactor; the precedent code (t-paliad-164 + t-paliad-133 cascade engine) is well-understood. NOT cronus per memory directive 2026-05-06.