3c840c0366a6c3b5098bba4f6e71ff058aff51a7
506 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| ed3c5d1f32 |
Revert "Merge: t-paliad-338 T1 — workflow-tracker shell replaces catalog (m/paliad#152)"
This reverts commit |
|||
| be570c2fd0 |
Revert "Merge: t-paliad-338 T3-T5 — workflow-tracker Akte/polish/cleanup (m/paliad#152)"
This reverts commit |
|||
| 58692513a8 |
Revert "Merge: tracker filter pill dark-mode contrast (m/paliad#152)"
This reverts commit |
|||
| 93c664c865 |
fix(procedures): tracker filter pill — dark-mode contrast (m/paliad#152)
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. |
|||
| 73f49c46ed |
chore(procedures): T5 — drop dead code from the U0-U4 catalog (m/paliad#152)
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
|
|||
| c80723fc85 |
feat(procedures): T4 — appeal-target + Alle Optionen + cross-party + polish (m/paliad#152)
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 |
|||
| 1ed75c56e3 |
feat(procedures): T3 — Akte landing + actuals overlay (m/paliad#152)
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
|
|||
| 7945bfb364 |
feat(procedures): T2 — anchor pin + zoom + multi-proceeding scope (m/paliad#152)
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 |
|||
| bfb38aab41 |
feat(procedures): T1 — workflow-tracker shell replaces catalog (m/paliad#152)
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 |
|||
| 1718ea2eae |
Merge: t-paliad-335 — unified /tools/procedures shipped U0-U4 (m/paliad#151)
knuth shipped all 5 slices in one shift, per cronus's design: U0 |
|||
| 39c8ef343b |
feat(procedures): U4 hard-cut legacy URLs + retire dual surfaces (m/paliad#151)
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). |
|||
| 48a07ef4ef |
feat(procedures): U3 fold Verfahrensablauf tree + 3-way detail filter (m/paliad#151)
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. |
|||
| c8390dd02a |
fix(admin-rules-list): default lifecycle filter to 'published' (hide archived clutter)
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. |
|||
| c8261da492 |
feat(procedures): U2 fold Mode B (Geführt wizard) (m/paliad#151)
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. |
|||
| 0568d340a7 |
feat(procedures): U1 fold Mode A (Direkt suchen) (m/paliad#151)
Mounts mountModeA() into #procedures-panel-search when the Direkt-suchen tab activates. The legacy fristenrechner-mode-a code runs unchanged inside a wrapper that reseeds the #fristen-overhaul-root / #fristen-overhaul-mode-host scaffold on every tab activation, so re-clicking the tab always restores a fresh Mode A surface even if the previous interaction committed an event into the result view. `?event=<code>` deep links still resolve: boot detects the param, activates the search tab, and hands directly to mountResultView() — the result lands inside the same root, the user sees the picked event's follow-up rules with the Direkt-suchen tab as the visible context. Search-box-in-filter-strip composition with chip filters (m's Q3 divergence) lands later, after Mode B + Verfahrensablauf are folded — the unified state machine pulls all three behind one search input. |
|||
| 60907e7153 |
feat(procedures): U0 skeleton — /tools/procedures page shell (m/paliad#151)
First slice of the unified procedural-events tool train. Ships only the page chrome — route, sidebar/header, filter strip with search box, four entry-mode tabs (Verfahren wählen / Direkt suchen / Geführt / Aus Akte), and the host containers later slices mount their UI into. No data wiring. Per m's decisions (design §11.5): URL is English (/tools/procedures, not /tools/verfahren); all four tabs visible from boot (not a single-default landing); search box lives in the top filter strip and will compose with chip filters once U1+ wire them. U1 fills #procedures-panel-search (Mode A), U2 fills -wizard (Mode B), U3 fills -proceeding + #procedures-output-tree (Verfahrensablauf), U4 hard-cuts /tools/fristenrechner and /tools/verfahrensablauf to 301 redirects and drops the legacy pages. |
|||
| c8999e2a8b |
fix(admin-rules-edit): accept /admin/procedural-events/{id}/edit in URL parser
Slice B.6 / S6 renamed the canonical edit URL from /admin/rules/{id}/edit
to /admin/procedural-events/{id}/edit. The backend handler + 301 redirect
landed, but the client-side regex in admin-rules-edit.ts:110 was missed —
it still only matches the legacy /admin/rules/.../edit shape. Result:
visiting the canonical URL from the list page shows 'Ungültige
Verfahrensschritt-ID in der URL.' even though the rule exists.
Fix: regex accepts both '/admin/procedural-events/{id}/edit' (canonical)
and '/admin/rules/{id}/edit' (legacy, kept for stale tabs / bookmarks
during the deprecation window).
m flagged 2026-05-27 17:57 on rule cc439590 (RoP.262.2, upc.inf.cfi).
|
|||
| f6add95d0a |
Merge: t-paliad-331 P3 — Verfahrensablauf three-way detail filter (m/paliad#149)
ritchie shipped m's headline UX (paliadin priority signal 14:58): 'The new timeline filters for optional / mandatory / show only selected is what I am most waiting for. I want this to be consolidated for all our deadlines so we can simulate all proceedings.' Three-way detail-level filter above the Verfahrensablauf result panel: - Nur Pflicht — only priority='mandatory' rules - Gewählt (default) — mandatory + recommended + every explicit per-rule override in projects.scenario_flags - Alle Optionen — every rule, unselected ones rendered dotted-border + muted State persists per-user via localStorage['verfahrensablauf:view_mode']. Per-rule Aufnehmen/Entfernen chips wire to projects.scenario_flags via the P0 SSoT (rule:<uuid> entries). New files: verfahrensablauf-detail-mode.ts (125), verfahrensablauf-detail-mode.test.ts (96), filter wiring in verfahrensablauf.ts (+204) and views/verfahrensablauf-core.ts (+37). 63 LoC CSS (dotted-border treatment). bun build clean, 264 frontend tests pass (8 new), go vet clean. Ritchie continuing with P2 (condition_expr write-validator) then P4 (legacy deprecation). |
|||
| 480332a5f5 |
feat(deadline-system): P3 — three-way detail filter on Verfahrensablauf (m/paliad#149)
m's headline UX ask (2026-05-27 14:58, paliadin priority signal):
"The new timeline filters for optional / mandatory / show only
selected is what I am most waiting for. I want this to be
consolidated for all our deadlines so we can simulate all
proceedings."
Phase 2 P3. Adds a three-way detail-level filter above the result
panel on /tools/verfahrensablauf:
( ) Nur Pflicht — only priority='mandatory' rules
(•) Gewählt — mandatory + recommended (default) + every
explicit per-rule override the user has set
in projects.scenario_flags
( ) Alle Optionen — every rule, with unselected ones rendered
dotted-border + muted so the user sees what
they're NOT considering
State persists per-user via localStorage["verfahrensablauf:view_mode"].
The filter is pure client-side narrowing on the calc payload — flipping
the toggle re-renders instantly without a fresh backend call.
Per-rule selection (design §2.4a): every optional / recommended card
now carries an [Aufnehmen] / [Entfernen] chip. Clicking writes a
"rule:<uuid>" entry into the project's scenario_flags via the P0 SSoT
PATCH endpoint, recording only deviations from the priority default:
recommended + entfernen → rule:<uuid> = false (explicit deselect)
optional + aufnehmen → rule:<uuid> = true (explicit select)
flipping back to the default deletes the entry
Mandatory rules never expose the chip — they cannot be deselected.
Wire-shape change: CalculatedDeadline gains `ruleId` (the backend already
emits it as `ruleId` in TimelineEntry; only the frontend interface needed
to surface it).
Conditional handling: a conditional rule whose predicate doesn't fire
is treated as unselected in "Gewählt" mode (even when priority=
mandatory) — mandatory means "must be filed IF the predicate fires",
not "always render". The "Alle Optionen" view re-surfaces it so the
lawyer can see what scenario would unlock it.
Cross-surface coherence: hydrating ?project=<id> reads scenario_flags
from the SSoT and pre-fills the existing flag checkboxes (with_ccr /
with_amend / with_cci) so the page reflects the project's persisted
state on first paint. Every flag toggle + every per-rule chip click
PATCHes back. The page also listens for the scenario-flag-changed
CustomEvent fired by peer surfaces (Mode B Fristenrechner result-view)
and re-renders without a fresh fetch.
i18n: 5 new keys (deadlines.detail.{label,mandatory_only,selected,
all_options,optional_unselected_hint,aufnehmen,entfernen}) DE + EN.
CSS: dotted-border + muted treatment on .timeline-item-header--
unselected; .timeline-selection-chip with --add (lime accent) and
--remove (discreet muted) variants.
Tests: 8 new unit tests covering isRuleSelected (4 priority × 2 flag
state matrix) and filterByDetailMode (3 modes × default/override cases).
Verified: bun build clean, bun test 264/264 (8 new), go vet clean.
Design: docs/design-deadline-system-revision-2026-05-27.md §2.4a
(selection state model), §3.3a (view-mode toggle), §6 (Entry A spec).
t-paliad-331. Re-prioritised by m via paliadin 14:58.
|
|||
| 97d90ce651 |
Merge: t-paliad-331 — Phase 2 slices P0 + S1+S1a + P1 (m/paliad#149)
ritchie shipped three slices of the Phase 2 deadline-system revision train per design §5:
P0 — Scenario SSoT (mig 154):
- ALTER TABLE projects ADD COLUMN scenario_flags jsonb DEFAULT '{}'
- New paliad.scenario_flag_catalog table
- GET/PATCH /api/projects/{id}/scenario-flags endpoints
- Verfahrensablauf + result-view checkbox binding read+write through scenario_flags
- Per-rule selection state via 'rule:<uuid>' entries (generalises the with_ccr dropdown pattern, no new column on sequencing_rules)
- New scenario_flags_service.go (375 LoC), scenario-flags.ts client, i18n keys
S1+S1a — Cross-party display + spawn-only picker filter:
- FristenrechnerService.LookupFollowUps stops filtering by party server-side; returns all + primary_party
- UI groups: own-side checked-by-priority, cross-party annotated 'Gegenseitig' badge + unchecked
- SearchEvents SQL adds AND sr.is_spawn = false to filter spawn-only events as triggers
- lookup_events_test.go regression coverage
P1 — upc.apl re-split (m's Q5 divergence, mig 155):
- Reverts upc.apl.unified (id=160) back into upc.apl.merits / upc.apl.cost / upc.apl.order split
- Retargets 16 sequencing_rules to the appropriate split id
- Mig 155 applied to live DB per ritchie's report
Next per m's 14:58 priority signal: P3 (Entry A Verfahrensablauf tree UI with three-way view-mode toggle) — ritchie jumping straight there, then P2 + P4.
|
|||
| 3533d79a25 |
feat(deadline-system): S1+S1a — cross-party display + spawn-only picker (m/paliad#149)
Phase 2 S1 + S1a (pre-ratified from t-paliad-327, folded into the
Phase 2 train).
S1 — Cross-party display:
- FristenrechnerService.LookupFollowUps stops filtering by party
server-side; queryFollowUpRows drops the perspective WHERE clause
and returns every published+active child.
- Server now computes is_cross_party per row (true only when
perspective ∈ {claimant,defendant} AND primary_party is the
opposite side; NULL/both/court is never cross-party).
- FollowUpRule wire shape gains the boolean.
- Frontend renderRule adds a "Gegenseitig" badge + is-cross-party
row class (muted styling, disabled checkbox affordance).
- defaultChecked returns false for cross-party rows.
- countSelected + submitWriteBack skip cross-party rows
unconditionally — even if a user manually checks the box, they
describe opposing-side filings and don't belong in our Akte set
(design §2.4 write-back exclusion).
- i18n: deadlines.overhaul.crossparty.badge / .tooltip (DE+EN).
- CSS: .fristen-overhaul-rule-crossparty + .is-cross-party row
modifier.
S1a — Spawn-only picker filter:
- SearchEvents WHERE now adds `sr.is_spawn = false` so spawn rules
(e.g. appeal_spawn, the inf.cfi → upc.apl.merits hop) no longer
surface as picker hits. Spawn rules are consequences, not
triggers — a lawyer searching "Berufung" wants the appeal-tree
root, not the inf.cfi spawn link.
- Terminal leaves (Duplik etc.) stay pickable per design §2.2's
carve-out: their own anchor is non-spawn, so they surface and
render an honest empty follow-up list.
Honest UX: hiding cross-party follow-ups lied about what the
workflow does next (cf. RoP.029.d falling off when perspective=
claimant on def_to_ccr — the workflow continues, just on the
defendant's docket). The fix makes the data legible without
contaminating the write-back path.
Verified: go vet clean, bun build clean, bun test 256/256,
go test ./internal/services/... -run LookupFollowUps... clean.
Design: docs/design-deadline-system-revision-2026-05-27.md §2.4
(cross-party) + §2.2 (spawn-only picker). t-paliad-331.
|
|||
| d36cc9ee15 |
feat(deadline-system): P0 — per-project scenario_flags SSoT (m/paliad#149)
Phase 2 P0 of the deadline + procedural-events revision. Establishes
paliad.projects.scenario_flags (jsonb) + paliad.scenario_flag_catalog as
the single source of truth for per-project scenario state — replacing
the three fragmented stores athena flagged (project_event_choices,
scenarios.spec, DOM-only). All three were empty per the audit so no
data migration is needed.
The jsonb map carries two key shapes:
* named flags (whitelist via scenario_flag_catalog) — today
with_ccr / with_amend / with_cci
* per-rule selection deviations of shape "rule:<uuid>" — wired up
here for validation; the consumer UI lands in P3
Endpoints:
GET /api/projects/{id}/scenario-flags
PATCH /api/projects/{id}/scenario-flags
PATCH semantics: bool = write; null = delete (priority-driven default
returns); missing key = leave alone. The service validates every key
on write (catalog lookup + UUID rule-membership + mandatory-cannot-be-
deselected) before persisting, so a single bad key fails the whole
patch.
Frontend bind: new scenario-flags.ts client module + Mode B's flag
checkboxes (ccr-flag / inf-amend-flag / rev-amend-flag / rev-cci-flag)
now hydrate from / persist to the project's scenario_flags on every
toggle. Kontextfrei (no project) is unchanged. Cross-surface coherence
via a scenario-flag-changed CustomEvent (peer surfaces — Verfahrens-
ablauf strip, Mode B result-view — will subscribe in P3).
Mig 154 is audit-defensive (set_config of paliad.audit_reason); no
audit trigger fires on paliad.projects today but a future one will
inherit the reason. Seeds the three known flags. CHECK constraints
enforce the top-level shape (jsonb_typeof = 'object') and the
catalog key pattern (lowercase, not 'rule:%' prefix).
Verified against the live DB: 18 projects default to '{}', catalog
has 3 rows, applied_migrations advanced to 154.
Design: docs/design-deadline-system-revision-2026-05-27.md §2.3, §2.4a,
§4.1, §5 (P0 row). t-paliad-331.
|
|||
| c48fa93e3d |
fix(verfahrensablauf): dark-mode token migration for timeline view CSS
The /tools/verfahrensablauf?view=timeline page (and the columns-view mirror via `.fr-col-item--*` modifiers) had hardcoded light backgrounds that survived a dark-mode flip. Root cause: four undefined custom properties (`--color-bg-soft`, `--color-border-soft`, `--color-accent-bg`, `--brand-lime`) consumed by the `.timeline-*` and adjacent `.event-card-choices-*` rules, each with a hardcoded hex / RGBA fallback. Since neither :root nor :root[data-theme='dark'] defines those tokens, every consumer fell through to the light fallback in both themes — leaving the conditional-row bg, the popover surface, the option-button bg, the chip-skipped bg, and the popover block-separator border stuck on near-white in dark mode. Earlier t-paliad-326 covered the new Fristenrechner overhaul CSS only; the timeline view styles are a separate block (~L3337-3810 in `frontend/src/styles/global.css`) and were not touched. Migrate every consumer to the existing dual-theme tokens already used across paliad. Same approach m approved for t-paliad-326, no new tokens introduced (all reuse): - Card / popover surfaces (`.event-card-choices-popover`, `.event-card-choices-option`) → `--color-surface` (white light / card-tint dark) so the popover reads as raised above the body in both themes. - Subtle raised surface for conditional row, skipped chip, option hover (`.timeline-item--conditional .timeline-content`, `.fr-col-item--conditional`, `.event-card-choices-chip-part--skipped`, `.event-card-choices-option:hover/:focus-visible`) → `--color-surface-muted`. **This is the visible bug fix m flagged.** - Lime-tint backdrops (`.timeline-context-note` bg, `.event-card-choices-caret:hover/:focus-visible`) → `--color-bg-lime-tint` (lime-alpha 0.10 light / 0.12 dark). - Active-option chip bg (`.event-card-choices-chip-part`) → `--color-accent-soft-bg`. - Lime accent borders / fills (`.timeline-context-note` left border, `.timeline-date--overridden`, `.frist-date-edit-input`, `.event-card-choices-unhide-btn`, `.event-card-choices-option--active`) → `--color-accent` / `--color-accent-fg`. Drops the legacy `#c6f41c` fallback that doesn't match the current brand (`--hlc-lime: #BFF355`). - Neutral borders (`.event-card-choices-caret`, `.timeline-item--hidden .timeline-content`, `.fr-col-item--hidden`, `.timeline-item--conditional .timeline-content`, `.event-card-choices-popover`, `.event-card-choices-option`, `.event-card-choices-block + .event-card-choices-block`) → `--color-border` (warm cream-derived light / cream-alpha dark). - Popover shadow `rgba(0, 0, 0, 0.12)` → `var(--shadow-md)` (auto-deepens to `rgba(0, 0, 0, 0.45)` in dark). - Status red (`.event-card-choices-error`) → `--status-red-fg` (defined in both themes; old `#b00020` fallback unreachable). Zero new tokens introduced — every replacement uses a token already shipped in both :root and :root[data-theme='dark']. Verified by mounting `frontend/dist/assets/global.css` against a representative static DOM (context-note banner, every timeline-item variant — conditional / skipped / hidden / overridden-date / mandatory — popover with active/inactive options, unhide button, error message, all four party-badge stances) and toggling `data-theme` between light and dark: conditional row bg flips from grey to muted-cream-on- midnight, popover lifts off the body via `--shadow-md`, every chip and border stays legible in both themes. bun build + bun test (256 pass) + go vet clean. |
|||
| 76d38c4c84 |
fix(fristenrechner): dark-mode token migration for overhaul CSS (m/paliad#146)
The Fristenrechner overhaul CSS shipped in S2/S3/S4 (commits |
|||
| ba3e0795f8 |
feat(fristenrechner): Slice S6 — drop cascade endpoint, neutralize legacy Pathway B (m/paliad#146)
Cleanup pass per design §7 / S6, executed as a measured first cut
that drops the cascade endpoint + neutralizes the legacy Pathway B
row-stack / cascade init without lifting the entire ~1500 LoC
subtree out of `fristenrechner.ts`. The dead helpers stay for one
follow-up that can lift them safely.
Backend:
* Deleted `internal/handlers/fristenrechner_event_categories.go`.
* Dropped the `GET /api/tools/fristenrechner/event-categories`
route from `handlers.go`. The `EventCategoryService` itself
stays — it still backs the legacy concept-card search's
`?event_category_slug=` filter, which dies in the same
follow-up that removes the concept-card response shape.
* `paliad.event_categories` TABLE is untouched per design §7
(kept for future tools).
Frontend:
* `loadEventCategoryTree()` reduced to a stub returning `[]` — the
endpoint it fetched no longer exists, and no overhaul surface
calls it.
* `initB1Cascade()`, `initForumFilter()`, `initInboxFilter()`
early-return. Their `DOMContentLoaded` registrations stay so
the bundle exports are stable, but no Pathway B cascade /
chip-strip / inbox-channel wiring fires in `?legacy=1` mode.
* The Pathway B markup in `fristenrechner.tsx` stays in place; it
renders inert when a user hits `?legacy=1&path=b`.
* `buildRowStack`, `renderRowStack`, `runB1Search`, and the row-
stack helper functions remain as unreachable code. Removing
them mechanically requires retiring the entire upper-half
Pathway B B2 search wiring (`runSearch` + `renderConceptCard`
+ `renderSearchResults` + `SearchResponse` types) which is
tangled with the legacy concept-card response shape — deferred
to a follow-up that lands together with the backend
concept-card removal.
Verified — bun build clean (2971 i18n keys unchanged), 256
frontend tests pass, go build + vet clean, live-DB tests
(TestListProceedings, TestSearchEvents, TestLookupFollowUps)
still green.
Follow-up scope tracked in design §7 S6 — pending the helper-tree
lift and the legacy concept-card response-shape removal from
/search.
|
|||
| 4571bd4980 |
feat(fristenrechner): Slice S5 — flip overhaul default; legacy under ?legacy=1 (m/paliad#146)
`isOverhaulMode()` now returns true unless the URL carries `?legacy=1`. The overhaul UI from S2-S4 (mode tabs + Mode A search + Mode B wizard + shared result view) becomes the default landing for /tools/fristenrechner; the legacy three-step wizard + Pathway A/B + cascade is reachable only via the explicit `?legacy=1` opt-out for the two-week deprecation window before S6 drops the legacy code paths entirely. The pre-existing `?overhaul=1` deep links from S2-S4 still resolve — the detector treats *absence* of `?legacy=1` as overhaul, so bookmarks stay valid. No sidebar / header / outbound link change needed: those all point at the bare `/tools/fristenrechner` URL, which now boots overhaul. Verified — bun build clean (2971 i18n keys unchanged), 256 frontend tests pass, go build + vet clean. |
|||
| 70985d88b0 |
feat(fristenrechner): Slice S4 — Mode B wizard (m/paliad#146)
Mode B "🧭 Geführt" — the guided 3-5 row wizard defined in
docs/design-fristenrechner-overhaul-2026-05-26.md §3.2. Lands the
user on a single procedural_event (the trigger), then transitions
to the shared §4 result view.
Frontend:
* `fristenrechner-wizard.ts` — row stack with R1..R5:
R1 Was ist passiert? (event_kind, always asked)
R2 Vor welchem Gericht? (jurisdiction, skip if R1 narrows)
R3 In welchem Verfahren? (proceeding_type, auto-skip when
narrowed pool has 1 option)
R4 Welches Schriftstück? (procedural_event, landing)
R5 Welche Seite vertreten Sie? (party, only when follow-ups
differ by primary_party)
Row badges per §11.Q3: R1+R2 = Filter, R3+R4+R5 = Qualifier.
R5 has NO "Beide" option per §11.Q8 — Mode B is the file-mode
where perspective is a qualifier.
* Project prefill — derives R3 + R2 jurisdiction from
project.proceeding_type, R5 from project.our_side. Annotates
pre-filled rows with "aus Akte" tag and implicit rows with
"implizit" tag per §11.Q10 ("erhalten" annotation when a pick is
carried across an upstream change).
* R4-to-result transition — after R4 the wizard fetches /follow-
ups (no dates) to inspect primary_party variance. If both
claimant and defendant rules exist AND R5 isn't already set,
swaps the loading row for the R5 chip picker. Otherwise jumps
straight to mountResultView.
* URL state — `?mode=wizard&kind=…&forum=…&pt=…&r4=…&party=…`
keeps deep-link / back-nav consistent (the launchResult step
sets `event=` so the result view picks up).
* `fristenrechner-result.ts` mountModeShell now dispatches the
"wizard" tab to the wizard module (was a coming-soon
placeholder).
* 18 i18n keys added (DE + EN parity), 145-line CSS block for the
wizard row stack with Filter / Qualifier badge styling and
"aus Akte" annotation chip.
Backend:
* `ProceedingListOptions.EventKind` adds an EXISTS subquery
filter on `paliad.sequencing_rules` ⨯ `paliad.procedural_events`
so Mode B R3 chips only show proceedings whose event roster
contains at least one event of the requested kind (design
§6.3). Endpoint param: `event_kind=` on
/api/tools/proceeding-types.
Test updates:
* `TestListProceedings` switched from SKIP-when-column-missing to
asserting the live filter — mig 153 has landed, `kind` column
is in place. New subtests: kind=proceeding includes
upc.inf.cfi and excludes the phase row upc.cfi.interim;
event_kind=filing narrows to proceedings with filing events.
* `fristenrechner-wizard.test.ts` covers
`followUpsDifferByParty` — the R5 trigger predicate. 7 cases:
asymmetric → true; uniform / both / court / empty → false.
Verified — bun build clean (2971 i18n keys), 256 frontend tests
pass (incl. 7 new), go build + vet clean, live-DB
TestListProceedings passes all 6 subtests against mig 153 data.
|
|||
| 2a2c5b8033 |
feat(fristenrechner): Slice S3 — Mode A direct search (m/paliad#146)
Mode A "⚡ Direkt suchen" — the power-user entry path defined in docs/design-fristenrechner-overhaul-2026-05-26.md §3.1. Renders above the §4 result view; clicking a result row locks the trigger event and transitions to the shared result surface from S2. Frontend: * `fristenrechner-mode-a.ts` — filter strip (Forum / Verfahren / Was passierte / Partei) + free-text search input + result list. Section-split visual hierarchy per m §11.Q3: filter chips in a bordered "Filter (eingrenzen)" strip on top, result list below. Inbox channel chip lives behind an "Erweitert" details summary per §3.3; picking CMS / beA auto-nudges the Forum chip. Party chip retains a "Beide" option (Mode A is filter mode per §11.Q8; Mode B drops it in S4). * `fristenrechner-result.ts` — new `mountModeShell(activeTab)` renders the two mode tabs per §11.Q2 and lazy-imports Mode A. Mode B tab is a placeholder until S4 lands. * `fristenrechner.ts` boot — when `?overhaul=1` is set and `?event` is empty, mountModeShell takes over (default tab = search; `?mode= wizard` opens the wizard tab when S4 ships). With `?event=` the flow still jumps straight to the result view. URL state syncs forum / pt / kind / party / q on every chip click. * 28 i18n keys added (DE + EN parity), 310-line CSS block for the mode tabs + Mode A surface. Backend: * New `ProceedingListOptions { Jurisdiction, Kind }` + service method `ListProceedings(ctx, opts)`. Legacy `ListFristenrechnerTypes` keeps the no-filter signature for existing callers. Handler `/api/tools/proceeding-types` accepts `?jurisdiction=` and `?kind=` query params. * `kind=proceeding` filter targets the taxonomy column landed in mig 153 (parallel branch t-paliad-325, m/paliad#147). Sequenced per the taxonomy doc §7 option (c): mig 153 merges before S3 ships to main, so the filter is never false-positive (no phase / side_action / meta rows leak into the chip strip). Verified — bun build clean (2955 i18n keys, data-i18n attributes clean), 249 frontend tests pass, go build + vet clean. New TestListProceedings — 4 PASS (no-filter, jurisdiction=UPC, jurisdiction=DE, ListFristenrechnerTypes alias) + 1 SKIP for the kind=proceeding case that probes the column and skips when mig 153 hasn't landed yet. S1 + S2 live tests still green. |
|||
| 9ab8dd8e0f |
feat(fristenrechner): Slice S2 — result view under ?overhaul=1 (m/paliad#146)
New `frontend/src/client/fristenrechner-result.ts` module renders the
shared result surface defined in
docs/design-fristenrechner-overhaul-2026-05-26.md §4:
* Sticky trigger card — event icon + name, proceeding/jurisdiction
chips, inline trigger-date input that re-fetches on change.
* Four follow-up groups — Mandatory / Recommended / Optional /
Conditional. SPAWNED rules fold into their priority bucket with
a `⇲ neues Verfahren` badge (§11.Q5). Conditional bucket holds
every rule with sr.condition_expr IS NOT NULL.
* Per-rule rows — title, duration phrase, party chip, legal-source
citation (with youpc.org link when available), pre-checked
checkbox driven by `defaultChecked(r)` (mandatory + recommended
on; conditional + court-set + optional off), inline ✏ Datum
override that re-renders.
* Write-back footer — conditional on `?project=<uuid>` per §11.Q7;
in kontextfrei mode the footer is hidden and an inline nudge
invites the user to pick an Akte. CTA submits to the existing
POST /api/projects/{id}/deadlines/bulk endpoint, stamping each
row with `audit_reason: "Aus Fristenrechner — Trigger: {name}
({date})"` per §11.Q12.
Mount + URL contract — when `?overhaul=1` is set in the URL,
`fristenrechner.ts` hides every legacy panel (`fristen-step1`,
`fristen-step2`, `fristen-pathway-a`, `fristen-pathway-b`,
`fristen-step3a`, the step-1 summary) and shows the overhaul root
instead. With `?overhaul=1&event=<code>&trigger_date=…` the surface
is deep-linkable end-to-end. Without `?event=` the empty-shell
nudge renders — S3+S4 will mount the entry-mode UIs onto this same
root.
Verified — bun build clean, 249 frontend tests pass (incl. 9 new
helper tests for groupFollowUps + defaultChecked), go build + vet
clean, S1 live-DB tests still green.
|
|||
| 6acb1167dd |
feat(admin): add proceeding-type column to /admin/procedural-events list (t-paliad-321 / m/paliad#144)
Surfaces the 3-segment proceeding code (e.g. upc.inf.cfi) on the admin
rules list so the 4 legitimately-distinct same-named groups are
visually disambiguated without opening each row's edit page.
Specifically helps with:
- "Antrag auf Patentänderung" × 4 (distinct proceeding_type_ids)
- "Beginn des Hauptsacheverfahrens" × 2
- "Berufungsbegründung-R.220.1" × 2
- "Berufungsschrift-R.220.1" × 2
(The 6× "Mängelbeseitigung / Zahlung" identical clones are dedup'd by
mig 152 in the sibling commit; this column lets m verify the dedupe
landed and confirms the remaining same-named groups are intentional.)
* internal/services/rule_editor_service.go —
- LoadProceedingTypeCodes(ctx, rows) — batch SELECT id, code FROM
paliad.proceeding_types WHERE id = ANY(...) for every distinct
non-NULL proceeding_type_id in rows. Returns id → code map.
Single round-trip, firm-wide reference data (no RLS / visibility
gate). Used only by the LIST endpoint; GetByID etc. don't need it.
* internal/handlers/admin_rules.go —
- adminRuleResponse gains ProceedingTypeCode *string field
(json:"proceeding_type_code,omitempty"). Populated by
wrapRuleListResponse from the id → code map.
- handleAdminListRules calls LoadProceedingTypeCodes after fetching
rows, passes the map to wrapRuleListResponse.
* frontend/src/admin-rules-list.tsx —
- Adds Proceeding column header in position 2 (between Submission
Code and Legal Citation) per paliadin's "Place between submission-
code and the existing columns" spec. Binds to canonical i18n
key admin.procedural_events.col.proceeding (added below).
- Drops the legacy Verfahrenstyp column at position 4 — the new
code-only column at position 2 replaces it; the old column
showed `code · name` which duplicates the new content.
* frontend/src/client/admin-rules-list.ts —
- Rule type gains proceeding_type_code?: string | null.
- New proceedingCodeCell(r) helper: prefers server-side
proceeding_type_code, falls back to dropdown-lookup
proceedingLabel for defense-in-depth on older API responses
(the old behaviour broke for rules whose proceeding_type_id
pointed at non-fristenrechner category proceedings; the new
column never has that bug because the join is server-side).
- Row rendering: new <td class="admin-rules-col-proceeding"><code>
proceedingCodeCell(r) </code></td> in column 2.
* frontend/src/client/i18n.ts —
- admin.procedural_events.col.proceeding alias added for DE +
EN ("Verfahren" / "Proceeding"). Mirror style of the other
canonical aliases from Slice A.
* frontend/src/i18n-keys.ts —
- Generated key union extended with
"admin.procedural_events.col.proceeding".
Build + vet clean. No new SQL — proceeding_types is firm-wide
reference data and the join uses an existing primary key.
|
|||
| bd7896ef68 |
feat(submissions): Composer Slice F — section reorder / hide / add custom (m/paliad#141)
The final Composer slice per design doc §12. Lawyer gains full
control over section composition: drag-and-drop reorder, per-section
delete, "+ Add section" picker for custom slugs that don't appear in
the base's default spec. Combined with Slice B's hide toggle, this
closes out the A→F sequence — Composer A→F is complete.
Backend (internal/services/submission_section_service.go, +120 LoC):
- SectionService.Create — adds a new section row to a draft. Validates
section_key + labels + kind (must be prose/requests/evidence).
Auto-assigns next order_index when OrderIndex=0; collisions on
(draft_id, section_key) surface as ErrInvalidInput.
- SectionService.Delete — removes one section by id. Returns
ErrSubmissionSectionNotFound when nothing was deleted.
- SectionService.Reorder — accepts a sequence of section_ids, rewrites
every row's order_index to (1..N)×10 transactionally. Returns the
refreshed list. Sections not present in the sequence are silently
ignored (defensive — partial reorder doesn't lose rows).
Handlers (internal/handlers/submission_sections.go, +180 LoC):
- POST /api/submission-drafts/{draft_id}/sections — owner-scoped via
SubmissionDraftService.Get. 400 on slug collision / invalid kind.
- DELETE /api/submission-drafts/{draft_id}/sections/{section_id} —
owner + section-belongs-to-draft cross-check. 204 on success.
- POST /api/submission-drafts/{draft_id}/sections/reorder — accepts
{"section_order": [uuid, uuid, ...]}; returns refreshed sections list.
Frontend (frontend/src/client/submission-draft.ts, +260 LoC):
- Each section row gains a drag handle (⋮⋮) on the left of the head.
Drag handle is the only draggable element; contentEditable
selections inside the editor body keep working. HTML5 native DnD,
no library.
- Drop-target highlighting via .submission-draft-section--drop-target
(border-top accent). Cleanup on dragend / drop / cancel.
- Per-section "Delete" button next to the existing Hide/Include
toggle. Confirm prompt prevents accidental loss of typed prose.
- "+ Add section" trailing affordance below the section list opens an
inline form (slug + DE label + EN label + kind dropdown). Submit
POSTs to the new endpoint; on success splices the row into
state.view.sections and re-paints.
CSS (frontend/src/styles/global.css, +65 LoC):
- .submission-draft-section-handle (grab cursor + hover background +
active=grabbing).
- .submission-draft-section--dragging / --drop-target visual states.
- .submission-draft-add-section form layout (dashed border + lime
primary submit).
Tests (internal/services/submission_section_slice_f_test.go, NEW,
TEST_DATABASE_URL-gated):
- Create custom section + slug-collision surface as ErrInvalidInput.
- Delete + repeat-delete returns ErrSubmissionSectionNotFound.
- Reorder reverses 10 seeded sections + verifies the resulting
order_index sequence is ascending and matches the input order.
Build hygiene: go build/vet/test -short clean (all packages);
bun run build clean (2906 i18n keys, data-i18n scan clean).
Hard rules honoured:
- NO new migrations (Slice F is pure code on Slice A's schema).
- NO behavior change for pre-Composer drafts (no section rows → no
drag handles to drag).
- {{rule.X}} aliases preserved (custom sections render through the
same composer pipeline as default sections).
- Q2/Q9/Q10 ratifications preserved.
This closes the Composer slice sequence A → F. The full feature set
ratified by m on 2026-05-26 is now in place:
A — base picker + read-only section list (mig 146/147/148)
B — editable prose + anchor-spliced render + MD→OOXML walker
C — building-blocks library + section picker (mig 149)
D — rich prose (headings, lists, blockquote, hyperlinks)
E — specialist bases lg-duesseldorf + upc-formal (mig 150)
F — section reorder / delete / add custom
t-paliad-318 Slice F
|
|||
| 5834e3dc66 | Merge: Composer Slice D — rich prose (headings, lists, blockquote, hyperlinks) in MD→OOXML walker (m/paliad#141) | |||
| 677849784c |
feat(submissions): Composer Slice D — rich prose (headings, lists, blockquote, hyperlinks) (m/paliad#141)
Extends the Composer's MD → OOXML walker per the design at
docs/design-submission-generator-v2-2026-05-26.md §12 Slice D from
Slice B's paragraphs + B/I baseline to the full rich-prose feature set:
headings 1-3, bullet + numbered lists, blockquote, inline hyperlinks.
MD walker (internal/services/submission_md.go, +320 / -75 LoC):
- RenderMarkdownToOOXMLWithStyles is the new Slice-D entry point;
RenderMarkdownToOOXML stays as a thin back-compat wrapper.
- splitMarkdownBlocks classifies every line into one of:
paragraph, heading_1/2/3, list_bullet, list_numbered, blockquote.
CommonMark-style 3-space indent tolerance; "N. " and "N) " for
numbered. Blank-line spacing semantics preserved from Slice B.
- renderBlockParagraph applies stylemap[blk.styleKey] (with
fall-back to stylemap["paragraph"]). List blocks emit visible
"• " / "N. " prefix runs so the structure surfaces even if Word
isn't configured with auto-list-numbering — lawyer can apply a
real Word list style post-export. Numbered-list ordinals reset
on every non-list block (so "1. A\nplain\n1. C" renders 1./1.,
not 1./2.).
- parseInlineRuns adds `[label](url)` recognition. Each link gets
routed through the optional HyperlinkAllocator; the walker emits
`<w:hyperlink r:id="{rId}">…runs…</w:hyperlink>` with the
"Hyperlink" character style on each child run. Nil allocator
falls back to plain-text label (URL drops, label survives).
Composer (internal/services/submission_compose.go, +130 / -10 LoC):
- composerLinkAllocator hands the walker fresh rIds (rIdComposer1,
rIdComposer2, …) outside the base's existing namespace; same URL
shared across multiple sections dedupes to one rId.
- patchDocumentXMLRels appends matching <Relationship Type="…/hyperlink"
Target="URL" TargetMode="External"/> entries to
word/_rels/document.xml.rels. Idempotent on rIds already present;
synthesizes a fresh rels part when missing (defensive for stripped
bases). Returns the patched parts slice (caller must overwrite
because append may grow the backing array — fixed in this slice).
- Compose now passes the full stylemap (paragraph + heading_1/2/3 +
list_bullet + list_numbered + blockquote) into the walker, not
just the paragraph-style entry.
Frontend (frontend/src/client/submission-draft.ts):
- Toolbar adds H1/H2/H3 buttons (formatBlock h1/h2/h3), bullet
list, numbered list, blockquote, and a link button that prompts
for a URL + wraps the selection via execCommand("createLink").
- domToMarkdown serializer extends to <h1>/<h2>/<h3>, <ul>/<ol>
with per-item ordinal counter for numbered lists, <blockquote>,
and <a href="…"> → `[label](url)`. Nested <li> handling sits in
the ul/ol branch.
Tests (internal/services/submission_md_test.go, internal/services/
submission_compose_test.go):
- TestRenderMarkdownToOOXML_Heading1 / _Heading2And3 — stylemap
applied.
- _BulletList / _NumberedList / _NumberedListResetsOnNonList —
prefixes + ordinal counter.
- _Blockquote — stylemap applied.
- _Hyperlink — allocator called, w:hyperlink rId wired, Hyperlink
character style on label runs.
- _HyperlinkNilAllocatorFallsBackToPlain — label survives, no
hyperlink tag emitted.
- TestDetectBlockMarker — 13 marker / non-marker cases.
- TestComposer_HeadingsAndLists — end-to-end through Compose with
a multi-construct draft; verifies stylemap presence + content +
ordinal prefixes.
- TestComposer_HyperlinkWiresRels — body has the right
<w:hyperlink r:id="rIdComposer{N}">, document.xml.rels has the
matching <Relationship> rows with External target mode.
- TestComposer_HyperlinkDedupesByURL — two `[label](url)` references
to the same URL share one rId; second allocation gets no new
Relationship row.
Build hygiene: go build/vet/test -short clean (all packages); bun run
build clean (2906 i18n keys).
NOT in scope (Slice D's brief was rich-prose + toolbar):
- Numbering.xml audit on bases — current approach emits visible
"• " / "N. " prefix runs without depending on numbering.xml. A
future slice can swap to `<w:numPr>` if firm-style auto-numbering
becomes a hard requirement.
- DOM-from-Markdown on initial editor paint — the editor still uses
textContent=md, so toolbar-applied formatting reverts to literal
Markdown text after autosave + repaint. Acceptable trade-off for
Slice D; a future polish could parse MD into the DOM on paint.
- Tables, images, footnotes (still design §13 out of scope).
Hard rules honoured:
- NO new migrations (Slice D is pure code).
- NO behavior change for pre-Composer drafts (gate on draft.BaseID
unchanged).
- {{rule.X}} aliases preserved (placeholders pass through the walker
verbatim, get substituted by the v1 SubmissionRenderer pass).
- Q2 ratification preserved (no building_block_id lineage).
- Q9 ratification preserved (4-tier BB visibility from Slice C).
t-paliad-316 Slice D
|
|||
| 9359e99a6b |
feat(handlers,frontend): Slice B.6 — admin URL rename /admin/rules → /admin/procedural-events with 301 redirects + .tsx i18n rebind (t-paliad-305 / m/paliad#93)
Closes the procedural-events rename loop opened by m/paliad#93. The admin surface now lives under its canonical URL; the legacy paths remain reachable for one deprecation cycle via 301 redirects so bookmarks, audit-log entries, and curl scripts keep working. * internal/handlers/handlers.go — - Registers the 12 canonical routes under /admin/procedural-events* (page paths and JSON API). Same handlers — just the new URL slot. - Registers the 12 legacy /admin/rules* routes as 301 redirects. * internal/handlers/admin_rules.go — - redirectToProceduralEvents(dst) — fixed-destination redirect for paths without an {id}. - redirectToProceduralEventEdit — page redirect carrying the {id}. - redirectToProceduralEventAPI(suffix) — JSON API redirect carrying {id} + optional suffix (/clone-as-draft, /publish, /archive, /restore, /audit, /preview). Query string is preserved on every redirect. - All three helpers add the IETF Deprecation header + a Link header pointing at the successor-version path. * frontend internal nav + URL strings — Sidebar.tsx, admin.tsx, admin-rules-list.tsx, admin-rules-edit.tsx, client/admin-rules-list.ts, client/admin-rules-edit.ts: every `/admin/rules*` reference flipped to `/admin/procedural-events*`. In-app navigation now hits the canonical paths directly without a redirect round-trip; external callers keep working via the 301s. * frontend .tsx i18n rebind — 9 admin .tsx i18n bindings rebound to the canonical `admin.procedural_events.*` keys that already exist as aliases in i18n.ts (per Slice A from t-paliad-262). Specifically: admin.rules.list.title → admin.procedural_events.list.title admin.rules.list.heading → admin.procedural_events.list.heading admin.rules.list.new → admin.procedural_events.list.new admin.rules.col.submission_code → admin.procedural_events.col.code admin.rules.edit.title → admin.procedural_events.edit.title admin.rules.edit.breadcrumb → admin.procedural_events.edit.breadcrumb admin.rules.edit.field.submission_code → admin.procedural_events.edit.field.code admin.rules.edit.field.event_type → admin.procedural_events.edit.field.event_kind admin.rules.edit.field.parent → admin.procedural_events.edit.field.parent The remaining ~142 admin.rules.* keys do NOT yet have procedural_events aliases. Migrating them is a follow-up slice — each needs a new alias entry in i18n.ts (DE + EN) before the .tsx reference can be flipped. The 9 keys touched here are the most visible (page titles + edit-page field labels) so the admin UI immediately reads as "Verfahrensschritte" everywhere. * frontend/src/client/i18n.ts header comment updated to reflect that the URL rename has shipped (Slice B.6 done) and to flag the remaining i18n-key migration as the next step. Scope (documented, paliadin authorised): - "go everything" applied: backend routes + frontend nav + .tsx rebind of the 9 keys whose canonical aliases exist. - Full migration of all 142 admin.rules.* keys deferred — would require seeding ~142 new alias entries in i18n.ts (DE + EN) plus another 142 .tsx rebinds. Out of scope for tonight; flag as follow-up `feat(i18n): finish admin.rules.* → admin.procedural_events.* alias migration`. - 12 legacy /admin/rules routes still hit a handler (the redirect helper) — they don't 404 yet. Once a deprecation window passes with no traffic on the old paths, a future slice can drop them outright. Build + vet clean. TestMigrations_NoDuplicateSlot passes. This concludes the m/paliad#93 procedural-events rename slice train (Slices A through B.6). curie stays parked persistently for any follow-up the deploy / monitor cycle surfaces. |
|||
| ee98db94fa |
feat(submissions): Composer Slice C — building blocks library (m/paliad#141)
Per the design at docs/design-submission-generator-v2-2026-05-26.md §8
and the Q2 / Q9 ratifications:
- Q2 (m, 2026-05-26): building blocks are plain text paste sources.
No building_block_id reference is stored on submission_sections.
- Q9 (m, 2026-05-26): four visibility tiers — private / team / firm
/ global.
Schema (mig 149):
- paliad.submission_building_blocks — library catalog. Columns: slug,
firm (NULL = cross-firm), section_key (binds to one section kind),
proceeding_family (NULL = any), title_de/_en + description_de/_en
+ content_md_de/_en, author_id, visibility (CHECK in 4-tier set),
is_published, created_at, updated_at, deleted_at (soft delete).
RLS: coarse-grained SELECT — every authenticated user sees
non-deleted non-private rows + own private rows. Tier-specific
predicate (private/team/firm/global) applied in Go-layer service so
semantics evolve without RLS migrations. Mutations admin-only (no
RLS write paths).
- paliad.submission_building_block_admin_versions — append-only
history per block, retention=20. Admin-side only; NOT referenced
from submission_sections (per Q2's plain-text-paste model). Exists
so accidental delete / overwrite are recoverable.
Backend:
- internal/services/submission_building_block_service.go (~510 LoC):
BuildingBlockService. ListVisible applies tier predicate at query
time (private = author_id match; firm = firm column NULL OR matches
branding.Name; team = author shares a project_team with caller via
paliad.project_teams self-join; global = open). ListAllForAdmin
drops the predicate. Create + Update + SoftDelete + RestoreVersion
all transactional; appendVersionTx writes one audit row +
GC-deletes anything past the retention=20 horizon in the same tx.
InsertIntoSection (the paste mechanic) clones content_md_<lang>
into the section row with a "\n\n" separator if section already has
content. NO building_block_id stamped per Q2.
- internal/handlers/submission_building_blocks.go (~480 LoC): nine
handlers split between the lawyer-facing picker (list, insert) and
the admin editor (list, get, create, update, delete, list-versions,
restore-version, page). buildingBlockUpdateInput uses presence-
tracking UnmarshalJSON for the four nullable fields (firm,
proceeding_family, description_de/_en) so PATCH can distinguish
"no change" from "set to null".
- Routes registered: lawyer-facing under /api/submission-building-blocks,
admin-gated under /api/admin/submission-building-blocks/* and
/admin/submission-building-blocks (page).
- Wiring: handlers.Services + dbServices + cmd/server/main.go all
gain SubmissionBuildingBlock. NewBuildingBlockService takes the
branding.Name firm hint for the visibility predicate.
Frontend:
- frontend/src/admin-submission-building-blocks.tsx (~85 LoC):
three-pane admin shell (list / editor / version log) registered
in build.ts.
- frontend/src/client/admin-submission-building-blocks.ts (~370
LoC): admin client — list paint, edit form (slug + firm +
section_key + proceeding_family + title/desc/content per lang +
visibility radio + is_published toggle), per-block version log
with restore button. Bilingual labels.
- frontend/src/client/submission-draft.ts: per-section "+ Baustein"
button on the Composer editor toolbar (Slice B substrate gets one
more affordance). openBlockPicker opens a modal filtered to the
section's section_key, 200ms-debounced search by free text against
title/description/content. Click a hit → POST insert-into-section
→ section row's content_md_<lang> gains the block's content
appended at the end (Q2's plain-text paste semantic, no lineage).
- ~240 LoC of CSS: modal overlay + picker rows with tier-colored
visibility chips + admin editor 3-pane grid + form rows + version
list.
- 12 new i18n keys × 2 langs (admin.building_blocks.*).
Tests:
- TestValidVisibility (8 cases including case-sensitivity + empty).
- TestAppendBlockContent (8 cases covering empty-existing / empty-
addition / whitespace-only / trailing newline collapse).
- TestBuildingBlockVisibilityConstants pins the 4 string literals
against drift (RLS predicate + DB CHECK depend on them).
Build hygiene: go build/vet/test -short clean; bun run build clean
(2906 i18n keys, data-i18n scan clean).
Hard rules per ratifications honoured:
- Q2: no building_block_id lineage on sections (paste is plain text).
- Q9: 4 visibility tiers (private/team/firm/global).
- NO behavior change for pre-Composer drafts (the picker just doesn't
show — section list is hidden for base_id NULL drafts).
- {{rule.X}} aliases preserved (block content goes through the same
v1 placeholder pass on export as section prose).
NOT in scope per Slice C brief:
- User-authored private blocks (Slice C ships admin curation only;
any-user create is a follow-up).
- Tier promotion review workflow (admin sets tier directly today).
- Per-section "where is this block used" reverse lookup (no lineage
to query).
- Slice D's rich-prose features (headings, lists, blockquote) still
Slice D's job; this Slice doesn't extend the MD walker.
t-paliad-315 Slice C
|
|||
| f963b0df34 |
feat(submissions): Composer Slice B — editable prose sections + anchor-spliced render (m/paliad#141)
The "Composer actually works" milestone per the design at
docs/design-submission-generator-v2-2026-05-26.md §12 Slice B. Builds on
Slice A's substrate (submission_bases, submission_sections, base_id on
drafts); no new migrations needed.
Backend additions:
- internal/services/submission_md.go (~240 LoC): Markdown → OOXML
walker. Per the head's Slice B brief, scope is paragraphs +
bold/italic + blank-line spacing. Placeholders pass through
unchanged for the v1 substitution pass. CRLF normalisation; nested
formatting (***bold-italic***); two delimiter forms (* and _);
XML-escaping for &/</>; explicit empty-paragraph emit so blank
lines round-trip. 12 unit tests.
- internal/services/submission_compose.go (~470 LoC): SubmissionComposer
service. Pipeline: ConvertDotmToDocx pre-pass → extract
word/document.xml → render each included section's content_md_<lang>
→ splice via {{#section:KEY}}/{{/section:KEY}} anchor pairs in
the body → strip anchors for excluded sections → append unanchored
sections before <w:sectPr> → repack zip → run v1 placeholder pass.
RE2-friendly anchor scanner walks markers in body-order and matches
open/close pairs with a stack (handles unbalanced anchors
defensively). 6 unit tests covering anchor-mode splice,
append-mode-no-anchors, excluded-section drop, placeholder
resolution, lang column pick, order_index ASC.
- internal/services/submission_section_service.go: SectionPatch +
Update method. Six optional fields (content_md_de/en, included,
label_de/en, order_index). Sentinel ErrSubmissionSectionNotFound on
RLS-filtered miss.
- internal/handlers/submission_sections.go (NEW, ~150 LoC):
PATCH /api/submission-drafts/{draft_id}/sections/{section_id}.
Owner-scoped via SubmissionDraftService.Get; section-belongs-to-draft
cross-check. 404 on both missing-draft and section-belongs-elsewhere
paths.
- internal/handlers/files.go: fetchComposerBaseBytes + composerBaseSlugMap
reuse the existing Gitea proxy cache for base .docx bytes. hlc-letterhead
→ existing firmSkeletonSubmissionSlug, neutral → existing
skeletonSubmissionSlug.
- internal/handlers/submission_drafts.go: exportSubmissionDraft helper
branches on draft.BaseID. When set AND base + bytes + sections all
resolve → Composer pipeline. Else v1 fallback render path stays.
Audit metadata jsonb gains "composer": true + "base_id" flag when
composer was used.
Wiring:
- handlers.Services gains SubmissionComposer.
- dbServices.submissionComposer wired from svc.SubmissionComposer.
- main.go instantiates NewSubmissionComposer with the existing
SubmissionRenderer (so the {{rule.X}} alias contract stays preserved
inside section content).
Frontend additions (~400 LoC):
- client/submission-draft.ts: paintSectionList rewritten to render a
contentEditable per included section with a per-section B/I
toolbar. Per-section autosave debounced 500ms; mousedown handlers on
toolbar buttons preserve editor focus mid-command. domToMarkdown
walks the contentEditable's DOM tree back to Markdown source-of-
truth (b/strong → **…**, i/em → *…*, div/p → paragraph break, br
→ newline). Updated state.view.sections in-place on PATCH success
without re-painting (avoids focus-stealing on every keystroke);
re-paints only on structural changes (included toggle, label edits,
order changes).
- client/submission-draft.ts: onSectionToggleIncluded hides/shows a
section via PATCH. flushSectionAutosave on blur force-flushes
pending edits so leaving an editor doesn't strand unsynced changes.
- styles/global.css: editor surface (contentEditable area with focus
ring + placeholder), toolbar buttons (B/I 1.8rem squares),
per-section "Hide"/"Include" toggle in the head row.
- Updated i18n hint copy: "Inhalt pro Abschnitt — Autosave nach
500ms. Letztes Layout in Word."
Templates regenerated on Gitea:
- _skeleton.docx → composer-mode body (anchors only): blob SHA
ac0cdeaf49f7cd417ec143e2319ffbb02ec65644.
- _firm-skeleton.docx → composer-mode body (anchors only, preserves
sectPr → firm header/footer rIds): blob SHA
f1e9a9fb9a29ca01bf7bee709a45c5dda2a8e317.
- Both uploaded as mAi via --netrc-file ~/.netrc-mai.
- gen-skeleton-submission-template script gains an -anchors flag
(default true) so future regens emit composer-ready bodies. The
_firm-skeleton.docx regen was done via a one-off /tmp helper since
the gen-hl-skeleton-template script requires the proprietary .dotm
source which lives in HL/mWorkRepo; extending that script to accept
an existing .docx as input is a follow-up cleanup.
Build hygiene: go build/vet/test -short ./internal/... ./cmd/... all
clean; bun run build clean (2900 i18n keys, data-i18n scan clean).
NO behavior change for pre-Composer drafts (base_id NULL → v1
fallback render path stays compiled in). NO migrations needed in this
slice — sections were already in the schema from Slice A; only
content_md_de/en UPDATEs happen via the new PATCH endpoint.
Hard rules per Q2/Q10 ratification still honoured:
- No building_block_id lineage (Slice C territory; Q2).
- Caption/letterhead/signature are regular prose sections, seeded from
base spec; lawyer can edit/hide freely (Q10).
- {{rule.X}} aliases preserved (renderer pass unchanged).
NOT in scope per Slice B brief:
- Headings 1–3, lists, blockquote (Slice D's MD walker extension).
- Building blocks library (Slice C).
- Reorder / add-custom-section (Slice F).
- Auto-upgrade of pre-Composer drafts (Slice C — explicitly NOT in
this slice per head's brief msg #2393).
t-paliad-313 Slice B
|
|||
| e2969fc358 |
feat(submissions): Composer Slice A — base picker + read-only section list (m/paliad#141)
The first slice of the Submission generator v2 ("Composer") per the
design at docs/design-submission-generator-v2-2026-05-26.md §12 Slice A.
Ships the base concept + per-draft section seeding end-to-end with NO
change to the .docx render path — v1 export still works exactly as
today.
Schema (mig 146/147/148):
- paliad.submission_bases — catalog table; one row per template base
(slug, firm, proceeding_family, label_de/en, gitea_path, section_spec
jsonb, is_default_for[]). RLS: wide-open SELECT for authenticated
users, mutations admin-only (handler-enforced, no RLS write paths).
Seeded with 2 rows: hlc-letterhead → _firm-skeleton.docx; neutral →
_skeleton.docx. Each section_spec carries the 10-section default
(letterhead, caption, introduction, requests, facts, legal_argument,
evidence, exhibits, closing, signature) with bilingual labels +
bag-driven seed Markdown for caption/letterhead/signature.
- paliad.submission_drafts gains base_id (FK SET NULL, optional) +
composer_meta jsonb (default '{}'). Purely additive; pre-Composer
drafts keep base_id NULL → v1 fallback render path stays active.
- paliad.submission_sections — per-draft section rows (draft_id,
section_key, order_index, kind ∈ {prose,requests,evidence},
label_de/en, included, content_md_de/en). RLS mirrors
submission_drafts (owner-scoped + can_see_project, four policies).
Backend:
- BaseService (read-only Slice A): List + GetByID + GetBySlug +
GetDefaultForCode (firm/family fallback chain).
- SectionService: ListForDraft + Get + SeedFromSpec (transactional
multi-INSERT).
- SubmissionDraftService.AttachComposer wires both; Create resolves
the firm default base and seeds base_id + section rows in one tx.
Composer wiring is additive — when bases==nil the service stays
v1-shaped.
- Update accepts BaseID **uuid.UUID (set / clear / no-change).
- submissionDraftView gains BaseID, ComposerMeta, Sections fields.
- Routes: GET /api/submission-bases (catalog list). PATCH endpoints
on both project-scoped and global drafts accept "base_id".
Frontend:
- submission-draft.tsx: base picker dropdown above language toggle
(hidden until catalog loads); section-list pane above the preview
(hidden when no rows).
- client/submission-draft.ts: loadBases() parallel-fetches on boot;
paintBasePicker rebuilds <option> list on every paint; onBaseChange
PATCHes base_id and repaints; paintSectionList renders each section
read-only (label + kind chip + excluded badge + Markdown body).
- Per the brief: NO auto-upgrade of existing 11 drafts (that's Slice C).
Pre-Composer drafts get the picker (catalog still loads) but the
section pane stays hidden until they pick a base on a new draft.
Tests:
- TestFamilyOfCode + TestBaseSectionSpec_DecodeShape + _EmptyDecode
(pure unit, no DB).
- TestComposerSeedFlow (live, TEST_DATABASE_URL-gated): asserts mig 146
seeded 10 default sections on both bases; GetDefaultForCode picks
hlc-letterhead for HLC/de.inf.lg.erwidg; new draft via Create seeds
base_id + 10 section rows in tx with ascending order_index and
bilingual labels populated.
NO behavior change to .docx export — the v1 path stays sole render
path this slice. Composer's anchor-based assembly engine + MD→OOXML
walker land in Slice B.
Build hygiene: go build/vet/test -short clean; bun run build clean
(2900 i18n keys, data-i18n scan clean).
t-paliad-313
|
|||
| 05ad43aa46 | Merge: t-paliad-308 — Verfahrensablauf URL state hybrid (chips in URL, scenario in localStorage) (m/paliad#137) | |||
| 43de8f9c7b |
feat(verfahrensablauf): URL state hybrid — filter chips in URL, scenario in localStorage (t-paliad-308, m/paliad#137)
Splits /tools/verfahrensablauf persisted state into two namespaces: URL params (timeline kind — paste-able, shareable, refresh-resistant): proceeding, side, target, trigger_date localStorage `paliad.verfahrensablauf.scenario.*` (per-user tweaks that should never leak into a shared link): event_choices, court_id, ccr, inf_amend, rev_amend, rev_cci, show_hidden Hydration order: URL wins. localStorage fills the rest. A shared link reproduces the timeline kind but each user sees their own scenario state. Added trigger_date and proceeding to URL (previously DOM-only — a refresh lost the date and the proceeding tile). Moved event_choices and show_hidden from URL to localStorage (verbose, per-user). Added court_id + flag persistence to localStorage (previously DOM-only). New pure module `views/verfahrensablauf-state.ts` owns the URL + localStorage contract: URL parsers + encoder (`applyFiltersToSearch`), scenario read/write helpers, and a `hydrate()` orchestrator that documents the URL→localStorage order. 31 unit tests pin the contract, including the "shared link doesn't leak scenario state" invariant. Anti-patterns explicitly avoided: - No ?appellant= resurrection (#132 removed it; engine reads from the single side picker for role-swap proceedings). - trigger_date in URL not localStorage (a shared link must reproduce the same dated timeline). - URL→localStorage hydration order is contract; localStorage never overrides an explicit URL value. Project-driven side-fill chip (?project=<id>) still overrides as before — parseSideFromSearch is called before the project's our_side is applied so an explicit ?side= still wins. Build clean: `bun run build`, `bun test` (240 pass / 594 expect calls), `go test ./...`, `go vet ./...`. |
|||
| b97f170c1d |
chore: footer "by" + paliadin diagnostic logs
- Footer: "© 2026 Paliad — ein Werkzeug von / a tool by" → "© 2026 Paliad — by" (both DE + EN). - Paliadin streaming handler now log.Printf on every error path (StreamError, silence_timeout, backend nil/err) so the next "Verbindung verloren" failure produces a server-side trace. Previous behaviour: silent SSE close + empty paliad logs, impossible to diagnose. |
|||
| 446c46e5c5 |
fix(css): repoint 12 var(--color-surface-alt, hex) sites to defined tokens (t-paliad-310, m/paliad#138)
The --color-surface-alt token was never defined in :root or :root[data-theme="dark"], so the var() fallback hex literal always won — leaving 12 surface sites with zero dark-mode treatment. Same pattern as t-paliad-087 / t-paliad-150 / t-paliad-291. Issue #138 surfaced four panels visibly broken in dark mode: 1. submission-draft no-project banner ("Kein Projekt zugeordnet…") — white-on-white 2. submission-draft preview header ("Vorschau / Read-only Vorschau…") — white-on-white 3. smart-timeline rule-chip (e.g. de.null.bpatg.berufung in Vorhersage rows) — grey-on-grey 4. submission-draft addparty manual form (Manuell / Aus DB / Name / …) — white-on-white Eight more latent sites with the same root cause are fixed in the same pass: .submissions-new-chip:hover, .submissions-new-project-item:hover, .submission-draft-import-row, .submission-draft-addparty-search-projref, .collab-invite-hint, .smart-timeline-status-icon, .smart-timeline-kind-chip--projected, .smart-timeline-add-choice:hover. Each site repointed to the semantically correct existing token (--color-surface-2 for #fafafa, --color-surface-muted for #f4f4f4, --color-bg-subtle for #f7f7f0, --color-bg-lime-tint for the lime-tinted collab-invite-hint). All four target tokens are defined in both :root and :root[data-theme="dark"]. No new tokens introduced. Light-mode hex values are functionally identical (#fafafa==#fafafa, #f4f4f4≈#f3f4f6, #f7f7f0≈#f7f3f0). Verified: bun run build clean; Playwright screenshots of the four panels in both light + dark modes show correct rendering. |
|||
| 367627af0d |
fix(verfahrensablauf): appeal side filter + parent in duration label + notes dedup (t-paliad-307, m/paliad#136)
Frontend half of the four Verfahrensablauf appeal bugs. Bug 1 (frontend half) — Side selector dead on appeal. The column bucketer now reads dl.appealRole (engine-stamped under appeal_target) and routes each "both" appeal rule via the user side: side=claimant maps the user to the appellant, so appellant filings land in 'ours' and appellee filings in 'opponent'; side=defendant mirrors. side=null keeps the legacy mirror so every appeal rule renders in both columns (every-rule-visible behaviour the brief calls out). The new appealAware opt gates the path so non-appeal proceedings keep their existing bucketing untouched. Removed upc.apl.unified from APPELLANT_AXIS_PROCEEDINGS — appeal routing is now per-rule via appealRole, not a page-level appellant collapse. Other role-swap proceedings (EPA opp, DE/DPMA appeals) keep the appellant axis since they have no appeal_target metadata. Bug 3 — Duration label appends parent name. formatDurationLabel now takes an optional parent fallback and renders "<n> <unit> <timing> <parent>". deadlineCardHtml resolves the parent per-rule (dl.parentRuleName / EN variant), falling back to opts.trigger EventLabel for root rules with a non-zero duration (e.g. Berufungseinlegung 2 mo. after the Endentscheidung). renderColumns Body + renderTimelineBody auto-derive the trigger event label from the response via the new pickTriggerEventLabel helper unless the caller passes one explicitly. Bug 4 — Duration prefix stripped from deadline_notes. New stripLeadingDurationFromNotes regex peels off leading "Frist N <unit> <vor|nach|ab|seit> …. " (DE) and "<N>-<unit> period from …" / "N <unit> BEFORE …" / "Period is N <unit> from …" (EN) up to the first sentence boundary. Wired into deadlineCardHtml so noteHint + notesBlock both render the deduped text. Per the brief's option (a): conservative regex, composite durations with "ODER" / "whichever is the longer" stay untouched as a follow-up editorial cleanup. deadline_rules DB untouched. Tests: 22 new test cases across appeal-aware bucketing, formatDurationLabel parent append, deadlineCardHtml duration tooltip resolution, and stripLeadingDurationFromNotes regex (positive + negative + composite + EN/DE variants). All 209 frontend tests pass. Engine wire fields added in the preceding commit (AppealRole, IsTriggerEvent). Reads them from CalculatedDeadline without breaking the wire contract for non-appeal callers. |
|||
| 2377f08bd7 | Merge: t-paliad-304 — R.109 anchor + columns-view duplicate fix (topo walk + 'both'→ours collapse) (m/paliad#135) | |||
| 1d704f6e04 |
fix(litigationplanner): R.109.1/R.109.4 mis-anchor + duplicate 'both' row in columns view (t-paliad-304, m/paliad#135)
Two bugs surfaced on /tools/verfahrensablauf?side=defendant for upc.inf.cfi:
1. Anchor regression for timing='before' children of court-set parents.
Rules R.109.1 (translation_request) and R.109.4 (interpreter_cost)
anchor on the oral hearing (parent_id=upc.inf.cfi.oral, IsCourtSet)
but were computing dates BEFORE the Statement of Claim — 1 month
resp. 2 weeks before the SoC instead of before the oral hearing.
Root cause: engine walked rules in sequence_order, and the two
"before"-timed children carry sequence_order 45/46 (their chronological
position, before the oral hearing at 50). Their parent had therefore
not been processed yet when the children were, so courtSet[oral.ID]
was still empty → parentIsCourtSet=false → the engine fell back to
the trigger date as the base.
Fix: walk rules in topological order (parent-first) during the
compute pass, then restore sequence_order on the output slice so
the wire shape and the linear timeline view's render order stay
identical to the legacy behaviour modulo the bug fix.
2. Duplicate "Antrag auf Simultanübersetzung" row in columns view.
With primary_party='both' and an explicit side pick (?side=defendant),
the bucketing mirrored the card into both 'Unsere Seite' and
'Gegnerseite' — the same card on the same row, visible as a
duplicate.
Fix: when the user has committed to a perspective (side picked)
but no appellant axis applies, collapse 'both' rows into ours.
The '↔ beide Seiten' indicator is suppressed in that path to match
the existing appellant-collapse semantics (no sibling row to mirror
to). Legacy mirror behaviour is preserved when side is null.
DB audit ruled out a data-level duplicate: exactly one published+active
row per submission_code in paliad.deadline_rules.
Tests:
- pkg/litigationplanner/before_court_set_anchor_test.go: synthetic
rules pinning the conditional-on-court-set-parent contract plus
the override path (1mo before user-pinned oral).
- frontend/src/client/views/verfahrensablauf-core.test.ts: two new
cases pinning the side-collapse routing for party='both'.
|
|||
| a75731a902 | Merge: t-paliad-302 — Verfahrensablauf duration indicator (hover + toggle, +3 lp.TimelineEntry fields) (m/paliad#133) | |||
| 3097df3918 |
mAi: #133 — Verfahrensablauf duration affordance (hover + toggle)
t-paliad-302 / m/paliad#133. Surface each event card's rule duration ("2 Mo. nach") on /tools/verfahrensablauf — by default as a hover tooltip on the date span, and optionally inline via a new "Dauern anzeigen" header toggle (localStorage key paliad.verfahrensablauf.durations-show). The issue scoped this as pure-frontend on the assumption that the duration fields were already on the /api/tools/fristenrechner payload. They were not: lp.TimelineEntry exposed only the computed dueDate, not the rule's (duration_value, duration_unit, timing) tuple. Added these as three additive optional fields and populated them in both engine emission sites (Calculate + CalculateByTriggerEvent) from the rule row directly. Source values are the base rule fields, not the post-alt-swap arithmetic — the tooltip reads as a property of the rule rather than a recap of which branch fired. Frontend wiring: - formatDurationLabel() in verfahrensablauf-core builds the "<value> <unit> <timing>" string from the existing deadlines.event.unit.<unit>.{one,many} + deadlines.event.timing.* i18n keys, reused from /tools/fristenrechner's event-mode renderer. - deadlineCardHtml attaches the label as title= on the date span (hover, default) and, when CardOpts.showDurations is on, emits an inline <span class="timeline-duration"> in the meta row. - Court-set / zero-duration rules (trigger event, hearings) skip the affordance — durationValue <= 0 short-circuits in formatDurationLabel. - Toggle persisted in localStorage under paliad.verfahrensablauf.durations-show, default off; sits next to the existing "Hinweise anzeigen" toggle. bun run build clean, go test ./pkg/litigationplanner/... and ./internal/... clean, bun test src/client/views clean (89/89). |
|||
| 9da4715137 |
feat(litigationplanner): Berufung tile UX — collapse side selectors + appeal-target trigger label (t-paliad-301, m/paliad#132)
Two bugs from the Slice B1 Berufung rollout, one fix surface:
Bug A — duplicate side selectors collapse into ONE proactive-side
picker with per-proceeding role labels. The Verfahrensablauf used to
show both ?side= (Klägerseite/Beklagtenseite) AND ?appellant= (same
labels in case-form) on the Berufung tile. Now: one side picker, with
labels that swap to Berufungskläger/Berufungsbeklagter on the unified
upc.apl.unified tile (and Antragsteller/Antragsgegner Nichtigkeit on
upc.rev.cfi, Einsprechende(r)/Patentinhaber(in) on epa.opp.*).
Bug B — 'Auslösendes Ereignis' label derives from appeal_target on
the unified Berufung tile (5 target-specific strings) instead of the
proceeding's own trigger_event_label. Endentscheidung (R.118) /
Kostenentscheidung / Anordnung / Entscheidung im
Schadensbemessungsverfahren / Anordnung der Bucheinsicht.
Migration 137 (additive, no triggers on proceeding_types — verified
via mcp__supabase__execute_sql before drafting; no updated_at on the
table — lesson from mig 134 HOTFIX 3; no audit_reason setup needed):
- ADD COLUMN role_proactive_label_de (text NULL)
- ADD COLUMN role_proactive_label_en (text NULL)
- ADD COLUMN role_reactive_label_de (text NULL)
- ADD COLUMN role_reactive_label_en (text NULL)
- Audit-first DO block lists the rows the UPDATE will touch.
- Backfill 4 proceedings (upc.apl.unified + upc.rev.cfi +
epa.opp.opd + epa.opp.boa); every other proceeding stays NULL
and the renderer falls back to default labels.
- Down drops the 4 columns.
Package additions (pkg/litigationplanner):
- ProceedingType gains 4 *string fields (RoleProactive/Reactive
LabelDE/EN) — db tags match the new columns; existing scans pick
them up via the proceedingTypeColumns extension.
- TriggerEventLabelForAppealTarget(target, lang) — Go-side map of
the 5 appeal-target slugs to their DE/EN trigger-event labels.
Empty result on unknown target signals "fall back to proceeding's
own trigger_event_label".
- Engine override: when CalcOptions.AppealTarget is set, the
resulting Timeline.TriggerEventLabel/EN are replaced from the
per-target map.
Frontend:
- Removed #appellant-row div (was a separate 3-radio selector
duplicating side).
- Dropped ?appellant= URL state + the change handler + the init
readback. The engine still consumes "appellant" — sourced from
currentSide for role-swap proceedings; null otherwise.
- applyRoleLabels(proceedingType) swaps the side-row radio labels
from a hardcoded ROLE_LABELS map mirroring mig 137's backfill.
Falls back to deadlines.side.claimant/defendant i18n keys for
proceedings without overrides.
- syncTriggerEventLabel reads data.triggerEventLabel from the calc
response — which the engine override now sets per appeal_target,
so no client-side mapping needed.
- i18n cleanup: removed orphan deadlines.appellant.* keys (label /
claimant / defendant / none) in both DE + EN.
Tests:
- pkg/litigationplanner/appeal_target_label_test.go pins the 5×2
label matrix + a coverage test that fails if a new entry in
AppealTargets is added without populating the label switch.
Acceptance:
- go build + go test all green (incl. new lp test).
- bun run build clean (i18n codegen drops 4 keys, regenerates).
- Live-DB audit before drafting confirmed: 4 target columns don't
exist on proceeding_types, zero triggers on the table, exact
column inventory matches the design.
|
|||
| e2d75c391d |
fix(litigationplanner): rename upc.apl → upc.apl.unified (HOTFIX, t-paliad-299, m/paliad#130)
mig 134 was inserting code='upc.apl' (2 segments) into paliad.proceeding_types, which carries paliad_proceeding_code_shape CHECK requiring 3 dot-segments OR '^_archived_'. Every container restart hit the constraint, rolled the migration TXN back, and crash-looped paliad.de. Rename the unified Berufung code to 'upc.apl.unified' (3 segments, satisfies the constraint, preserves design intent). The pre-existing constraint is a useful jurisdiction.category.specific invariant — keep it, fix the new row. Touched only string literals: - mig 134 up.sql + down.sql (insert, lookups, post-checks) - frontend/src/verfahrensablauf.tsx (UPC_TYPES code + i18nKey) - frontend/src/client/verfahrensablauf.ts (APPELLANT_AXIS + APPEAL_TARGET sets) - frontend/src/client/i18n.ts (DE + EN translation rows) - frontend/src/i18n-keys.ts (auto-regen via bun build) - internal/services/lookup_events_test.go (anchor-row assertion) Verified: `grep -rn "'upc\.apl'\|\"upc\.apl\""` returns zero hits. go build, bun run build, go test ./... all green. |
|||
| 07acf7b4a2 |
feat(litigationplanner): Berufung unification — one upc.apl + 5 appeal_target chips (Slice B1, m/paliad#124 §18.1)
Collapses the 3 UPC appeal proceeding_types (upc.apl.merits 7 rules,
upc.apl.cost 2, upc.apl.order 7 = 16 total across 3 codes) into ONE
unified upc.apl proceeding type + a per-rule applies_to_target[]
discriminator. The verfahrensablauf picker now shows one "Berufung"
tile; after picking it, the user selects which decision the appeal is
directed AT via a 5-chip group (Endentscheidung / Kostenentscheidung /
Anordnung / Schadensbemessung / Bucheinsicht) and the engine filters
rules whose applies_to_target contains the picked slug.
m's 2026-05-26 decision: Schadensbemessung-as-appeal is a NEW first-
class target with its OWN rule set (no shared inheritance from
merits). The 5 enum values are all defined + addressable; for now
schadensbemessung and bucheinsicht return empty timelines until rules
are seeded in a follow-up slice (likely via /admin/rules or pairing
with t-paliad-193 orphan-concept-seed).
Migration 134 (additive only):
- ADD proceeding_types.appeal_target text (CHECK on 5 slugs OR NULL)
- ADD deadline_rules.applies_to_target text[] (CHECK each element
in the 5 slugs)
- INSERT the unified upc.apl row (inherits sort/color from
upc.apl.merits)
- Audit-first RAISE NOTICE pass listing every row about to be
touched + a post-migration sanity check
- Reassign rule rows: merits → applies_to_target={endentscheidung},
cost → {kostenentscheidung}, order → {anordnung}
- Archive (is_active=false, NOT DELETE) the 3 old proceeding_types
so historical FKs stay intact
- Down migration restores is_active=true on the 3 old types, points
rules back by their applies_to_target stamp, drops the unified
row, drops both columns. Safe.
Package additions (pkg/litigationplanner):
- AppealTarget* constants + AppealTargets[] ordered list +
IsValidAppealTarget(s) predicate (silent no-op on unknown slugs
so a stale frontend chip doesn't break the render)
- ProceedingType.AppealTarget *string field (top-level marker;
NULL on non-appeal proceedings)
- Rule.AppliesToTarget pq.StringArray field (per-row applies-to set)
- CalcOptions.AppealTarget string (engine filter — when set,
keeps only rules whose AppliesToTarget contains the slug)
Engine filter runs after ApplyRuleOverrides but before the rule walk
so the existing condition_expr / spawn / appellant-context machinery
operates on the filtered subset transparently.
paliad-side wiring:
- deadline_rule_service.go: ruleColumns + proceedingTypeColumns
extended to scan the new columns
- handlers/fristenrechner.go: AppealTarget JSON field on the
request payload, threaded into CalcOptions
Frontend (verfahrensablauf surface only):
- Single "Berufung" tile replaces the 3 separate Berufung tiles
- New 5-chip appeal-target row, shown only when upc.apl is picked
- URL state ?target=<slug>; default endentscheidung when none set
- APPELLANT_AXIS_PROCEEDINGS updated: upc.apl.* (3 entries) →
upc.apl (1 entry)
- i18n keys (DE + EN) for the new tile + the 5 chip labels +
the "Worauf richtet sich die Berufung?" / "Appeal against:" prompt
- calculateDeadlines threads appealTarget through to the API
Acceptance:
- go build clean, go test all green (existing test suite — no new
tests on the engine filter as a follow-up; the migration's
sanity-check DO block guards the rule-reassignment count)
- Live audit before drafting confirmed: 3 active UPC appeal
proceeding_types, 16 rules total, primary_party already conforms
to 4-value vocab on all proceeding-bound rules
|
|||
| 593e6243e0 | Merge: t-paliad-295 — side-aware Verfahrensablauf column headers (Proaktiv/Reaktiv ↔ Unsere/Gegenseite) (m/paliad#127) |