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.
The current "Wochenende/Feiertag" / "weekend/holiday" label hides the cause
of long shifts — m's reproduction had a deadline jump from 4.8.2026 to
31.8.2026 (+27 calendar days) across UPC Summer Vacation, and the UI made
it look like a bug. The math was correct; the explanation was lying.
Backend:
- AdjustForNonWorkingDaysWithReason returns an AdjustmentReason alongside
the adjusted date. Walks the same 60-iter loop, classifies the dominant
cause (vacation > public_holiday > weekend), collects every named
holiday hit, and for vacations scans outward to report the contiguous
block boundary (27.7.–28.8., not the 25 individual rows).
- AdjustForNonWorkingDays now wraps the new method, preserving its
3-tuple signature for existing callers (deadline_calculator,
event_deadline_service).
- UIDeadline gains an AdjustmentReason field; FristenrechnerService
populates it on every shifted deadline.
- Date fields serialise as YYYY-MM-DD strings (HolidayDTO + string
vacation span) — the Fristenrechner client already speaks that format.
Frontend:
- AdjustmentReason → human-readable phrase via renderAdjustmentReason:
vacation → "{vacation_name} ({span})"
public_holiday → "Feiertag ({first_holiday_name})" / "{name} holiday"
weekend → "Wochenende" / localised weekday
- Surrounding format becomes "Verschoben wegen X: A → B" (DE) or
"Shifted (X): A → B" (EN). Falls back to the legacy reason string
when the backend hasn't sent a structured reason.
- Vacation names render verbatim from paliad.holidays — no hardcoded
i18n mapping for individual closures (those rotate via the seed, not
via i18n.ts).
Tests cover the three Kind paths plus the no-shift case; UPC vacation
test injects the migration-010 seed into the cache so the assertion
runs without a live DB.
Out of scope (raised in conversation, deferred):
- Whether "UPC Summer Vacation" / "UPC Winter Vacation" are the right
names for the seeded rows, and whether the winter block belongs in
paliad.holidays at all (m flagged this as BS while reviewing the
task — needs a data-side decision before renaming/removing).
- holidays.country isn't filtered by proceeding-type jurisdiction, so
UPC vacation currently shifts EP_GRANT / EPA_APP / German national
deadlines too. Bigger fix; flagged for a follow-up issue.
Add dark-mode rules for `.termin-type-chip.termin-type-XYZ`. Without them
the chip inherited the bare `.termin-type-XYZ` swatch rule (line 8407+),
which intentionally paints text the same colour as the background for
the dot/border-left swatch use case → text invisible in the chip context.
Mirror the existing badge dark rules (lines 8413-8415) so chip and badge
share the same colour family. Adds the missing `consultation` variant for
the chip (badge consultation dark is also missing but out of scope).
The `.admin-et-variables-list` was a 4-track CSS grid designed for flat
name/type/desc/sample siblings. The renderer wraps each variable in a
`.admin-et-variable-row` div, so each variable became a single grid item —
and 4 rows packed into one visual row, producing the wide multi-column
overflow.
Switch the outer container to `flex-direction: column` (one variable per row)
and the row to a baseline-aligned wrapping flex (name, type pill, desc, sample
left-to-right). `margin-left: auto` pushes the sample right when there's room
and lets it wrap below on narrow widths.
Pre-existing dark-mode issue (`.admin-et-variable-name` hardcoded to #1c1917,
unreadable on dark background) is unchanged by this fix and predates the
layout regression — flagged separately, not in scope here.
Three small polish bugs on the unified /events filter row.
A) i18n leak on lang change. The static [data-i18n] options in the appointment-
type select are retranslated by initI18n's applyTranslations(), but the project
select is rebuilt at runtime via populateProjectFilter() and the multi-select
trigger label is set via t() inside attachEventTypeMultiSelectFilter. Neither
re-ran on lang change, so DE→EN→DE left "All matters" / "All types" stuck on
the page. Wire onLangChange:
- events.ts: re-call populateProjectFilter() inside the existing handler.
- event-types.ts: attachEventTypeMultiSelectFilter now subscribes to
onLangChange itself and re-renders updateLabel() + (when open) renderPanel().
Self-contained side effect — also fixes /agenda's multi-select for free.
B) Filter caption above the field. The pre-existing .filter-row + .filter-group
pattern (already used on /projects) had .filter-group laid out horizontally
(label-left, select-right). Wrapped each events.tsx label+control pair in
.filter-group and switched .filter-group to flex-direction: column with a 4px
gap, so the caption sits above the select / button trigger. The whole filter-
row stays a horizontal flex-wrap of column cells. Mobile (≤480px) still falls
back to a single-column stack. /projects gets the same polish since it already
used the same wrapper pattern.
C) Multi-panel anchoring. The event-type popover was rendered as a sibling of
the trigger inside .filter-row and absolute-positioned with auto/auto, so it
landed wherever the wrapping flex layout put it (~226px above the trigger in
practice). Wrapped trigger+panel in <div class="multi-anchor"> with
position: relative and pinned the panel via top: 100%; left: 0 — only when
inside .multi-anchor, so /agenda's existing column-flex auto-positioning is
untouched.
toggleFilterPair simplified to hide the wrapping .filter-group via closest()
instead of toggling the label and control separately.
m's call (2026-05-04): the Dashboard's secondary "Termine auf einen
Blick" rail (3 cards) was redundant — the upcoming-Termine list lives
right below it on the same page, and the Fristen rail above is what
matters for the at-a-glance read. Drop the cards.
frontend/src/dashboard.tsx: remove the <section
aria-labelledby="dashboard-appointment-summary-heading"> and its 3 cards.
frontend/src/client/dashboard.ts: drop renderAppointmentSummary +
the AppointmentSummary type + the appointment_summary field on
DashboardData (kept the API-side payload — other consumers may use it
later; just stop wiring the dashboard to it).
Also two related event-page styling bugs:
- frontend/src/client/events.ts:721 was force-stamping
`style.display = "block"` on the event-type multi-panel popup whenever
the type filter was anything but appointment. The panel is supposed
to be a hidden flyout owned by the trigger button via `panel.hidden`;
the inline display:block trumped the `.multi-panel[hidden]` CSS rule
and left it visible on larger screens (m flagged the
`<div ... hidden="" style="display: block;">` artefact). Fix: never
set inline display from this code path; force-close the panel only
when switching to appointment view.
- frontend/src/styles/global.css `.multi-list` + `.multi-option`: long
event-type labels overflowed the 22rem panel horizontally because
`.multi-list` only had `overflow-y: auto` and the flex options had no
`min-width: 0` / overflow-wrap rules. Add `overflow-x: hidden` +
`min-width: 0` on the list and `overflow-wrap: anywhere` on options.
Two adjacent i18n leaks in /tools/fristenrechner "Was kommt nach…" mode,
same pattern as t-paliad-112's deadline_rules fix but on event_deadlines:
A) title_de empty for all 70 rows. The DTO already falls back to title
silently, so DE locale rendered English titles ("Statement of Defence",
"Decision of the EPO", …). Backfilled via mig 035 with UPC RoP DE
terminology that matches the trigger_events.name_de translations from
mig 033, so the picker label and the deadline row read the same.
B) notes column carries English text on rows 50, 52, 70 (DE-named column
was DE-only in spec, but seeds slipped EN strings through). Mig 036
adds a parallel notes_en column following the t-112 mig 032 pattern,
copies the existing English into notes_en, and replaces notes with
proper DE for those three rows.
Render path:
- service: select notes_en, plumb through EventDeadlineResult.NotesEN
- frontend: getLang() === "en" ? (notesEN || notes) : notes (mirrors the
proceeding-tree timeline branch already in fristenrechner.ts)
Head's fe8ba15 corrected the standard fee but kept SMEFee=2925 (collapsed
the tier). m's call: the data should reflect the actual reduced fee
(2.015€) even while the UI doesn't surface the tier yet. Taking fritz's
SMEFee=2015 over main's 2925.
Standard appeal fee per Rule 6 EPC-Gebühren v2024-04-01 was raised from
2.255€ to 2.925€; reduced fee per Rule 7a(2)(a-d) was raised from 1.880€
to 2.015€. Both stale in calc.EPAFees, surfaced wrong amount on
/tools/kostenrechner and /tools/gebuehrentabellen EPA tab.
Reduced-fee tier is updated for data accuracy; UI surfacing of that tier
is deferred per m's call (out of scope for this task).
The Kostenrechner showed EUR 2.255 for the EPA appeal fee, a stale
pre-restructure figure. Per Rule 6 EPC-Gebühren (version 2024-04-01)
the standard appeal fee is EUR 2.925 — applies to any legal person not
under Rule 7a(2)(a-d). The reduced fee of EUR 2.015 for natural persons
/ SMEs / non-profits / academic institutions / public-research orgs is
documented in a code comment but not surfaced separately yet — m said
keep it simple; ship the standard fee only.
SMEFee aligned to the same 2925 since we're not exposing a tier toggle.
Updated TestComputeEPAInstance_SME accordingly.
Both /tools/kostenrechner and /tools/gebuehrentabellen read from the
same EPAFees map, so this single edit fixes both surfaces.
Picked up from fritz's worktree — fritz hit a Claude usage rate limit
mid-edit; head completed the same diff plus the test update.
Two migrations both named 032 collided when t-111 and t-112 merged in
parallel — 032_deadline_notes_en (t-112, already applied to the DB and
tracker bumped to v33) vs. 032_deadlines_rule_code (t-111). The Go
migration runner refuses to init the driver when two files share a
prefix, so paliad.de was 404 across all routes (container in restart
loop with `migration failed: ... duplicate migration file:
032_deadlines_rule_code.down.sql`).
Renumbering t-111's pair to 034 (033 was used by t-112's
trigger_events_de backfill).
Three correctness bugs from the t-paliad-101 QA sweep, fixed together since
they all change displayed/saved numbers users rely on.
B1 — Kostenrechner UPC GESAMTKOSTEN double-count
ComputeUPCInstance was setting InstanceTotal = effectiveCourtFee +
recoverableCeiling. The R.152 recoverable-cost cap is the OPPOSING
side's worst-case loss-of-suit liability, not the user's own cost —
folding it into GESAMTKOSTEN inflated the UPC total under a label
that means "your outlay," and the DE LG/OLG/BGH branches don't add
any opponent estimate. Drop it from InstanceTotal; the ceiling
still surfaces as its own RecoverableCeiling line item.
Live pre-fix on paliad.de (Streitwert 100k, UPC 1. Instanz only):
instanceTotal = 52600 = 14600 court fee + 38000 R.152 ceiling
Post-fix:
instanceTotal = 14600 (court fee only); RecoverableCeiling stays 38000
B3 — Court-determined Termine emit trigger date as a real-looking date
Zwischenverfahren / Mündliche Verhandlung / Entscheidung all live in
paliad.deadline_rules with duration_value=0 and parent_id=NULL, so
Calculate() classified them as IsRootEvent and emitted the trigger
date as their own DueDate. Worse, RoP.151 "Antrag auf Kostenentscheidung"
parents off inf.decision and chained 1 month off the placeholder ->
bogus deadline that the UI rendered as real.
Fix: classify a zero-duration rule as IsCourtSet (not IsRootEvent)
when primary_party = 'court' or event_type ∈ {hearing, decision,
order}. Track court-set rule IDs and propagate IsCourtSet downstream
to any rule whose parent is court-set, so RoP.151 also surfaces as
court-set rather than a fabricated date. Save-modal already greys
out IsCourtSet rows so the "Gerichtsbestimmte Termine ohne Datum
werden übersprungen" footnote becomes truthful again.
Live pre-fix on paliad.de (UPC_INF, trigger 2026-04-29):
Zwischenverfahren / Oral / Entscheidung -> dueDate 2026-04-29
Antrag auf Kostenentscheidung -> 2026-05-29 (bogus, +1mo from trigger)
B6 — Fristenrechner save flow stored rule code in TITLE
Frontend was concatenating "RoP.023 — Klageerwiderung" into the
title because deadlines had nowhere else to put the citation, and
the /deadlines REGEL column ended up showing "—". Add migration 032
with a paliad.deadlines.rule_code text column, plumb it through
CreateDeadlineInput / insertTx, drop the now-redundant r.code AS
rule_code JOIN alias on the list query (the deadline owns its
citation), and render f.rule_code on the project-detail deadlines
table + /deadlines events list + deadline-detail page.
Build, vet, and tests all clean. New unit test
TestIsCourtDeterminedRule pins the B3 discriminator across the
event_type / primary_party combinations seen in migrations 012 + 031.
Repro creds: tester@hlc.de
PR-2 of t-paliad-115. The unified Fristen + Termine surface now lives at
/events. Old /deadlines and /appointments list URLs 301-redirect to
/events?type=deadline and /events?type=appointment so existing bookmarks
still land on the right view. Detail pages (/deadlines/{id},
/appointments/{id}) stay type-specific.
Backend (Go).
- New `GET /events` route → handleEventsListPage serves dist/events.html.
- `GET /deadlines` → handleDeadlinesListRedirect (301 → /events?type=deadline).
- `GET /appointments` → handleAppointmentsListRedirect (301 → /events?type=appointment).
- /deadlines/new, /deadlines/calendar, /deadlines/{id}, /appointments/new,
/appointments/calendar, /appointments/{id} unchanged — type-specific
detail / form / legacy-calendar surfaces stay where they are.
Frontend.
- build.ts now emits ONE events.html (not events-deadlines /
events-appointments) with defaultType="all" baked in. The page reads
?type=… and ?view=… on hydration, so /events?type=deadline lands on
the Fristen-only Cards view, /events?view=calendar opens the calendar,
and bare /events shows the Beides view.
- Sidebar Fristen / Termine entries point at /events?type=deadline and
/events?type=appointment. The SSR active-state matches exactly via
href === currentPath, so detail/new/calendar pages that pass
currentPath="/events?type=deadline" (resp. appointment) still
highlight the right entry.
- events.ts hydration adds applySidebarTypeHighlight(): on bare /events
the sidebar SSRs with neither entry lit, and we re-highlight the
matching entry whenever the in-page chip toggle changes the active
type. Sidebar stays in sync without a server round-trip.
- Updated every list-target reference: palette-actions.ts (Cmd-K
navigation), deadlines-detail.ts + appointments-detail.ts (post-delete
redirect), and the back-link / cancel hrefs in the *-new + *-detail +
*-calendar TSX templates. Detail-page Sidebar/BottomNav currentPath
also moved from "/deadlines" → "/events?type=deadline" so the new
highlight contract holds end-to-end.
Out of scope (per task brief).
- A third "Ereignisse / Alle Events" sidebar entry pointing at /events
bare. m's call: keep two entries; defer until signal.
- Removing /deadlines/calendar + /appointments/calendar standalone
pages. The new /events?view=calendar covers the same need but the
legacy URLs stay live for one cycle.
Build clean: `cd frontend && bun run build` + `go build/vet/test ./...`.