Relocate the in-house OOXML machinery out of internal/services into the
first docforge adapter, with zero behaviour change:
submission_merge.go -> pkg/docforge/docx/merge.go (placeholder
substitution renderer + preview-HTML emitter)
submission_md.go -> pkg/docforge/docx/markdown.go (Markdown->OOXML
walker incl. the b78a984 underscore-fix)
submission_render.go -> pkg/docforge/docx/dotm.go (.dotm->.docx)
+ their _test.go files (git-tracked renames, 84-99% identical)
internal/services keeps thin type-alias + forwarder shims
(docforge_shims.go) so every caller in services/handlers/main compiles
and behaves identically: PlaceholderMap, MissingPlaceholderFn,
SubmissionRenderer, HyperlinkAllocator (aliases); NewSubmissionRenderer,
DefaultMissingMarker, RenderMarkdownToOOXML[WithStyles], ConvertDotmToDocx,
SanitiseSubmissionFileName (forwarders). docx.XMLAttrEscape is exported so
submission_compose.go's hyperlink-rels inserts reuse the walker's escaping.
Three mis-filed pretty-printer tests (legalSourcePretty, ourSideDE/EN,
patentNumberUPC) that exercise the vars layer move back to
internal/services/submission_vars_pretty_test.go.
Placeholder grammar + PlaceholderMap stay co-located with the renderer in
docx for now; slice 3 hoists the format-neutral grammar to the docforge
root with the VariableResolver interface.
Verification: go build ./... clean, go vet clean, full module test green
(the byte-exact OOXML golden tests in merge/compose/render pass unchanged
= behaviour preserved). gofmt drift on the moved files is pre-existing
(72/169 services files already drift; no gofmt gate).
m/paliad#157
Extract the submission generator into pkg/docforge: neutral document model
+ opaque carrier (lossless .docx), VariableResolver interface per namespace,
pluggable importer/exporter (.docx first), WYSIWYG authoring page, generic
editor UI package. 8-slice train, extract-in-place migration that protects
the b78a984 underscore fix, the placeholderRegex + data-var contracts, and
the building-block/section model.
Includes all 13 of m's decisions (5 prose-grill metaphor + 8 structured).
upc-kommentar deferred as a live consumer (it is Bun/SvelteKit/TS, zero Go);
abstractions sized for a later HTTP veneer.
m/paliad#157
m had a one-off /tmp/paliad-deadline-export.py (work/head delegation
#2572) that dumped every published sequencing_rules row. Output
showed 37 entries on upc.inf.cfi including optional rules
(Lodging of translations, Review of CMO, ...) which fights the
engine's IncludeOptional=false default and m's "naked proceeding
with options but not always displayed" mental model.
Move to exports/gen-deadline-list.py as the canonical re-runnable
script and add a SQL-level priority filter that matches the
engine. Default suppresses priority='optional'; --include-optional
opts back in for an exhaustive catalog dump.
- DSN overridable via PALIAD_DEADLINE_EXPORT_DSN env var.
- argparse-driven: --include-optional / -o OUT / --generated-for LABEL.
- Header explains the mode so the PA reader knows what's suppressed.
- Regenerated exports/upc-deadlines-2026-05-28.md: now 178 rules across
25 proceedings (vs the unfiltered run). upc.inf.cfi section drops
from ~37 to 28 mandatory + conditional rules - the optional ones
are gone; trigger_event_id mandatory rules stay in the catalog
(they're a real PA-knowable surface; runtime anchor state is what
decides whether they project into a timeline, separate concern).
Run:
uv run exports/gen-deadline-list.py [--include-optional]
(m/paliad#153)
The Litigation Builder triplet renders /api/tools/fristenrechner output
verbatim and never applied the pre-existing filterByDetailMode pass that
the legacy /tools/verfahrensablauf page uses. With the engine fix
(3c840c0 — pkg/litigationplanner default IncludeOptional=false + trigger
event semantic anchoring) already in main, optional rules are dropped
server-side but rules with an unsatisfied trigger_event_id surface as
IsConditional. Without filterByDetailMode those still rendered as
"abhängig von ..." cards on the triplet, polluting m's "naked
proceeding with options but not always displayed" mental model.
upc.inf.cfi went from 7 mandatory backbone events to 29 visible cards
(22 conditional noise — Lodging of translations, Mängelbeseitigung,
Antrag auf Verweisung, Wiedereinsetzung, ...). Live BEFORE/AFTER
captured in exports/screenshots/.
Fix layers:
- Go handler (internal/handlers/fristenrechner.go): accept
includeOptional + triggerEventAnchors from request body and
forward to services.CalcOptions. Default zero values match the
engine defaults (suppress optionals + no fabricated dates for
trigger_event_id rules), so the wire is unchanged when callers
don't set them.
- TS calc surface (frontend/src/client/views/verfahrensablauf-core.ts):
add the same two fields to CalcParams + forward in the fetch body;
surface rulesAwaitingAnchor on DeadlineResponse mirroring
Timeline.RulesAwaitingAnchor.
- Builder triplet (frontend/src/client/builder.ts hydrateTriplet):
apply filterByDetailMode(detailgrad) before renderColumnsBody, with
detailgrad sourced from the proceeding row. "selected" (default)
drops conditional + optional rules; "all_options" passes
includeOptional=true so the engine returns the optional rules the
user can opt into.
- Legacy /tools/verfahrensablauf (frontend/src/client/verfahrensablauf.ts):
pass includeOptional based on detailMode + a small hasOptionalOptIn
helper so per-rule rule:<uuid>=true deviations still surface their
optional rule even in "selected" mode (the engine has no rule:<uuid>
awareness; without the opt-in the user's pick would silently no-op).
Tests:
- frontend/src/client/views/verfahrensablauf-core.test.ts: pin the
fetch body shape - includeOptional=true and triggerEventAnchors={...}
round-trip through the request; empty/default values are omitted so
the wire stays minimal.
bun build + bun test (269 pass) + go vet + go test
./internal/handlers/... ./pkg/litigationplanner/... all clean.
(m/paliad#153)
PRD §2.3 + §10. Implements the dual-write rule (load-bearing
complexity per PRD §10): project-backed scenarios mirror flag
toggles to paliad.projects.scenario_flags and filed event states
to paliad.deadlines, while kontextfrei scenarios continue writing
only to paliad.scenario_events. Visible affordances: page-header
Akte picker, enabled "Aus Akte" mode tab, Akte banner on the
project-backed canvas, cross-surface scenario-flag-changed
dispatch + listener for live peer-surface coherence.
Backend
- ScenarioBuilderService takes ProjectService + ScenarioFlagsService
deps so dual-write hits live tables.
- CreateScenarioFromProject seeds a scenario from a project: copies
proceeding_type_id + scenario_flags, normalises our_side to the
builder's binary claimant|defendant axis, surfaces existing
rule-bound deadlines as scenario_events (filed when completed,
planned otherwise).
- PatchProceeding on a project-backed top-level triplet dual-writes
scenario_flags to projects.scenario_flags via flagDeltaFromBuilder.
- PatchEvent transitioning to state='filed' on a project-backed
scenario upserts paliad.deadlines (status='completed', completed_
at, source='rule') inside the same tx as the scenario_events
UPDATE — canvas and project surfaces never diverge mid-flight.
- POST /api/builder/scenarios/from-project handler wires the entry
point.
Frontend
- builder-akte.ts: project list fetch + dropdown render, Akte
banner, createScenarioFromProject POST helper.
- builder.ts: mode branching — picking an Akte (search hit or
page-header pick) creates a project-backed scenario and loads it;
loaded scenarios reflect their origin_project_id on the picker +
banner; flag toggles on Akte-backed top-level triplets dispatch
scenario-flag-changed so the Verfahrensablauf strip / project
surfaces refresh; the builder listens to inbound scenario-flag-
changed and refetches its scenario when the changed project
matches origin_project_id.
- procedures.tsx: enable the previously-disabled Aus Akte tab.
- i18n + CSS: builder.akte.banner.prefix key (DE+EN); lime-tinted
banner styling.
Tests
- TestScenarioBuilderAkteDualWrite (live DB) pins the dual-write
contract: Akte flag toggle → projects.scenario_flags updated,
Akte filed event → deadlines row inserted; kontextfrei flag
toggle leaves projects.scenario_flags untouched, kontextfrei
filed event leaves deadlines untouched.
- Existing TestScenarioBuilderService passes against the new
signature (nil deps short-circuit dual-write paths).
Verification: go test ./... + go vet ./... + bun run build all
clean. Playwright smoke against the static dist build confirms
the Akte tab + picker render correctly, fetchAkteProjects fires
on mount, and the scenario-flag-changed CustomEvent dispatches +
receives without runtime errors.
t-paliad-347
PRD §2.2 + §3.1: the page-header search box drives a typed dropdown
returning grouped event / scenario / project hits, and the "Ereignis"
entry mode is enabled. Picking an event creates a scratch scenario
with one triplet anchored on that event's proceeding type, with the
event card auto-anchored (lime band + "━━━━ DU BIST HIER ━━━━" divider
above the next-coming events).
Backend: new GET /api/builder/search reuses
DeadlineSearchService.SearchEvents for the events corpus (UPC v1),
filters owned scenarios by ILIKE on name, and reuses ProjectService.List
for the Akten group (team-RLS via visibilityPredicate). Each group is
capped independently (default 8 events / 5 scenarios / 5 projects, max
30). Missing services degrade gracefully — empty group, not 503.
Frontend: builder-search.ts owns the dropdown (debounced 180ms,
arrow-key navigation, Enter to pick, abort on next query). builder.ts
gains mode state ("cold" | "event" | "akte"), wires the mode bar +
search input, and runs applyAnchorHighlight after triplet hydration —
the helper finds the .fr-col-item with the picked rule_id, adds the
.builder-anchor-card lime band, and inserts a full-width
.builder-anchor-divider after the anchor's row in the columns grid
via JS row-index math (the grid is row-major with 3 header cells
+ 3-cells-per-row body).
Filter pill reset: setMode() clears the search input and closes the
dropdown when switching entry modes. Forum/proc/party/kind chips are
not yet rendered separately (they live in the search dropdown today);
the reset hook attaches there too when those land in a follow-up.
Verification:
- bun build (frontend bundles + i18n scan clean)
- go vet ./... + go test ./... (all packages pass)
- Playwright: mode switch focuses search, debounced fetch fires,
typed result groups render with N · M · K pluralization, event
pick creates scratch scenario + adds proceeding, anchor card
+ DU BIST HIER divider render in the columns grid (screenshots
confirmed visually)
The Litigation Builder's "+ Verfahren hinzufügen" silently failed in
prod after t-paliad-343 B2 shipped — clicking a Verfahren chip in the
picker did nothing, no visible error.
Root cause: the wire shape FristenrechnerType (the response of
/api/tools/proceeding-types) carried code+name+nameEN+group but not
id. Builder.ts mountAddProceedingPicker's callback POSTed
`{proceeding_type_id: meta.id}` to
/api/builder/scenarios/{id}/proceedings — meta.id was undefined,
JSON.stringify dropped the key, the server returned 400 ("invalid
input: proceeding_type_id is required"), and fetchJSON swallowed the
error to console. The user saw "nothing happens".
Fix:
- Add `ID int json:"id"` to lp.FristenrechnerType.
- SELECT id in FristenrechnerService.ListProceedings + Scan into the
new field.
- Defensive guard in builder.ts openAddProceedingPicker — refuse to
POST without a positive integer id and log a clear error, so a
future wire-shape regression cannot recreate the silent-fail.
- Regression test in pkg/litigationplanner/types_wire_test.go pins the
contract (id present in JSON, round-trips as integer).
Side-benefit: fristenrechner-wizard.ts:599-628 documented this exact
gap as a known limitation ("S5/follow-up can extend the wire shape to
include id"). That workaround can now be retired in a follow-up.
Refs m/paliad#153 (Litigation Builder)
GetScenarioDeep returned nil slices for proceedings/events/shares when
a scenario had zero rows, which Go's encoding/json serialises as `null`
rather than `[]`. The builder's renderCanvas then unconditionally calls
`state.active.proceedings.filter(...)` on a null and dies with
`procedures.js:101 TypeError: Cannot read properties of null
(reading 'filter')` — every cold-open scenario crashed the page before
the empty canvas could render.
Backend (root cause): initialise Proceedings / Events / Shares to empty
slices in BuilderScenarioDeep before SelectContext, so the wire shape
is always arrays. Existing rows still load via SelectContext, which
truncates the placeholder and refills from the DB.
Frontend (defence in depth): on loadScenario(), normalise each of the
three arrays to `[]` if the server response is not an array. Catches a
future regression (or an older deployed build) without re-introducing
the same crash class.
bun build clean, go vet + go test ./... green.
Two SELECTs still referenced paliad.deadlines.rule_id after mig 140
(Slice B.4) dropped that column in favour of sequencing_rule_id:
- internal/services/deadline_service.go:268 — DeadlineService.
ListVisibleForUser. Powers /api/events?type=deadline (dashboard
deadline rail, /deadlines page, every status bucket). Threw
`pq: column f.rule_id does not exist` on every request → 500
for any authenticated user hitting the dashboard.
- internal/services/projection_service.go:1250 — collectActualsForOverrides.
Same column on `paliad.deadlines d`. Logged once per projection
pass (`ERROR service: projection: deadlines: ...`); aliased the
rename to `rule_id` so the receiving struct tag still scans.
Live container logs confirmed the failure mode — a 60-row burst of
`pq: column f.rule_id does not exist at position 3:36 (42703)` starting
the minute the post-B0 container came up (mig 140 had applied to the
DB but the SELECT still used the dropped name). EXPLAIN against the
live schema after the edit plans cleanly; the LEFT JOIN to
paliad.deadline_rules_unified on sequencing_rule_id was already correct
(only the SELECT projection was stale).
Root cause: mig 140 commit (1129bab) renamed the JOIN to
`f.sequencing_rule_id` but left the SELECT clause on the older name.
The model tag is already `db:"sequencing_rule_id" json:"rule_id"`, so
the wire shape is unchanged — only the column reference flips.
bun build clean, go vet ./... clean, go test ./... green.
Builds on B1 (commit 6c1d8cc). After this slice a user can compose a
multi-proceeding scenario kontextfrei: stack proceedings, flip
perspective per-triplet, toggle scenario flags, auto-spawn child
proceedings on flag transitions, and mark individual event cards as
planned / filed / skipped — all auto-saved to paliad.scenario_*.
PRD §7.1 B2 acceptance shipped:
- Multi-triplet stack: top-level proceedings sorted by ordinal,
child proceedings nested inline with a left lime border.
- Per-triplet controls bar: perspective radio (none / claimant /
defendant), Detailgrad pill (selected / all options), Entfernen
action. Each control PATCHes the proceeding row and re-renders the
affected triplet.
- Per-triplet flag strip: every paliad.scenario_flag_catalog row
rendered as a checkbox, bound to scenario_proceedings.scenario_flags.
Active flags also surface as chips in the triplet header for quick
legibility.
- Spawn nesting: when `with_ccr` flips ON on upc.inf.cfi the builder
auto-POSTs an upc.ccr.cfi child proceeding linked via
parent_scenario_proceeding_id; flip OFF deletes the child (events
cascade via the schema). The SPAWN_MAP table is data-driven so
future spawn flags slot in.
- 3-state event cards (planned / filed / skipped):
overlayEventStates walks the rendered .fr-col-item nodes (the
data-rule-id hook added to verfahrensablauf-core in this slice)
and stamps each card with data-builder-state + per-state action
buttons (File / Skip / Reset to planned). Filed cards prompt for
a date; skipped cards prompt for an optional reason. POSTs or
PATCHes paliad.scenario_events keyed by sequencing_rule_id.
- Per-card optional horizon chip: stores horizon_optional on the
scenario_event row, increment / decrement chip on every card.
The full surface awaits a calc-engine "optionals available"
counter (PRD §3.4 follow-up); the persistence layer + UX hook are
in place so the wiring lands without another schema touch.
- Page-header Stichtag drives default dates for every triplet (the
triplet's per-stichtag override path is wired but the per-triplet
Stichtag input is a B3+ affordance).
verfahrensablauf-core.renderColumnsBody now stamps data-rule-id (and
data-submission-code as a future hook) on every .fr-col-item root —
non-breaking enhancement; the legacy /tools/* pages don't read either
attribute. Verified by re-running the existing 57-test suite.
Backend: one new read-only endpoint
GET /api/builder/scenario-flag-catalog passes through
ScenarioFlagsService.ListCatalog so the builder doesn't need a
per-project round-trip to render flag toggles.
bun run build clean (3050 i18n keys), go vet ./... clean, go test ./...
clean, frontend bun test (verfahrensablauf-core suite) 57 / 57 pass.
Replaces cronus's U0-U4 catalog at /tools/procedures with a
persistence-backed builder shell on top of B0's API surface
(/api/builder/scenarios/*, t-paliad-340).
PRD §7.1 B1 acceptance shipped:
- Page header: scenario picker, name action, Akte picker stub,
Stichtag input, search input, save status indicator.
- Entry-mode radio (cold-open active; event-triggered + akte
rendered disabled for B3/B4 layout stability).
- Empty canvas with "Neues Szenario starten" CTA and a 5-most-recent
list rendered when the user has saved scenarios.
- Side panel "Meine Szenarien" with the Aktiv bucket; clicking an
item loads the scenario into the canvas.
- Add-proceeding inline picker (Forum chip row → Verfahren chip row
→ Hinzufügen). UPC v1; other forums chipped but disabled.
- First proceeding triplet renders end-to-end via
verfahrensablauf-core.calculateDeadlines + renderColumnsBody (the
existing 3-column proaktiv|court|reaktiv body, read-only in B1).
- Auto-save with 500ms debounce on name + stichtag patches; save
status flips idle → saving → saved/error in the page header.
New client modules under frontend/src/client/:
- builder.ts — orchestrator (URL state, fetch, auto-save loop,
canvas render, scenario-list re-paint).
- builder-picker.ts — inline Forum/Verfahren popover for the
add-proceeding affordance.
- builder-triplet.ts — single-triplet header + body wrapper.
procedures.tsx rewritten as the shell scaffolding (sidebar, page
header, mode radio, two-column body); procedures.ts now boots the
builder instead of toggling the 4-tab catalog.
Legacy U0-U4 modules (verfahrensablauf.ts, verfahrensablauf-state.ts,
VerfahrensablaufBody.tsx, procedures' tab toggle in client/procedures.ts,
fristenrechner-* mounts) are no longer reachable from /tools/procedures
but kept in the tree for the B6 cleanup sweep per PRD §7.4.
i18n.ts grew 60 keys × 2 langs under builder.*. global.css grew a
self-contained .builder-* block at the file tail.
bun run build, go vet ./..., and go test ./... all green.
Two paired engine semantics fixes:
1. trigger_event_id is now the authoritative semantic anchor. When a
rule carries trigger_event_id, the engine no longer falls back to
the proceeding's trigger date — it resolves the anchor via
CalcOptions.TriggerEventAnchors keyed by paliad.trigger_events.code.
Missing anchor renders the rule as IsConditional (empty date) and
propagates via courtSet so descendants also surface as conditional.
Fixes the RoP.109.5 bug where the engine fabricated a date 2 weeks
before the user's SoC instead of waiting for the oral_hearing date.
2. priority='optional' rules are suppressed from the default
Calculate output. Callers (paliad /tools/procedures,
youpc.org/deadlines) opt in via CalcOptions.IncludeOptional=true to
restore the legacy "show optional applications" behaviour. The
suppression cascades through skippedIDs so child rules drop too.
Wire shape additions:
- CalcOptions.IncludeOptional bool
- CalcOptions.TriggerEventAnchors map[string]string
- Timeline.RulesAwaitingAnchor int (count of suppressed-by-missing-
anchor rules, for caller telemetry / "N rules need an anchor" UX)
Existing before-court-set-anchor tests opt in to IncludeOptional=true
to preserve their non-optional-related test intent.
Refs: youpcorg/head delegations #2568 + #2570, m/paliad#153 (Litigation
Builder PRD path).
The Markdown inline scanner (parseInlineSpans) treats _ and * as
italic delimiters. A placeholder like {{project.case_number}} fed
through the scanner had its underscores consumed as italic markers,
leaving {{project.casenumber}} in the composed OOXML. The v1
placeholder pass then looked up the wrong key, surfacing
[KEIN WERT: project.casenumber] in the preview. The form ↔ preview
highlighting also stopped working because data-var attributes
mismatched between the input (snake_case) and the rendered span
(stripped).
parseInlineSpans now detects {{ at the cursor and skips ahead to
the matching }}, copying the entire placeholder verbatim into the
current text run. Unmatched {{ falls through to the existing
character handling so legal prose with stray braces still renders.
Tests: regression test for underscored keys (single + multiple +
mixed-with-italics), direct guard on parseInlineSpans, and an
italic-around-placeholder structural test.
t-paliad-340 — B0 of edison's 7-slice train (PRD §7.1). DB-only: schema
+ RLS land, dev-only test route exercises the surface, no user-facing
change. B1 wires the actual builder UI on top.
Migration 157 (additive on the legacy mig-145 scenarios table — 0 rows
in prod, safe to relax):
- paliad.scenarios gets owner_id / status / origin_project_id /
promoted_project_id / stichtag / notes. spec drops NOT NULL and the
scenarios_unique_per_scope constraint drops (the builder allows
multiple scratch + Unbenanntes Szenario rows per user).
- New tables: scenario_proceedings, scenario_events, scenario_shares.
- paliad.projects.origin_scenario_id for the promote-to-project audit
trail (the FK lands now; the wizard ships in B5).
- paliad.can_see_scenario(uuid) STABLE SECURITY DEFINER helper covering
owner / share / global_admin / two legacy paths.
- Replacement RLS on scenarios + RLS on the three new tables; legacy
service + handlers stay live and unchanged.
PRD §5.1 deviations called out in the migration header:
- proceeding_type_id is integer (live schema), not uuid (PRD draft).
- FK target is paliad.users, matching the rest of paliad's schema.
Go surface:
- ScenarioBuilderService — list/create/get-deep/patch scenarios,
add/patch/delete proceedings, add/patch/delete events,
add/delete shares. Writes wrap in transactions with set_config(
paliad.audit_reason, ..., true) per event_choice_service.go pattern.
- /api/builder/scenarios/* — handlers register under a builder/
prefix so the legacy /api/scenarios surface still works.
- /dev/scenario-builder — single-page HTML form gated to
PaliadinOwnerEmail, exercises the B0 surface without Postman.
- Live-DB integration test (TEST_DATABASE_URL gated) covers
create + list + deep-get + share + visibility negatives + patch.
Audit-first: every DDL block ran clean via BEGIN/ROLLBACK against
the live DB before commit; end-to-end sanity (insert chain + CHECK
constraints + CASCADE-on-delete) verified via the Supabase MCP.
bun build clean. go vet + go test -short ./... green.
PRD for the columnar litigation planner replacing today's 4-tab catalog
at /tools/procedures with a Litigation Builder backed by a new Scenario
DB. Captures 20 chip-picker decisions (5 batches via AskUserQuestion)
covering: unified-builder shape with 3 entry modes (cold-open /
event-triggered / Akte), separate paliad.scenarios table with
multi-proceeding constellations, auto-save + named-list, per-proceeding
flags + perspective + Detailgrad, 3-state event cards
(planned/filed/skipped), per-event-card optional horizon, vertical
stacked column-triplets with inline spawn children, universal search
(events + scenarios + Akten), 3-step promote-to-project wizard,
read-only team sharing, desktop v1 + mobile basic-read.
Includes data model deltas (4 new tables + 1 column on
paliad.projects), 6-slice migration plan from the current live U0-U4
catalog, and coder hand-off notes. Cross-proceeding peer triggers and
DE/EPA/DPMA full expansion deferred to v1.1.
Selected .tracker-pill.is-active used --color-accent-fg, which in dark
mode resolves to lime → lime text on lime background, unreadable.
Switch to --color-accent-dark (midnight in both modes) so the selected
pill has midnight text on lime in both light + dark. Same pattern as
the older .filter-pill.active rule.
The workflow tracker (T1-T4) replaces every consumer of the entry-mode
modules. Verified via grep that no non-deleted file imports the
following before removal:
Deleted (10 files):
- client/fristenrechner-mode-a.ts (Mode A search panel)
- client/fristenrechner-wizard.ts (Mode B guided wizard)
- client/fristenrechner-wizard.test.ts
- client/fristenrechner-result.ts (post-commit result-view)
- client/fristenrechner-result.test.ts
- client/verfahrensablauf.ts (Verfahrensablauf panel client)
- client/views/event-card-choices.ts (per-card choice popover —
only verfahrensablauf.ts consumed it)
- client/views/verfahrensablauf-state.ts (URL + storage helpers —
only verfahrensablauf.ts consumed it)
- client/views/verfahrensablauf-state.test.ts
- components/VerfahrensablaufBody.tsx (the 4-tab proceeding picker
body — no consumer after T1)
Kept (still load-bearing):
- client/views/verfahrensablauf-core.ts — procedures-tracker uses
calculateDeadlines + CalculatedDeadline + escHtml + formatDate.
- client/views/verfahrensablauf-core.test.ts
- client/verfahrensablauf-detail-mode.ts — procedures-tracker uses
filterByDetailMode under the per-proceeding "Alle Optionen"
toggle (T4).
- client/verfahrensablauf-detail-mode.test.ts
The .css classes (.fristen-wizard-*, .verfahrensablauf-*) still live
in global.css; they're cheap orphans (no selector match in the new
DOM) and a CSS housekeeping pass is outside this train's scope. The
i18n keys (deadlines.flag.*, deadlines.detail.*, deadlines.view.*,
deadlines.side.*) likewise stay — some are used dynamically via tDyn
on the tracker, others remain candidates for a future i18n sweep.
Frontend tests: 217 pass (264 → 217, the deltas are the 3 deleted
test files: fristenrechner-result, fristenrechner-wizard,
verfahrensablauf-state). Build + go vet clean.
t-paliad-338
The final tracker layer per design §3.4 / §3.6 / §11 polish list:
- Per-proceeding "· Gewählt · / Alle Optionen" toggle (§3.4) lives in
the card header next to the show/hide button. State persists in
localStorage per proceeding code, so a page with multiple cards
can keep one expanded without affecting siblings. Toggle drives the
detail mode for filterByDetailMode + sets includeHidden=true on the
calc, so previously-skipped conditional rules re-surface muted.
- Appeal-target chip group (§3.2 #3) renders below the header on
proceedings with applies_to_target rules — today only upc.apl.unified.
Endentscheidung / Kostenentscheidung / Anordnung / Schadensbemessung
/ Bucheinsicht. Picking a target re-fetches the calc with the
appealTarget param so the timeline narrows to the matching subset.
- Cross-party muted treatment (§3.6) — when the find-header Partei
pill is set, rows whose primary_party is the opposite side render
with a "Gegen." badge and a muted style. Court / both / informational
rows are never cross-party.
- "Unselected" + "hidden" styling — under "Alle Optionen" the rules
that filterByDetailMode stamps __detailUnselected on render dotted
italic, and previously-skipped (isHidden) rules render at reduced
opacity. Honest preview of what the user is NOT considering.
- Cross-surface scenario-flag-changed listener — the tracker now reseeds
its flags state when Mode B / Verfahrensablauf / Verlauf patches the
same project's flags, so toggling there flows through here without a
refresh.
Out of T4 (court-set choices_offered chip groups and the court-set date
override from appointments) — those need a follow-up backend pass to
surface the choicesOffered payload on TimelineEntry through the calc
response in a usable shape. The data field exists on CalculatedDeadline
but isn't yet wired to a paint route on the tracker.
t-paliad-338
Wires the workflow tracker to projects via ?project=<uuid>, per design
§6.4 + §11.Q5:
- loadAkte fetches /api/projects/{id}, /api/projects/{id}/timeline
and /api/projects/{id}/scenario-flags in parallel:
1. Project title + proceeding_type — pre-seeds the Verfahren pill.
2. Timeline events → ActualsMap keyed by deadline_rule_id with
status (done / overdue / open / court_set), due / completed
date, and deadline / appointment ids.
3. scenario_flags → seeds state.flags so the gating-flag checkboxes
render in the persisted state. Per-rule rule:<uuid> flags stay
out of the calc payload (they drive priority deviations via
isRuleSelected, handled by the existing detail-mode filter).
- Auto-pin: the first render with no explicit ?event= pins the most
recent status='done' deadline. URL pin (shared link) is preserved.
- Per-node overlay: each node carries the actuals badge — ✓ (done +
strike-through), ⚠ (overdue + red wash), 📅 (open ≠ projected), ◇
(open ≡ projected). Date column shows the actual date.
- Fork write-back: PATCH /api/projects/{id}/scenario-flags fires on
every flag toggle so Mode B / Verlauf / dashboard re-render with the
same scenario on next visit. Fire-and-forget; UI doesn't wait.
- Find-header summary chips: "Akte: <title>" alongside "Anker: <name>"
+ "{n} Verfahren".
Out of T3 (deferred):
- ?project= picker UI (today's user navigates here from /projects/{id}
via deep-link).
- Per-rule rule:<uuid> flag write-back (priority deviations) — the
detail-mode filter doesn't take an interactive toggle yet.
- Cross-surface scenario-flag-changed CustomEvent listener — patching
fires the event, the tracker just doesn't yet re-render on incoming
ones (T4 polish).
t-paliad-338
Layers the anchor / focus interactivity on top of T1's shell per
design §6.1–§6.5:
- Click-to-pin (📌) on every node with a real rule_id sets the anchor.
Clicking the already-anchored pin un-pins. URL state ?event=<id>.
- Anchored node renders with a "── DU BIST HIER ──" divider beneath
its meta line + the lime left-band styling. The find-header summary
surfaces "Anker: <name>" so the user can confirm where they are.
- Fokus chip (🔍) on the anchored node toggles zoom (?zoom=1). Zoom
renders the anchor's parent chain as a breadcrumb at the top of the
proceeding card and renders only the anchored subtree below. A
"{n} weitere Schritte verborgen" footer reports what zoom hid.
- Multi-proceeding scope (§6.5): when an anchor is pinned and >1
proceeding is visible, non-anchored proceedings auto-collapse to a
one-line header card with a [zeigen] / [ausblenden] toggle. The
user's explicit expansions persist for the current anchor; pinning
a different node clears them.
- Auto-pinning from the search input (T1's single-hit behaviour) now
routes through onAnchorChanged so the multi-proc scope kicks in
consistently.
Anchor + zoom state writes through history.replaceState — sharable URL.
Un-pinning clears zoom and restores the full multi-proceeding view
automatically (lastAnchor tracking).
t-paliad-338
Direct-replace per m's Q7 divergent pick in atlas's design
(docs/design-procedures-workflow-tracker-2026-05-27.md §9): /tools/procedures
drops the 4-tab catalog (U0-U4 shipped this morning) for the single
canonical workflow-tracker shape.
T1 ships:
- Sticky find header — search input, forum / Verfahren / Partei pill
rows, global Stichtag, live result summary.
- Per-proceeding timeline cards — one card per matched proceeding,
rendered as a chained tree by parent_id with priority-styled bullets
(mandatory solid, recommended muted, optional dotted, informational
faded, court-set blue). Party badge per node.
- Cold-open default: the 6 curated proceedings from design §8 / §11.Q4
(upc.inf.cfi, upc.rev.cfi, upc.apl.unified, de.inf.lg, epa.opp.opd,
dpma.opp.dpma) render stacked with a hint above.
- Scenario-flag forks — per-proceeding "Optionen" strip on each card's
header surfaces the applicable flags (with_ccr, with_amend, with_cci)
derived from condition_expr or a fallback map. Tick re-runs the calc.
- URL state: ?q, ?forum, ?procs, ?party, ?trigger_date, ?event, ?flags.
?event= scroll-highlights the matching node (no zoom yet — T2 layers).
- Legacy ?mode= dropped silently on first state write so bookmarks
self-clean. /tools/fristenrechner + /tools/verfahrensablauf 301s
still resolve here.
Floor T1 honours: every catalog workflow it replaces — pick proceeding
(forum + Verfahren pills), search event (search input → auto-narrow +
?event= anchor), wizard narrowing (pills compose), Akte entry
(?project= read-only for T1; full overlay in T3).
Per-node fork placement (the design's stated final shape — checkbox on
the gating node itself, not a card-level strip) is a T2 refinement;
T1 keeps forks scoped per proceeding so they're not the global-page
strip m's bug #5 flagged.
Aux-proceedings inline-expandable (design §5) and the appeal-target
chip group are scoped to T4; the calculator currently doesn't surface
isSpawn / spawnProceedingCode through TimelineEntry to support them.
t-paliad-338
atlas shipped the workflow-tracker design after m's 21:01 grilling-round reframe (single timeline-with-forks, find=search+pills+result-timelines, aux inline, zoom from within full tree). 510-line doc, 2 rewrite iterations.
7 Qs answered in 2 batches (4+3). 5 on-recommendation, 2 divergent:
- Q3 (divergent): multi-proceeding anchor scope — auto-collapse other proceedings to header-only (new §6.5)
- Q7 (divergent): migration strategy — direct replace at T1, no feature flag (§9)
4-slice + cleanup train. T1 ships minimum-viable tracker visibly at /tools/procedures, replacing the catalog UI knuth shipped today.
Inventor parks. Head dispatches Sonnet coder (NOT atlas per project memory directive).
5 picks on-recommendation, 2 diverged:
Q3 (multi-proceeding anchor scope): m picked 'other timelines auto-collapse to header-only' over the recommended 'stay expanded'. Added §6.5 with the header-card render rule.
Q7 (migration cadence): m picked 'direct replace at T1, no flag' over the recommended flag-gated dev. §9 rewritten end-to-end: T1 ships the minimum-viable tracker visibly to users, replacing the catalog UI in the same PR. T2-T4 layer zoom + Akte + polish. T5 is cleanup-only.
The 5 on-rec picks: inline checkbox forks (Q1), sibling-collapse zoom (Q2), 6 curated defaults on cold open (Q4), latest-done-deadline Akte anchor (Q5), global Stichtag (Q6) — all locked as drafted in §1-§8.
Ready for review. Coder gate held; head decides T1 hire.
m's reframe (2026-05-27 20:43): /tools/procedures should be a workflow
tracker, not a catalog browser. Pick any procedural event, see backward
(predecessors) + self (where I am) + forward (successors), with
scenario_flags as togglable predicates and alternative constellations
explorable.
This shift-1 doc covers:
- 4-tab UX redo (single-pane radio-revealed entry form to fix the
pre-form-leak bug)
- Anchor visualisation (vertical waterfall with anchor at centre line)
- Three views — Anchor / Verfahren / Konstellationen — toggle preserves
anchor + scenario state
- Forward walk (current constellation only by default, conditional
reveal toggle, view-mode toggle reused from atlas P3)
- Backward walk (3 hops default, Akte mode overlays paliad.deadlines
actuals onto template chain)
- Compound rules drawer (per-anchor Querverweise affordance — column
shape owned by curie editorial workstream)
- Constellation viewer (inline per-flag preview drawer + full
Constellation view for browse)
- Akte entry (anchor derives from latest completed deadline)
- Migration: T1-T5 flag-gated dev under ?tracker=1, then hard-cut
Coder gate held. 11 open questions for m staged for AskUserQuestion in
4+4+3 batches. Decisions append as §13 before the
TRACKER DESIGN READY FOR REVIEW signal.
Per m's Q11 divergence in the design (no 2-week dual-ship), this slice
flips /tools/fristenrechner and /tools/verfahrensablauf to permanent 301
redirects to /tools/procedures and deletes the legacy frontend pages.
Bookmarks resolve via Location preservation of query params; no
?legacy=1 escape, no in-product affordance pointed back at the retired
URLs after the merge.
Server:
- handleFristenrechnerPage + handleVerfahrensablaufPage now 301 to
/tools/procedures, carrying any query string through unchanged.
- pillDrillURL in deadline_search_service.go retargets to
/tools/procedures so freshly indexed search pills land on the new
page directly (cached snapshots still work via the 301).
Frontend:
- Deleted src/fristenrechner.tsx, src/verfahrensablauf.tsx,
src/client/fristenrechner.ts.
- src/client/verfahrensablauf.ts loses its DOMContentLoaded auto-boot
and the now-unused initI18n / initSidebar imports; procedures.ts is
the sole caller of initVerfahrensablauf().
- frontend/build.ts drops the legacy entrypoints and renderXxx HTML
outputs.
- Sidebar.tsx, Header.tsx, index.tsx, paliadin-context.ts repointed
to /tools/procedures.
- Unused nav.fristenrechner / nav.verfahrensablauf /
tools.verfahrensablauf.* i18n keys removed.
Tests:
- verfahrensablauf_test.go rewritten to assert both legacy URLs return
301 with the correct Location (query string preserved).
Mounts the full Verfahrensablauf wizard — proceeding picker, perspective
chooser, date inputs, scenario flag rows, detail-mode toggle, view
toggle, timeline-container — under the /tools/procedures "Verfahren
wählen" tab. Per-rule scenario_flags chips (P0 SSoT) and the
Aufnehmen/Entfernen affordances reach the unified page unchanged since
they're delegated handlers on the timeline-container.
Refactor steps:
- Extracted the wizard body markup into a shared TSX component
(components/VerfahrensablaufBody) used by both verfahrensablauf.tsx
(legacy) and procedures.tsx (unified). U4 will retire the legacy
page; the shared component lets U3 ship without code duplication.
- Lifted the verfahrensablauf.ts DOMContentLoaded body into
initVerfahrensablauf() and re-exported it. The legacy auto-boot
stays in place but skips itself when #procedures-panel-proceeding
is present, so the unified page imports the module without
double-init. procedures.ts calls initVerfahrensablauf() the first
time the proceeding tab activates, gated by a one-shot flag to
preserve module-local selectedType / lastResponse across tab
toggles.
m flagged 2026-05-27 20:26: archived rules (e.g. the 5 mig 152 Mängelbeseitigung clones) clutter the /admin/procedural-events default view. They were correctly archived by mig 152 but visually noisy alongside active rules.
Fix: default activeLifecycle = 'published'. The 'Alle' chip still exists for when the user wants to see drafts + archived; 'Archived' chip surfaces them on demand. Initial view shows only the active corpus.
Mounts mountWizard() into #procedures-panel-wizard when the Geführt tab
activates. Same 5-row wizard, same backend (event search + follow-ups
probe) as the legacy /tools/fristenrechner. On R4 launchResult, the
wizard hands off to mountResultView which renders into the same
overhaul-root inside the panel.
The wizard renders into #fristen-overhaul-mode-host while Mode A and
the result view write into #fristen-overhaul-root. To keep those IDs
unique in the DOM — both modes look up via document.getElementById —
the host scaffold is no longer static on the search panel. The new
installOverhaulHost() helper tears down any existing host and installs
a fresh one inside the active tab's panel before each mount, so two
parallel hosts can't cross-wire when the user toggles between the
Direkt-suchen and Geführt tabs.
The U1/U2 placeholders are dropped from the panel markup since the
panels are populated dynamically now.