fix(verfahrensablauf): m/paliad#58 — UPC CCR roadmap (EN label + spawn-as-standalone)

m's 2026-05-20 14:08 reports on /tools/verfahrensablauf:

  1. "There seems to be a lacking english term here" — picking
     UPC CCR shows "Trigger event: Widerklage auf Nichtigkeit" on EN.
  2. "Nothing shows in the roadmap" — the timeline is empty because
     upc.ccr.cfi has no native rules (it's an illustrative peer that
     normally runs as a sub-track of upc.inf.cfi with with_ccr).

Root cause for (1): UIResponse.proceedingName was DE-only. When a
proceeding had no root rule the frontend fell back to that field, so
EN users saw the DE label. The DB already has bilingual names; this
was pure plumbing.

Root cause for (2): the upc.ccr.cfi proceeding-type row exists for
the picker (mig 096) but ResolveCounterclaimRouting — the helper
that maps it to upc.inf.cfi with the with_ccr flag — was defined
but never called. Calculate queried rules directly off upc.ccr.cfi
and got an empty list.

Fix:

  * Add ProceedingNameEN, ContextualNote, ContextualNoteEN to
    UIResponse. Frontend triggerEventLabelFor now consults the EN
    name on EN, falling back to DE only if the EN field is empty.
  * New SubTrackRouting registry in proceeding_mapping.go and a
    LookupSubTrackRouting lookup — single source of truth for the
    "this proceeding has no native rules, route to a parent with
    flags + show a contextual note" pattern. Today's only entry is
    upc.ccr.cfi → upc.inf.cfi + with_ccr; the pattern generalises
    to other sub-tracks via data-only additions.
  * Calculate consults the registry at the top: when a hit, the
    proceeding type is re-resolved to the parent for rule lookup, the
    default flags are merged into the user's flag set (user flags win
    on conflict), and the response identity (Code/Name/NameEN) stays
    on the user-picked proceeding so the page header still reads
    "Counterclaim for Revocation". The bilingual note surfaces in
    ContextualNote{,EN}.
  * Frontend renderResults paints a lime-accent banner above the
    timeline body when the response carries a note
    (.timeline-context-note). escHtml already exported from
    views/verfahrensablauf-core — imported here for the banner.

No DB migration: SELECTs against paliad.proceeding_types,
paliad.deadline_rules, and paliad.trigger_events confirm every
active row already has a non-empty name_en / name. The bug was
the API + frontend never reading the EN columns through the
proceedingName fallback path.

Tests: TestSubTrackRoutings pins the registry shape (every entry
has matching key/value, non-empty parent+flags, bilingual notes;
CCR's exact shape is asserted; non-sub-tracks miss). The existing
TestResolveCounterclaimRouting continues to pass because the
helper now consults the registry but the CCR semantics are
unchanged.
This commit is contained in:
mAi
2026-05-20 14:51:55 +02:00
parent 111c7c39e8
commit ea9823db80
6 changed files with 215 additions and 15 deletions

View File

@@ -13,6 +13,7 @@ import { initSidebar } from "./sidebar";
import {
type DeadlineResponse,
calculateDeadlines,
escHtml,
formatDate,
populateCourtPicker,
renderColumnsBody,
@@ -157,13 +158,19 @@ async function doCalc() {
// the first event in the proceeding — e.g. Klageerhebung for
// upc.inf.cfi, Nichtigkeitsklage for upc.rev.cfi. Falls back to the
// active proceeding name if no root rule fires (shouldn't happen for
// healthy data, but safer than a blank).
// healthy data, but safer than a blank). Fallback respects language —
// proceedingNameEN is consulted on EN before the DE proceedingName
// (m/paliad#58: prior fallback rendered DE on EN for sub-track
// proceedings like upc.ccr.cfi which had no rules → no root).
function triggerEventLabelFor(data: DeadlineResponse): string {
const root = data.deadlines.find((d) => d.isRootEvent);
if (root) {
return getLang() === "en" ? (root.nameEN || root.name) : (root.name || root.nameEN);
}
return data.proceedingName || "";
if (getLang() === "en") {
return data.proceedingNameEN || data.proceedingName || "";
}
return data.proceedingName || data.proceedingNameEN || "";
}
function syncTriggerEventLabel() {
@@ -193,11 +200,23 @@ function renderResults(data: DeadlineResponse) {
<span class="timeline-trigger-date">${t("deadlines.trigger.label")}: ${formatDate(data.triggerDate)}</span>
</div>`;
// Sub-track contextual note (m/paliad#58). Surfaces above the
// timeline body when the server routed the user-picked proceeding
// through a parent (e.g. upc.ccr.cfi → upc.inf.cfi with with_ccr).
// Plain-text banner — server-side copy is plain text per the
// SubTrackRouting contract.
const noteText = getLang() === "en"
? (data.contextualNoteEN || data.contextualNote || "")
: (data.contextualNote || data.contextualNoteEN || "");
const noteHtml = noteText
? `<div class="timeline-context-note" role="note">${escHtml(noteText)}</div>`
: "";
const bodyHtml = procedureView === "columns"
? renderColumnsBody(data, { editable: true, showNotes })
: renderTimelineBody(data, { showParty: true, editable: true, showNotes });
container.innerHTML = headerHtml + bodyHtml;
container.innerHTML = headerHtml + noteHtml + bodyHtml;
if (printBtn) printBtn.style.display = "block";
if (toggle) toggle.style.display = "";

View File

@@ -95,8 +95,21 @@ export function priorityRendering(
export interface DeadlineResponse {
proceedingType: string;
proceedingName: string;
// proceedingNameEN: English label of the picked proceeding. Empty
// when not populated server-side; frontend falls back to
// proceedingName. Used for the "Trigger event" fallback when the
// timeline has no root rule. (m/paliad#58)
proceedingNameEN?: string;
triggerDate: string;
deadlines: CalculatedDeadline[];
// contextualNote / contextualNoteEN render as a banner above the
// timeline. Populated when the picked proceeding is a sub-track of
// another proceeding (e.g. upc.ccr.cfi runs inside upc.inf.cfi with
// with_ccr) — the server routes to the parent's rules but keeps the
// picked proceeding's identity in the response, and the note
// explains the framing. (m/paliad#58)
contextualNote?: string;
contextualNoteEN?: string;
}
export interface CourtRow {

View File

@@ -3304,6 +3304,23 @@ input[type="range"]::-moz-range-thumb {
font-size: 1rem;
}
/* Sub-track contextual note banner (m/paliad#58). Renders above the
timeline body when the picked proceeding is a sub-track of another
proceeding (e.g. UPC CCR rendered standalone). Plain-text content;
white-space: pre-line preserves paragraph breaks if server copy
ever uses them. */
.timeline-context-note {
margin: 0 0 1rem;
padding: 0.7rem 0.9rem;
background: rgba(198, 244, 28, 0.10);
border-left: 3px solid var(--brand-lime, #c6f41c);
border-radius: 4px;
color: var(--color-text, #222);
font-size: 0.9rem;
line-height: 1.4;
white-space: pre-line;
}
.timeline {
position: relative;
}