m's spec lock §10 Q1 (2026-05-05): "Retire legacy tabs - we are only
resorting." This commit drops the .fristen-mode-tabs nav (Verfahrensablauf
+ Was kommt nach…) and the ?legacy=1 escape hatch. Pathway A becomes
Verfahrensablauf-only; the trigger-event panel (mode-event-panel) stays
in the DOM but is hidden by default and surfaces only via concept-card
pill drill-in (drillToTrigger flips the panels directly).
Frontend deltas:
- frontend/src/fristenrechner.tsx: drop .fristen-mode-tabs section;
rename mode-event-panel role/label to standalone tabpanel.
- frontend/src/client/fristenrechner.ts:
- drop isLegacyMode() + ?legacy=1 branch in showPathway().
- drillToTrigger() now flips procedure ↔ event panels directly
(no more #mode-event-tab click → handler chain).
- initModeTabs() bails on tabs.length===0 (already does); no
further changes needed.
- frontend/src/styles/global.css: drop .fristen-pathway-shell--legacy.
Backend untouched.
Build: clean. Frontend bundle 1473 keys unchanged. go build + vet +
tests pass.
The deadlines.mode.procedure / deadlines.mode.event i18n keys remain
in i18n.ts as orphans for now; cleaning them up is purely cosmetic
and lives outside the v3 scope.
Wires the v3 Gericht/System multi-select filter on the Pathway B/B2
panel. 10 forum-bucket chips per m's spec lock §10 Q8 (UPC CFI, UPC
CoA, DE LG/OLG/BGH/BPatG, EPA Erteilung/Einspruchsabt./Beschwerdek.,
DPMA).
UX:
- Chip click toggles its membership in activeForums Set.
- Multi-select; chips AND across the result set
(UNION within forum, AND with other filters — backend handles).
- ?forum=<comma-separated> URL state round-trips on every toggle.
- popstate restores active set; lang switch re-renders chip labels.
- Shared between B1 and B2: tree-mode reissues runB1Search;
filter-mode dispatches input event on the search box.
Frontend file deltas:
- frontend/src/client/fristenrechner.ts: FORUM_BUCKETS array,
activeForums Set, renderForumChips(), reissueSearchWithCurrentFilters()
(mode-aware), getActiveForumsParam() consumed at every search call.
- B2 search fetch + B1 cascade fetch both send ?forum= when active.
Frontend i18n keys for the 10 forum labels (DE+EN) shipped with
Phase B; this commit just renders them.
Backend was wired in Phase C; this commit completes the user-facing
path. Forum filter narrowing applies AND-wise with q / event_category_slug
/ proc / party / source — empty-result UX shows the existing "no hits"
status, m can drop a chip to widen.
Build: clean. Frontend bundle unchanged size delta (≈+50 lines, 1473 keys).
Phase D-2 (party-perspective selector + is_bilateral mirroring renderer)
ships next.
Reshapes /tools/fristenrechner into the v3 landing fork. Default
view: two big pathway cards (📖 Verfahrensablauf informieren
vs 📅 Frist eintragen aufgrund Ereignis) plus a quick-pick chip
shortcut row that jumps straight into Pathway B + filter mode +
prefilled query.
URL state machine:
- ?path=a → Pathway A (existing wizard, wrapped in fristen-pathway-a)
- ?path=b → Pathway B shell with mode toggle (B1 tree / B2 filter)
- ?mode=tree → B1 panel (stub for Phase B; Phase C wires the cascade)
- ?mode=filter → B2 panel (search bar + chips + concept-card results)
- ?path absent → landing fork
- ?legacy=1 → pre-v3 layout (legacy escape hatch; dropped in Phase E)
- localStorage remembers last-used pathway
Pathway B's B2 panel hosts the existing Phase D search bar (relocated
from page-top into the pathway shell). The forum-filter row + chips
container exist in the DOM hidden — Phase D wires them.
Pathway A wraps the existing Verfahrensablauf wizard (proceeding tile
grid + date input + timeline / columns view) plus the legacy "Was
kommt nach…" tab. Both keep working unchanged in this commit; tabs
retire entirely in Phase E.
Phase B B1 panel is a stub: "Der Entscheidungsbaum ist in Vorbereitung."
Phase C replaces it with the data-driven cascade.
Files:
- frontend/src/fristenrechner.tsx: landing fork + pathway shells
- frontend/src/client/fristenrechner.ts: pathway state machine,
URL parser, popstate restore, fork-chip → ?path=b shortcut
- frontend/src/client/i18n.ts: 30+ new keys (deadlines.pathway.*,
deadlines.filter.forum.*, deadlines.perspective.*) DE+EN
- frontend/src/styles/global.css: .fristen-pathway-fork,
.fristen-pathway-card, .fristen-pathway-shell, .fristen-mode-toggle,
.fristen-forum-filter, .fristen-forum-chip rules
Frontend build: clean (1472 i18n keys). go build + vet: clean.
The legacy tabs (Verfahrensablauf-Tab + Was kommt nach…) live inside
Pathway A and continue to work — m's spec lock §10 Q1 retires them
in Phase E, not now.
Backend layer for the v3 decision tree:
- internal/services/event_category_service.go (NEW)
- Tree(): nested tree of all active event_categories for the
Pathway B / B1 cascade UI. Uses single SELECT + in-memory
parent-child stitching; corpus is small (≤100 nodes).
- ConceptsForSlug(): recursive CTE walks descendants of a slug and
joins event_category_concepts to return the candidate concept
outcomes (with optional proceeding_type_code narrowing).
- ConceptIDsForSlug(): convenience reduction for
`WHERE concept_id = ANY(...)` queries against the existing
deadline_search matview.
- ProceedingCodesForSlug(): per-leaf proceeding-code narrowing for
Phase D's forum filter intersection.
- internal/handlers/fristenrechner_event_categories.go (NEW)
- GET /api/tools/fristenrechner/event-categories returning the
nested tree as JSON. Frontend will ETag-cache via localStorage.
- Wired EventCategory into handlers.Services + dbServices + main.go.
The existing /api/tools/fristenrechner/search handler stays
unchanged in this commit; Phase D will add ?event_category_slug=
and ?forum= query params on top.
Build + vet clean.
Three migrations land the data layer for the Fristenrechner v3 decision
tree (Pathway B / B1) plus the bilateral-rule flag for the new party-
perspective selector. All purely additive — no breaking changes to the
v2 (t-paliad-131) corpus.
Migration 048 — schema:
- paliad.event_categories: recursive taxonomy tree (parent_id self-FK,
unique slug as materialised dot-path, step_question_de/en on internal
nodes, is_leaf bool, optional emoji icon).
- paliad.event_category_concepts: many-to-many junction (leaf →
deadline_concepts) with optional proceeding_type_code narrowing.
UNIQUE NULLS NOT DISTINCT prevents duplicate (leaf, concept, NULL)
rows (PG 15+).
- paliad.deadline_rules.is_bilateral bool: when true AND
primary_party='both', the rule mirrors into both party columns of
the v3 columns view; otherwise 'both' resolves single-side via the
perspective selector.
Migration 049 — seed taxonomy:
6 root buckets (cms-eingang, muendl-verhandlung, beschluss-entscheidung,
frist-verpasst, ich-moechte-einreichen, sonstiges) with 70+ leaves and
115+ junction rows. Tree depth reaches 4 today (cms-eingang › gericht
› endentscheidung › <leaf>) but the schema supports unlimited depth
per design lock §10 Q2. Coverage gate at the end raises if any
category='submission' concept is unreachable from a leaf, except the
3 pure-administrative slugs (filing, request-for-examination,
approval-and-translation) that live on Pathway A only.
Migration 050 — bilateral backfill:
Tags exactly 4 genuinely-bilateral rules:
- de_null.stellungnahme (Stellungnahme zum Hinweisbeschluss, PatG §83.2)
- epa_opp.r79_further (Stellungnahme weiterer Beteiligter)
- epa_opp.r116, epa_app.r116 (Eingaben vor mündl. Verhandlung)
All other primary_party='both' rules (Berufungsfristen, Anschlussberufung,
…) are role-swap appeals that resolve via the perspective selector at
render time.
Schema dry-run validated end-to-end against Supabase PG 15.8.
Design ref: docs/plans/unified-fristenrechner-v3.md §4.1 + §10 Q12.
m approved all 12 open questions in one batch. Locked spec:
1. Legacy tabs RETIRED in Phase E.
2. Decision-tree depth UNLIMITED (was: 4 max). Property of
event_categories data, not hard-coded.
3. Clickable breadcrumb for navigation.
4. Partial-path bookmarks (?b1=...).
5. Multi-select forum filter, default 1 selected.
6. Path-matching cards at each step. Renamed "Pfad lockern" →
"Schritt zurück".
7. Emojis only, no separate colour treatment.
8. Forum buckets simplified to 10: UPC CFI + UPC CoA + DE LG/OLG/
BGH/BPatG + EPA Erteilung/Einspruchsabt./Beschwerdek. + DPMA.
m collapsed UPC LD/CD into UPC CFI (rules identical).
9. B1↔B2 share filter state.
10. Single branch / sequential commits / one final merge.
11. Party perspective default Claimant/Proactive; localStorage
remembers last-used. URL ?my_side= + ?appeal_filed_by=.
12. Bilateral rules tagged via new is_bilateral column on
deadline_rules; mirroring only when flagged.
Maria's two scope additions folded in:
- Court-system granularity for forum filter (clarification).
- Party-perspective selector absorbing t-paliad-132.
Implementation now starting on this branch.
m's 2026-05-05 brief restructures the page surface that v2 (t-paliad-131)
shipped. The current Fristenrechner stacks three blurred entrypoints —
Phase D search bar, Verfahrensablauf tile grid, "Was kommt nach…" tab.
v3 forks the page so each mental model has its own entry:
- Pathway A — Verfahrensablauf informieren (Browse): existing wizard.
- Pathway B — Frist eintragen aufgrund Ereignis (Event → Deadline),
subdivided into:
- B1 Entscheidungsbaum: data-driven button cascade (CMS-Eingang →
Vom Gericht → Hinweisbeschluss → cards), max 4 deep, back +
breadcrumb + bookmark URLs.
- B2 Filter / Suche: Phase D concept-card search PLUS new
Gericht/System multi-select chip filter (Q8 reversal). All filters
AND-narrow.
Adds two new tables (Phase A — purely additive):
- paliad.event_categories — recursive taxonomy tree, with step
questions on non-leaf nodes.
- paliad.event_category_concepts — leaf → concept junction with
optional proceeding_type_code narrowing.
Existing data layer (deadline_concepts, deadline_rules, trigger_events,
deadline_search matview) untouched. Phase D search handler gains
?event_category_slug= and ?forum= query params; forum-bucket map lives
in Go (UPC / DE LG / DE OLG / DE BGH / DE BPatG / EPA / DPMA).
Phasing: A (data) → B (landing fork) → C (B1 tree) → D (B2 forum
filter) → E (retire legacy tabs, gate-gated). Each phase independently
shippable.
Open questions for m at §10: retire legacy tabs, decision-tree depth,
back/breadcrumb, partial-path bookmarks, multi vs single-select forum,
all-vs-path-matching cards per step, austere icons, 7 forum buckets,
B1↔B2 state-sharing, PR phasing.
Inventor parked. Next: m's go/no-go before coder shift.
Cross-references docs/plans/unified-fristenrechner.md (v2, shipped) for
concept-layer / search-backend / coverage details v3 inherits unchanged.
Closes the user-facing half of the unified Fristenrechner. The proceeding
tile grid + the two existing modes (Verfahrensablauf / Was kommt nach…)
stay in place per m's "augment, not replace" — the search bar lives
above them and drills *into* either mode pre-selected.
frontend/src/fristenrechner.tsx:
- New search section above the mode tabs:
• search input with magnifier icon and clear (✕) button
• 8 quick-pick chips per design Q8 (Klageerwiderung · Berufung ·
Einspruch · Replik · Beschwerde · Statement of Defence ·
Schadensbemessung · Wiedereinsetzung)
• #fristen-search-results container the client renders cards into
- i18n keys live in deadlines.search.* with DE primary / EN mirror.
frontend/src/client/fristenrechner.ts:
- Search subsystem with the same debounce-and-sequence-counter pattern
the existing event-mode and procedure-mode calc paths use.
- GET /api/tools/fristenrechner/search?q=…&limit=12 with same-origin
credentials. Empty q clears results; failures fall back to the
"no hits" placeholder.
- Concept card layout: name + alt-language name, optional description,
"auch bekannt als" line for matched aliases, and one pill per
(proceeding × rule). Cross-cutting trigger pills (Wiedereinsetzung,
Versäumnisurteil, Schriftsatznachreichung, Weiterbehandlung) render
in a separate pills section labelled "Verfahrensübergreifend:".
- Pills are <a href="…drill_url"> elements so middle-click / cmd-click
opens in a new tab; the JS click handler intercepts plain clicks
and drills client-side:
• rule pill → activate procedure mode tab + selectProceeding(code)
+ pendingFocus(rule_local_code) so the next
renderProcedureResults scrolls to and pulses the
focused row (.fristen-focus-highlight, 2.4 s ease).
• trigger pill → activate event mode tab + selectTriggerEvent(id).
- URL state on ?q=… via history.replaceState; popstate restores.
Initial load reads ?q= from the URL so /tools/fristenrechner?q=foo
shareable links work.
- onLangChange re-fires the search so card / pill labels follow the
active locale (matches the existing onLangChange wiring for
procedure + event results).
frontend/src/styles/global.css:
- .fristen-search input + .fristen-search-chip + .fristen-search-icon
(magnifier inset 14px from the left, search-input padded 2.6rem
on the left to clear it).
- .fristen-card / .fristen-pill grid layout with party badges in the
project's existing accent palette (claimant blue, defendant red,
both grey, court amber). Mobile @media collapses the pill grid
to a 2-column shape so legal_source + duration stack cleanly.
- .fristen-focus-highlight keyframes for the post-drill pulse.
Out of scope for this shift (deferred):
- "Vollständige Instanzenkette" toggle (design Q5). The toggle is a
multi-stage timeline render that calls Calculate independently per
stage with one date input per stage anchor — a calculator-side
feature, not the search bar. Will land as a follow-up phase.
- Columns-view sequence preservation for undated court-set events
(design §7 "Out of scope — separate task" note). Already flagged
as a separate task to file.
Validation: `bun run build` clean (1443 i18n keys, no orphans);
`go build ./... && go vet ./... && go test ./internal/...` green
across all packages. The dist bundles confirm the new symbols
landed in fristenrechner.js (search wiring), global.css (48 hits on
new selectors), and fristenrechner.html (9 unique fristen-search-*
classes). Live browser verification with auth happens after merge —
the route is auth-gated and the playwright profile is held by
another process, so a static smoke test against the dist HTML
isn't representative of the rendered authenticated page.
Closes the search half of the unified Fristenrechner. Phase D (concept-card
UI on /tools/fristenrechner) follows in a subsequent shift.
Migration 047:
- Seed the missing `wiedereinsetzung` concept and re-point the four
Wiedereinsetzung trigger_events (200..203) at it. PR-7 referenced
the slug `re-establishment-of-rights` but never seeded the concept,
so the four cross-cutting triggers were dropping out of any concept-
JOINing query. Per m's slug rule (Q1: shared cross-cutting concepts
use DE slug because German term dominates HLC vocabulary).
- Create paliad.deadline_search materialised view: UNION ALL of
(deadline_rules joined to deadline_concepts) and (trigger_events
joined to deadline_concepts via slug). Trigram GIN indexes on
legal_source / concept_name_de / concept_name_en / rule_name_de /
rule_name_en / rule_code; gin (concept_aliases) for array
containment; UNIQUE INDEX on a synthetic row_key so refresh can
run CONCURRENTLY.
Refresh strategy: data only mutates via migration files at server
startup, so no AFTER triggers and no pg_cron — main.go calls
services.RefreshSearchView right after db.ApplyMigrations. CONCURRENTLY
keeps reads online and stays well under 100 ms at < 1k rows.
Service `internal/services/deadline_search_service.go`:
- Two-query pipeline per request: (1) rank concept_ids by
GREATEST(similarity()) across name / aliases / legal_source / rule_code
plus a 0.2 alias-hit boost; (2) load all matview rows for the top-N
concepts and assemble per-pill JSON.
- normalizeQuery strips legal-prefix noise (`§`, `Art.`, `Section`,
`Rule `) so users typing `§ 82` find DE.PatG.82.1 even though the
structured legal_source column doesn't carry the prefix.
- FormatLegalSourceDisplay renders structured codes back to the
pleading form HLC users expect:
UPC.RoP.23.1 → "UPC RoP R.23(1)"
DE.PatG.82.1 → "PatG §82(1)"
EU.EPÜ.108 → "EPÜ Art.108"
EU.EPC-R.79.1 → "EPC R.79(1)"
EU.RPBA.12.1.c → "RPBA Art.12(1)(c)"
- Drill URLs route per kind: rule pills → ?proc=…&focus=…, trigger
pills → ?mode=event&triggerId=…
Handler `GET /api/tools/fristenrechner/search?q=&party=&proc=&source=&limit=`:
- Returns the JSON shape from design §6.1 (cards-with-pills).
- 503 with friendly DE message when DATABASE_URL is unset, mirroring
the other Fristenrechner endpoints.
- Empty q returns an empty cards array (browse surface is Phase D).
Tests:
- Pure-Go: TestFormatLegalSourceDisplay (12 cases across all known
prefixes) + TestNormalizeQuery (8 cases).
- Integration (skipped without TEST_DATABASE_URL): golden table
pinning the design's binding queries — Klageerwiderung returns the
statement-of-defence card with UPC.RoP.23.1, DE.ZPO.276.1,
DE.PatG.82.1, EU.EPC-R.79.1, DE.PatG.59.3 pills; "RoP 23" returns
the same card; "§ 82" → normalized "82" → BPatG hit; Wiedereinsetzung
returns one card with exactly 4 trigger pills (ids 200..203);
party / source filters narrow as expected; limit cap honoured.
- SQL semantics validated against live data via supabase MCP using a
CTE-inlined matview definition with the slug fix simulated; results
match the golden table.
Per design doc `docs/plans/unified-fristenrechner.md` §4.6 (matview
shape) + §6 (search ranking + API).
PR-7 of the Unified Fristenrechner. Final Phase B migration. Closes
all named cross-procedural deadline gaps in the design.
These concepts fire across many proceedings (any patent application,
any civil case, any opposition, any appeal) and don't naturally belong
to one proceeding-tree timeline. Modelled per design §5.2.4 + §5.3 as
event-trigger-only entries: the user picks the trigger ("the moment
the obstacle was removed", "the date the Versäumnisurteil was served")
and the calculator returns the deadline.
Migration 046 adds 7 trigger_events (ids 200–206, paliad-native space
above the youpc-imported 1–114 range so future resync stays clean) and
7 corresponding event_deadlines + 3 new concepts.
WIEDEREINSETZUNG IN 4 LEGAL CONTEXTS (one shared concept slug
re-establishment-of-rights, seeded in PR-1):
- PatG §123(2): trigger 200, 2 months / max 1 year
- ZPO §234(1): trigger 201, **2 WEEKS** / max 1 year
← critical distinction; the 2-weeks-not-months ZPO
case is the most-confused detail of DE
Wiedereinsetzung. notes_de explicitly capitalises
"WOCHEN" so the user reads it before computing.
- EPC Art.122 + R.136(1): trigger 202, 2 months / max 12 months
- DPMA via PatG §123: trigger 203, 2 months / max 1 year
OTHER CROSS-CUTTING:
- Versäumnisurteil-Einspruch (ZPO §339): trigger 204, 2 weeks
Notfrist — keine Verlängerung möglich.
- Schriftsatznachreichung (ZPO §296a): trigger 205, 3 weeks
(court-set typical; placeholder the user can adjust via
click-to-edit if the court actually set a different period)
- Weiterbehandlung (Art.121 EPÜ + R.135): trigger 206, 2 months
Distinct from Wiedereinsetzung — niedrigere Gebühr, applies
BEFORE final loss of rights.
Three new concepts (slug naming per design §4.4):
- versaeumnisurteil-einspruch (DE-only procedure → DE slug)
- schriftsatznachreichung (DE-only → DE slug)
- weiterbehandlung (EPC-native + DE term dominates HLC vocab → DE slug)
Live-verified all 7 trigger_events on paliad.de (tester@hlc.de) via
the existing /tools/fristenrechner "Was kommt nach…" mode:
trigger 200 → 2026-07-06 (2mo PatG, weekend-shift)
trigger 201 → 2026-05-18 (2 WEEKS ZPO — the critical case)
trigger 204 → 2026-05-18 (2 weeks ZPO §339)
trigger 205 → 2026-05-26 (3 weeks ZPO §296a)
trigger 206 → 2026-07-06 (2mo EPC weiterbehandlung)
Out of scope (no calculator-relevant deadlines, would just be search
clutter): Mahnverfahren-Widerspruch (ZPO §345), Validierungsfristen
national (Art. 65 EPÜ → varies per state), Teilanmeldung (R.36 EPC →
"until end of pending parent" is anchor-on-revocation-of-grant).
Phase B is now complete. Phase C (search backend) + Phase D (concept-
card UI) follow per design.
PR-6 of the Unified Fristenrechner. Fills the EPA-side coverage gaps
named in the design + repairs three pre-existing EPA bugs surfaced
during this work.
Migration 045:
PRE-EXISTING BUG FIXES
1. EPA anchor convention bug. epa_opp.grant and epa_app.entsch were
seeded with party='court' + event_type='decision' → calculator's
isCourtDeterminedRule(r) returned true → those anchor rows
rendered as IsCourtSet (no date), propagating IsCourtSet to every
downstream rule that chained off them. Result on prod: EPA_OPP
showed "court-set" for Einspruchsfrist / Erwiderung / Entscheidung
instead of computed dates; ONLY the trailing beschwerde + begr
rendered dates (and only by accident, because they had parent_id=
NULL and computed off triggerDate directly).
Fix: changed both anchors to party='both' + event_type='filing' so
they render as IsRootEvent. Matches the convention I established
for DE_INF_OLG / DE_INF_BGH / DE_NULL_BGH / DPMA_BPATG_BESCHWERDE /
DPMA_BGH_RB anchors in PR-3/4/5.
2. EPA_OPP appeal-phase parent bug. epa_opp.beschwerde +
beschwerde_begr had parent_id=NULL → were computing 2mo and 4mo
from the GRANT date instead of from the OPPOSITION DECISION date.
Re-parented both on epa_opp.entsch. They now correctly render as
IsCourtSet placeholders (because entsch is court-set) until the
user enters the real decision date via the Phase A click-to-edit
affordance.
3. EPA_APP.erwidg modelling bug. Was parent_id=NULL + duration=0 +
party=both + event=filing → IsRootEvent → emitted the trigger date
as "Erwiderung". Now properly modelled per Art. 12(1)(c) RPBA 2020:
parent=epa_app.begr, duration=4 months, name="Beschwerdeerwiderung",
legal_source=EU.RPBA.12.1.c, response-to-appeal concept.
NEW COVERAGE (per design §5.3)
EPA_OPP gains 2 rules:
- epa_opp.r79_further: Stellungnahme weiterer Beteiligter
(R.79(2)/(3) EPC) — court-set, parent=erwidg
- epa_opp.r116: Eingaben vor mündl. Verhandlung
(R.116(1) EPC) — court-set, parent=entsch (so it surfaces in the
opposition phase but stays IsCourtSet until oral hearing date is
entered via override)
EPA_APP gains 2 rules:
- epa_app.r116: Eingaben vor mündl. Verhandlung
(R.116(1) EPC + Art. 13 RPBA) — court-set, parent=oral
- epa_app.r106: Antrag auf Überprüfung
(Art. 112a EPÜ) — 2 months from service of decision, parent=
entsch2 (the BoA decision)
Three new EN-slug concepts (UPC/EPC-native): r79-further-stellungnahme,
r116-final-submissions, petition-for-review.
Live-verified on paliad.de:
EPA_OPP trigger 2026-05-04 → grant IsRootEvent / Einspruchsfrist
2027-02-04 (9mo) / Erwiderung 2027-06-04 (4mo from frist) /
r79_further 2027-06-04 (filed-with-erwidg) / Entscheidung +
Beschwerde + Begründung + r116 IsCourtSet (waiting for entsch).
EPA_APP trigger 2026-05-04 → entsch IsRootEvent / Beschwerde
2026-07-06 (2mo, weekend-shift) / Begründung 2026-09-04 (4mo from
entsch) / Beschwerdeerwiderung 2027-01-04 (4mo from Begründung
per RPBA 12.1.c) / r116 IsCourtSet (parent=oral) / r106 IsCourtSet
(parent=entsch2, will compute 2mo from BoA decision once entered).
Out of scope (deferred to PR-7 cross-cutting): Wiedereinsetzung
(Art. 122 EPÜ + R.136 EPC), Weiterbehandlung (Art. 121 EPÜ + R.135 EPC),
Validierungsfrist national (Art. 65 EPÜ).
PR-5 of the Unified Fristenrechner. Three new proceeding types
covering the DPMA → BPatG → BGH opposition / appeal chain. Closes the
DPMA gap m named — paliad has had zero DPMA-specific timelines until
now (DPMA-granted patents in Nichtigkeit went to DE_NULL but the DPMA
opposition + Beschwerde + Rechtsbeschwerde chain had no home).
Migration 044 adds:
- DPMA_OPP (Einspruch DPMA, sort=310): 4 rules. Anchor "Veröffentlichung
der Erteilung" + Einspruchsfrist (PatG §59.1, 9mo) + Erwiderung
Patentinhaber (PatG §59.3, court-set ~4mo, party=defendant) +
DPMA-Entscheidung (court).
- DPMA_BPATG_BESCHWERDE (Beschwerde BPatG, sort=320): 5 rules. Anchor
"Zustellung DPMA-Entscheidung" + Beschwerde (PatG §73.2, 1mo) +
Beschwerdebegründung (PatG §75.1, 1mo from filing, extension on
request) + mündliche Verhandlung + BPatG-Entscheidung.
- DPMA_BGH_RB (Rechtsbeschwerde BGH, sort=330): 4 rules. Anchor
"Zustellung BPatG-Entscheidung" + Rechtsbeschwerde (PatG §100.1, 1mo)
+ Begründung (PatG §102 i.V.m. ZPO §551, 1mo from filing) +
BGH-Entscheidung.
Naming note: head's PR brief listed the third type as
"DPMA_BPATG_NICHTIGKEIT" but Nichtigkeitsklage is filed directly at
BPatG (already covered by DE_NULL — never chained off DPMA). The
natural BGH endpoint of the DPMA chain is the Rechtsbeschwerde per
§§ 100/102 PatG. Using DPMA_BGH_RB; trivially renamable if head
intended a different shape.
Two new DE-only concepts: rechtsbeschwerde (BGH legal appeal — DE-
specific procedure, no UPC/EPC equivalent), rechtsbeschwerde-
begruendung. Other rules reuse shared concepts (publication,
opposition, statement-of-defence, notice-of-appeal, statement-of-
grounds-of-appeal, oral-hearing, decision).
Frontend: new DPMA tile group in /tools/fristenrechner with 3 tiles,
positioned after the EPA group. 5 new i18n keys (DE+EN: deadlines.dpma
group label + 3 tile names + tile labels for 3 procs).
Live-verified all 3 trees on paliad.de (tester@hlc.de):
DPMA_OPP trigger 2026-05-04 → Einspruch 2027-02-04 (9mo) /
Erwiderung 2027-06-04 (4mo from Einspruch).
DPMA_BPATG_BESCHWERDE trigger 2026-05-04 → Beschwerde 2026-06-04
(1mo) / Begründung 2026-07-06 (1mo from Beschwerde, weekend-shift).
DPMA_BGH_RB trigger 2026-05-04 → Rechtsbeschwerde 2026-06-04 /
Begründung 2026-07-06.
PR-4 of the Unified Fristenrechner. Three new proceeding types so the
user can pick "I'm at OLG defending a Berufung" or "I'm at BGH on the
Nichtigkeitsberufung" and get the per-instance timeline directly,
rather than chaining off DE_INF / DE_NULL trailing rows.
Migration 043 adds:
- DE_INF_OLG (Berufung OLG, sort_order=210): 7 rules. Anchor
"Zustellung LG-Urteil" + Berufungsschrift (ZPO §517, 1mo) +
Berufungsbegründung (ZPO §520(2), 2mo, anchored on Urteil not on
notice) + Berufungserwiderung (ZPO §521(2), court-set 1mo typ.) +
Anschlussberufung (ZPO §524(2), filed-with-erwiderung) +
mündl. Verhandlung + OLG-Urteil.
- DE_INF_BGH (Revision/NZB BGH, sort_order=220): 8 rules. Anchor
"Zustellung OLG-Urteil" + parallel NZB (§544.1, 1mo) /
NZB-Begründung (§544.4, 2mo) / Revisionsfrist (§548, 1mo) /
Revisionsbegründung (§551.2, 2mo) — all four from the
OLG-Urteil-Datum since they're alternatives. Plus
Revisionserwiderung (§554, 1mo court-set) + mündl. + BGH-Urteil.
- DE_NULL_BGH (Berufung BGH gegen Nichtigkeit, sort_order=230): 6
rules. Anchor "Zustellung BPatG-Urteil" + Berufungsschrift
(PatG §110.1, 1mo) + Berufungsbegründung (PatG §111.1, 3mo) +
Berufungserwiderung (PatG §111.3 → ZPO §521.2, 2mo court-set typ.)
+ mündl. + BGH-Urteil.
Anchor convention: synthetic 0-duration root rule "Zustellung [prev-
instance] Urteil" with party='both' + event_type='filing' so it
renders as IsRootEvent (= the trigger date). Per design, this is the
honest model — the user enters the actual previous-instance Urteil
date, no fabricated inter-stage gap.
Four new DE-only concepts (per slug rule: DE for German-only
procedures): nichtzulassungsbeschwerde, nichtzulassungsbeschwerde-
begruendung, revisionsfrist, revisionsbegruendung. Other rules reuse
the existing shared concepts (notice-of-appeal, statement-of-grounds-
of-appeal, response-to-appeal, cross-appeal, oral-hearing, decision).
Frontend: 3 new tiles in DE_TYPES + 8 new i18n keys (DE+EN). Tiles
appear between DE_INF and DE_NULL per sort_order grouping.
Out of scope (kept in DE_INF / DE_NULL trees during transition until
Phase D Full Appeal Chain ships): the existing trailing rows
de_inf.berufung / de_inf.beruf_begr / de_null.berufung /
de_null.beruf_begr stay live so users picking those trees still see
the appeal-period entry. Phase D will gate the visibility.
Live-verified all 3 trees on paliad.de:
DE_INF_OLG trigger 2026-05-04 → Berufung 2026-06-04 (1mo) /
Begründung 2026-07-06 (2mo from Urteil, weekend-shift) /
Erwiderung 2026-08-06 (1mo from Begründung) / Anschluss
2026-08-06 (filed-with-erwiderung).
DE_INF_BGH trigger 2026-05-04 → NZB 2026-06-04 (1mo) /
NZB-Begr 2026-07-06 / Revision 2026-06-04 / RevBegr 2026-07-06
(parallel options) / RevErw 2026-08-06.
DE_NULL_BGH trigger 2026-05-04 → Berufung 2026-06-04 / Begr
2026-08-04 (3mo per PatG §111.1 = the now-fixed seed) / Erwidg
2026-10-05 (2mo from Begr, weekend-shift).
PR-3 of the Unified Fristenrechner. Three concerns bundled in migration
042 since they touch only DE_INF / DE_NULL trees and ship together
without coverage interactions:
1. PatG §111(1) bug fix. Current paliad seed had de_null.beruf_begr at
1 month. Current text of §111(1) BGBl. 2022: "Die Frist zur
Begründung der Berufung beträgt drei Monate. Sie beginnt mit der
Zustellung des in vollständiger Form abgefassten Urteils, spätestens
mit Ablauf von fünf Monaten nach der Verkündung." Bumped to 3 months
+ deadline_notes documenting the 5-month outer cap.
2. DE_NULL Hinweisbeschluss cycle (PatG §83). 4 new rules added between
Klageerwiderung and Mündliche Verhandlung:
- de_null.replik_klaeger (Replik, 2mo typical court-set, R.83.2)
- de_null.hinweisbeschluss (court order, R.83.1) — IsCourtSet
- de_null.stellungnahme (response, parent=hinweisbeschluss, R.83.2)
— IsCourtSet via parent propagation
- de_null.duplik (Rejoinder, 1mo typical court-set, R.83.2)
The court-set typical durations match the existing DE_INF replik/
duplik pattern — a placeholder date the user can override via the
Phase A click-to-edit affordance once the court actually sets it.
3. DE_INF Anzeige der Verteidigungsbereitschaft (ZPO §276(1) Satz 1).
New rule de_inf.anzeige, 2 weeks from Klage, defendant. Was the
biggest gap in the LG-civil cycle.
Three new concepts: preliminary-opinion (court order, sort 65),
response-to-preliminary-opinion (submission, sort 39),
notice-of-defence-intention (submission, sort 19). All seeded with
DE+EN aliases for search.
DE_INF + DE_NULL sequence_orders renumbered to leave gaps so future
inserts (B6 cross-cutting Wiedereinsetzung, B4-style instance-split)
can interleave without re-renumbering.
Live-verified on paliad.de (tester@hlc.de):
- DE_INF trigger 2026-05-04 → Anzeige 2026-05-18 (2w), Erwiderung
2026-06-15 (6w), backbone unchanged.
- DE_NULL trigger 2026-05-04 → Klageerwiderung 2026-07-06 (2mo),
Replik 2026-09-07 (2mo from Erwiderung, weekend-shift), Duplik
2026-10-07 (1mo from Replik), Hinweisbeschluss + Stellungnahme
IsCourtSet, Berufungsbegründung 2026-09-04 (3mo, was 1mo).
Out of scope (deferred to B6): cross-cutting Wiedereinsetzung,
Versäumnisurteil-Einspruch (only fires on default), Schriftsatz-
nachreichung. Out of scope (deferred to PR-4): new instance-split
proceeding types DE_INF_OLG / DE_INF_BGH / DE_NULL_BGH.
Closes m's primary complaint: today's `with_ccr` flag on UPC_INF only
swaps the Replik / Duplik durations. Per UPC RoP R.29 the with-CCR flow
ALSO adds 5–7 new submissions across the claimant / defendant exchange.
Same gap on UPC_REV: Application to amend (R.49.2.a → R.55 = R.32 m.m.)
and Counterclaim for infringement (R.49.2.b → R.50, R.56 cycle) were
entirely missing.
UPC_INF gets a nested `with_amend` flag under `with_ccr` (R.30 amend
is only available with a CCR). UPC_REV gets two parallel independent
flags `with_amend` + `with_cci`; both can be on. Citations verified
against data.laws_contents (youpcdb, UPCRoP).
Migration 041 (waved INSERTs because each subsequent rule references
the prior wave's parent_id):
- Wave 0: 11 new concept rows (counterclaim-for-revocation,
defence-to-counterclaim-for-revocation, defence-to-application-to-amend,
reply-to-defence-to-counterclaim-for-revocation,
reply-to-defence-to-application-to-amend,
rejoinder-on-reply-to-defence-to-ccr, rejoinder-on-reply-to-amend,
counterclaim-for-infringement, defence-to-counterclaim-for-infringement,
reply-to-defence-to-counterclaim-for-infringement,
rejoinder-on-counterclaim-for-infringement). counterclaim-for-revocation
also seeded for the search bar even though its rule lives implicitly
in inf.sod (the with_ccr flag captures it).
- UPC_INF + UPC_REV sequence_orders renumbered to leave gaps (10/20/30…)
so new cross-flow rows interleave chronologically with the backbone.
- 7 new UPC_INF rules: inf.def_to_ccr (R.29.a), inf.app_to_amend (R.30.1),
inf.def_to_amend (R.32.1), inf.reply_def_ccr (R.29.d),
inf.reply_def_amd (R.32.3), inf.rejoin_reply_ccr (R.29.e),
inf.rejoin_amd (R.32.3).
- 8 new UPC_REV rules: rev.app_to_amend (R.49.2.a), rev.def_to_amend
(R.43.3), rev.reply_def_amd (R.32.3 m.m.), rev.rejoin_amd (R.32.3 m.m.),
rev.cc_inf (R.49.2.b), rev.def_cci (R.56.1), rev.reply_def_cci (R.56.3),
rev.rejoin_cci (R.56.4).
Calculator (services/fristenrechner.go):
- Zero-duration rules now split into 4 buckets, not 2:
1. parent=nil + non-court → IsRootEvent (existing)
2. parent=nil + court → IsCourtSet (existing, e.g. inf.oral when stand-alone)
3. parent set + court → IsCourtSet (existing, waypoints)
4. parent set + non-court → "filed-with-parent" — inherit parent's
date. NEW. Used by rev.app_to_amend / rev.cc_inf which per
R.49(2) are filed AS PART OF the Defence to revocation.
- AnchorOverrides on a zero-duration rule short-circuits to the user's
date, propagating downstream as before.
Frontend:
- New checkboxes inf-amend-flag (UPC_INF, nested under ccr-flag),
rev-amend-flag, rev-cci-flag (UPC_REV). Visibility per proceeding
type; inf-amend disabled until ccr is on (R.30 dependency).
- Three new i18n keys (DE+EN). Small CSS for nested-checkbox indent
and disabled-state colour.
Live-verified via curl on paliad.de against tester@hlc.de:
UPC_INF + with_ccr+with_amend, trigger 2026-05-04 → all 7 new rules
render at correct dates (R.29.a 2mo, R.30.1 2mo, R.32.1 2mo from
app_to_amend, R.29.d 2mo from def_to_ccr, R.32.3 1mo, R.29.e 1mo,
R.32.3 1mo).
UPC_REV + with_amend+with_cci → rev.app_to_amend / rev.cc_inf show
rev.defence's date (filed-with-parent), R.43.3 2mo / R.56.1 2mo /
R.32.3 + R.56.3 1mo / R.32.3 + R.56.4 1mo all line up.
PR-1 of the Unified Fristenrechner. Purely additive: new search-grouping
layer + per-rule date override capability. No coverage changes yet
(those land in PR-2 = Phase B1 UPC counterclaim cross-flows).
Migrations:
- 037: paliad.deadline_concepts (id, slug, name_de/en, aliases text[],
party, category, sort_order). Trigram + GIN indexes for the search bar.
- 038: deadline_rules.concept_id (uuid FK), legal_source (text);
event_deadlines.legal_source; trigger_events.concept_id (text slug,
soft-link — youpc imports keep their bigint PK).
- 039: deadline_rules.condition_flag text → text[] (USING ARRAY[old]).
Semantic: rule renders iff every element is in CalcOptions.Flags.
Single-element arrays preserve the legacy with_ccr swap exactly.
- 040: seed 30 concept rows + backfill all 74 fristenrechner deadline_rules
with concept_id; backfill legal_source from existing rule_code
(e.g. 'RoP.023' → 'UPC.RoP.23.1', '§ 276 ZPO' → 'DE.ZPO.276.1',
'Art. 108 EPÜ' → 'EU.EPÜ.108', 'R. 79(1) EPÜ' → 'EU.EPC-R.79.1').
Calculator (services/fristenrechner.go):
- ConditionFlag is now pq.StringArray (matches text[] schema). New
allFlagsSet() helper gates rule rendering; rules with multi-element
flags require ALL of them set (prep for Phase B1 with_amend ∧ with_cci).
- CalcOptions.AnchorOverrides map[string]string (rule_code → YYYY-MM-DD).
The tree-walk consults overrideDates[parent.code] before reading the
computed-date map; lets a downstream rule re-anchor on a user-set date.
- IsCourtSet rows that get an override stop being placeholder and emit
the user's date as a real anchor (so downstream cost_app etc. compute
off it). New IsOverridden flag in UIDeadline so the UI can highlight
user-edited rows.
- LegalSource surfaced on UIDeadline for future search-card display.
UI (frontend/src/client/fristenrechner.ts + global.css + i18n):
- Each timeline / column rule date is click-to-edit. Click → inline
date input → blur or Enter → POST with anchorOverrides → re-render.
Empty value clears the override. Escape cancels. Root-event rows
(the trigger anchor) stay non-editable — that's the trigger-date input.
- Override map cleared on proceeding switch / reset; persists across
trigger-date / flag toggle changes within the same proceeding.
- New CSS: subtle hover underline on .frist-date-edit; lime border on
.timeline-date--overridden + .frist-date-edit-input.
- New i18n key deadlines.date.edit.hint (DE + EN).
Handler (handlers/fristenrechner.go):
- POST body gains optional anchorOverrides map<string,string>; passed
through to CalcOptions.
Tests:
- TestAllFlagsSet covers single-flag legacy semantic, two-flag AND
semantic, empty-required unconditional, extra-flags-no-effect.
- Existing TestIsCourtDeterminedRule unchanged.
Phase A ships standalone — Phase B1 (UPC counterclaim cross-flows) and
Phase C/D (search backend + concept-card UI) follow.
m's revisions (23:36):
- Q1 corrected: EN slug for shared concepts too (klageerwiderung →
statement-of-defence, replik → reply-to-defence, berufungsfrist →
notice-of-appeal, einspruchsfrist → opposition, wiedereinsetzung →
re-establishment-of-rights). DE slug only for German-law-only
concepts (nichtzulassungsbeschwerde, versaeumnisurteil-einspruch,
hinweisbeschluss-stellungnahme).
- Q4 simplified: drop the customizable-extension flag_param mechanism.
Replace with a generalised "user can override any computed date,
downstream re-anchors off it" capability. CalcOptions gains
AnchorOverrides map[string]string; tree-walk consults it before the
computed-date map. UI gives each row a click-to-edit date affordance
(also unlocks court-set decision dates being entered post-hoc, which
the existing IsCourtSet placeholder UX has been hinting at). PatG §82
seed stays at 2 months static; user-set extensions handled by inline
date override, not by a flag_param mechanism.
Cleaner. No new DB column. Generalises beyond extensions to any case
where the user knows the real date better than the calculator's
projection.
- Q1 concept slug naming: mixed convention. EN slug for UPC/EPC-native
concepts (application-to-amend, request-for-discretionary-review).
DE slug for German-only concepts (nichtzulassungsbeschwerde,
versaeumnisurteil-einspruch). DE slug for SHARED concepts that exist
in both DE and UPC/EPC (klageerwiderung, replik, berufungsfrist,
einspruchsfrist, wiedereinsetzung) because m works primarily in
German and the slug is internal/maintenance-facing only.
- Q2 EU.EPÜ confirmed for EPÜ namespace.
- Q3 PatG §111(1) 1mo→3mo confirmed for Phase B3.
- Q4 PatG §82(1): shape (b) — 1mo base + with_extension flag with
CUSTOMIZABLE extension duration (default 1mo). New flag_param
mechanism on flag-conditioned rules: CalcOptions.Flags becomes
map[string]int; rules with flag_param_code add caller's param to
duration. UI shows number input next to checkbox. Generalises to
PatG §75 etc. Phase A5 picks up the calculator extension; Phase B3
hooks PatG §82.
- Q5 Full Appeal Chain: multiple date inputs per stage, no inter-stage
gap guessing. Stage N's downstream deadlines render as IsCourtSet
placeholders until user enters Stage N-1's terminal decision date.
- Q6/Q7/Q8 confirmed as drafted.
§5.2.2 PatG §82 row updated to reflect flag-based shape. §4.4 concept
slug examples expanded with the mixed-convention rule rendered
explicitly. §7 Phase A5 added for the flag_param calculator change.
Significant restructure after m's 10 answers (relayed via head 23:10):
- Augment, not replace — search bar at top + existing tile grid stays as
browse fallback. Both existing tabs stay live. Phase E (subsumption)
dropped.
- Unifier shape: new paliad.deadline_concepts layer above existing
deadline_rules; deadline_rules gains concept_id FK + structured
legal_source. condition_flag scalar→array (Q3) for AND-of-flags
semantics on UPC_REV (with_amend ∥ with_cci).
- Search hits as ONE card per concept with proceeding pills inside (NOT
a flat list of one-per-proceeding hits). Card body: pills [UPC R.23.1
3mo] [LG §276.1 6w] [BPatG §82.1 1mo] [EPA R.79.1 4mo] etc.
- Structured legal_source codes: UPC.RoP.23.1, DE.ZPO.276.1,
EU.EPÜ.108, DE.PatG.111.1 — parseable, filterable, indexed.
- "Vollständige Instanzenkette" checkbox synthesises LG→OLG→BGH (or
BPatG→BGH) timeline as one tree at render-time; data stays per-
instance.
- Forum filter dropped (Q8). Filters now: Verfahrensart / Partei /
Rechtsquelle.
- Court-set placeholders ("Verhandlung", "Entscheidung",
"Zwischenverfügung") surface as searchable triggers (Q10).
- Columns-view sequence preservation (Q9) flagged but punted to a
separate follow-up task — t-paliad-129 column renderer must respect
sequence_order even on undated court-set events.
8 remaining open questions for m (concept slug convention, EPÜ
namespace, PatG §82(1) modeling, Full Appeal Chain anchor handoff,
quick-pick chip seed, etc.).
ComputeBaseFee walked the bracket table indefinitely, so a Streitwert of
e.g. €100M produced fees far above what German law actually permits. §34
GKG / §22(2) RVG cap the table at €30M — above that the fee stays at the
30M-row value.
Surgical fix: clamp streitwert to GermanFeeStreitwertCap (30M) at the top
of ComputeBaseFee. Applies to all GKG/RVG fee versions (2005, 2013, 2021,
2025); UPC value-based fees use a separate code path (lookupUPCValueFee
against UPCFeeSchedule.ValueBased) and stay uncapped — UPC has its own
statutory tier structure with explicit 50M and unlimited brackets.
Tests: cap holds across all four versions for both GKG and RVG; values
below 30M continue to scale as before; UPC remains uncapped.
Spot check (GKG / RVG base, 2025 schedule):
1M EUR → 6278.00 / 5553.50
5M EUR → 23078.00 / 19553.50
30M EUR → 128078.00 / 107053.50
50M EUR → 128078.00 / 107053.50 (capped)
100M EUR → 128078.00 / 107053.50 (capped)
1B EUR → 128078.00 / 107053.50 (capped)
Three changes to the columns view + the Drucken button, per m's 2026-05-04
polish round on top of t-paliad-127 / t-paliad-126:
1. Date-aligned grid timeline. The columns view used to render three
independent vertical stacks; now each distinct dueDate gets a grid row
so a Court hearing on the 15th lines up beside a Proactive Antrag on
the 15th (and an empty cell where the third party has nothing to do).
Court-set / dateless rows collapse into a final trailing row.
2. "both"-party deadlines are mirrored, not spanned. Previously they
rendered as a full-width row beneath the columns; now they appear in
BOTH the Proactive AND Reactive cell of their date-row, with a
"↔ beide Seiten" / "↔ both parties" caption so the duplication reads
as deliberate. The Court column at that row stays empty unless a
court-party deadline also lands on the same date. The full-width
spans block (.fr-columns-spans) is gone.
3. Drucken button restyle. The grey-square default-button look is
replaced with a tertiary-action treatment: hairline border, accent
on hover, subtle lift, inline 16px printer SVG. To keep the icon
from being wiped by [data-i18n] (which sets textContent), the label
moved into a child <span data-i18n="deadlines.print"> while the SVG
sits as its sibling. Both fristen-print-btn and event-print-btn pick
up the new style via the shared .print-btn class.
Redefines the "Nur persönliche" filter on /events from "appointment with
NULL project_id" to "items where created_by = me", applied uniformly to
deadlines and appointments.
Before: client-side filter dropped every deadline row because the type
guard was `x.type === "appointment"`. m saw zero deadlines under "Nur
persönliche" even though he created plenty.
After:
- /api/events?personal_only=true (and /api/events/summary?personal_only=true)
narrow BOTH rails to f.created_by / t.created_by = current user.
ProjectID is ignored when personal_only is set (the two are
contradictory).
- DeadlineService.ListFilter and AppointmentService.AppointmentListFilter
gain CreatedBy *uuid.UUID — composes with existing visibility (AND), so
a row created on a team the user has since left still won't leak.
- Frontend drops the client-side filter; sends personal_only=true when
projectFilter === PERSONAL. URL ?personal_only=true also accepted on
initial load (bookmark-friendly alias for ?project_id=__personal__).
Personal option now shows for type=Fristen too — applies uniformly.
- 3 new live subtests covering personal_only across type=deadline /
appointment / all, with mixed-creator + multi-project + null-project
fixtures.
Add a third Fristenrechner layout that splits the computed deadlines into
three vertical lanes by party:
- Proactive (claimant) | Court | Reactive (defendant)
- Each lane is independently date-ordered.
- party=both rows render as a full-width strip below the columns
(separate "Beide Parteien" block) since they apply to all sides.
- View toggle (Zeitstrahl / Spalten) lives in step 3 of the procedure
wizard. Persisted via ?view=columns; reload restores the choice.
- Mobile (≤640px): grid collapses to a single column stack.
Event-mode results are not split into lanes — `EventDeadlineResult` has
no party field and the spec rules out backend changes; the toggle is
scoped to the procedure-mode results panel only.
The Verfahrensablauf and "Was kommt nach" tabs now render results
immediately, without requiring a click on "Fristen berechnen". The
button stays as a manual force-recalc affordance.
- Pre-select the first proceeding type on load so step 3 has data
out of the box.
- Pre-select the first trigger event on first event-tab activation
(or right after the list loads if the tab was already active).
- Auto-recalc on date / proceeding-type / condition-flag change.
- Debounce input events to 200ms so spam-edits coalesce into one
request, with a per-mode sequence counter so a stale fetch result
can never overwrite a fresher one.
The /events Project filter dropdown was sorted by `updated_at DESC`, so a
recently-touched Case appeared above its parent Client and cousins
interleaved unrelated branches — m's report (2026-05-04): "Siemens cases
come directly after 'mandant vs Gegner' and are not under 'Siemens-AG'".
Backend: switch ProjectService.List to ORDER BY p.path so every
descendant immediately follows its ancestor — the same ordering BuildTree
produces. Both callers (handleListProjects, searchProjects) gain a
stable, hierarchical default that matches user expectation.
Frontend: add project-indent.ts shared helper and apply NBSP indent
prefix in every <select> picker fed by /api/projects: events filter,
/deadlines/new, /appointments/new, checklist new-instance modal,
Fristenrechner save modal. NBSP avoids browser whitespace collapse
inside <option> labels. Multi-parent repetition is out of scope (data
model has singular parent_id today).
Tests: project_list_order_test pins the path-order contract against a
seeded mixed-recency tree.
Selecting a Client in the project filter now returns rows attached to
that Client AND every Litigation / Patent / Case below it (and so on
down the tree). Previously the filter was exact-match: picking a Client
hid every item in the subtree, which was the opposite of what users
expect when they pick a parent in a hierarchical picker.
The descendant set comes from paliad.projects.path - every project's
path always contains its own id and every ancestor's id, so any project
whose path includes the filter UUID is either that project or a
descendant. Pattern matches the existing visibility predicate (which
walks the path UPWARD for inheritance); the new helper just inverts the
direction.
Filter sites updated:
- DeadlineService.ListVisibleForUser (/deadlines, /events)
- DeadlineService.SummaryCounts (deadline summary cards)
- AppointmentService.ListVisibleForUser (/appointments, /events)
- EventService.deadlineBuckets (/events deadline rail)
- EventService.appointmentBuckets (/events appointment rail)
ListForProject (deadline/appointment/checklist/note) is unchanged - it
fetches items for ONE specific project on the project detail page, not
a filter.
Visibility predicate (paliad.can_see_project) untouched - that walks
upward and is a different concern.
Until now, /events hid the Status dropdown when Type=Termine. The
date-bucket filters (Heute, Diese Woche, Nächste Woche, Später) only
worked on the deadline rail — m wanted them on appointments too, even
without a "completed" dimension.
Frontend (events.ts):
- New populateStatusFilter() rebuilds the Status <select> options based
on currentType: deadlines get the full 8-option set, appointments
narrow to 5 (Alle + 4 buckets). The "completed/pending/overdue"
options drop because they have no appointment analogue.
- applyTypeVisibility() no longer hides the Status filter for
appointments; it calls the populator instead. The populator runs on
type-chip click and on language change so labels translate live.
- When switching type while a now-invalid status is selected (e.g.
Termine + status=completed via URL), the populator falls back to the
per-type default (deadline → pending, appointment → all) and updates
URL params.
- syncURLParams + isFilterPristine + initFilters use a per-type default
so the appointment view treats `all` as pristine and stays out of the
URL until the user picks a bucket.
- loadList always sends `status` to /api/events; backend already
applies bucket-aware appointment filtering via
bucketAppointmentWindow().
events.tsx:
- The static <option> list collapses to a single placeholder; the
populator owns the option set at hydration.
i18n:
- New `events.filter.status.all` ("Alle"/"All") for the appointment-only
case — `deadlines.filter.all` says "Alle (offen & erledigt)" which is
wrong for appointments (they don't have a completed/pending state).
Backend (event_service_test.go):
- Three new live subtests confirming type=appointment + status=today
narrows to today's appointments, status=later narrows to far-future,
and status=completed collapses the appointment rail (defensive vs.
URL-hacking — the dropdown excludes that value for appointments).
Per UPC AC decision 2023-05-26, the UPC has summer + winter judicial
vacations but the Court continues to operate during them — they do NOT
extend procedural deadlines. paliad's HolidayService was treating every
paliad.holidays row as a non-working day, including vacation entries, so
a deadline landing on Tue 2026-08-04 (a regular working Tuesday) was
incorrectly shifted to Mon 2026-08-31 by walking the entire summer-
vacation run.
Fix: gate IsNonWorkingDay on Holiday.IsClosure (true for public_holiday
and closure types, false for vacation). IsHoliday still returns the row
regardless of type — UI surfaces that want to flag "this date is inside
UPC vacation" can still ask. paliad.holidays data is unchanged: the UPC
vacation rows stay as informational metadata.
The Kind="vacation" branch of AdjustForNonWorkingDaysWithReason is now
unreachable in practice (every vacation entry is IsClosure=false, so the
walk loop never enters with a vacation as the cause). Left in place as
defensive code for any future vacation type that should shift.
Tests: replaced TestAdjustForNonWorkingDaysWithReason_Vacation (asserted
the old wrong behaviour) with TestVacationDoesNotShiftDeadlines covering
m's reproduction (Tue 2026-08-04 → no shift), winter-vacation no-shift
(Mon 2026-12-28), Christmas/Neujahr regression (still shift correctly,
and walk through informational vacation entries to land on the next
real working day), and a Karfreitag regression to lock public-holiday
shifts.