Commit Graph

551 Commits

Author SHA1 Message Date
m
355e718516 design(t-paliad-131): unified Fristenrechner — search-by-anything + complete coverage
Inventor design doc at docs/plans/unified-fristenrechner.md.

Covers:
- Single search bar UX over both deadline_rules (proceeding-tree) and
  trigger_events (event-driven) backends, federated via a materialised
  view with pg_trgm indexes.
- Faceted filters: forum, proceeding type, party, legal source.
- UPC counterclaim cross-flows missing today (R.29(a)/(d)/(e), R.30,
  R.32, R.43.3, R.49(2), R.51, R.52, R.55, R.56) — verbatim citations
  pulled from data.laws_contents (UPCRoP).
- German PatG/ZPO gap audit: missing OLG + BGH-Revision + BGH-NZB cycles,
  ZPO §339 Versäumnis, §521 Berufungserwiderung, PatG §111 (likely 1mo→3mo
  fix), DPMA Einspruch + Beschwerde. EPA gaps: R.116, R.79(2/3), R.106
  Überprüfung, Wiedereinsetzung × 3.
- Phased migration plan A–E, each independently shippable.
- 10 open questions for m's go/no-go before coder shift.

No code changes; awaiting m's review.
2026-05-04 23:11:16 +02:00
m
6eece2d0ff Merge: t-paliad-130 — Kostenrechner caps GKG/RVG Streitwert at €30M (§34 GKG / §22(2) RVG) 2026-05-04 21:00:08 +02:00
m
0e1d4869fb fix(t-paliad-130): cap GKG/RVG Streitwert at €30M (§34 GKG / §22(2) RVG)
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)
2026-05-04 20:58:08 +02:00
m
7fdd74ed5d Merge: t-paliad-129 — Fristenrechner polish (date-aligned columns, both-mirrored, Drucken restyle) 2026-05-04 20:03:03 +02:00
m
cca433cb10 feat(t-paliad-129): Fristenrechner polish — date-aligned columns + Drucken icon
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.
2026-05-04 19:57:32 +02:00
m
049136d424 Merge: t-paliad-128 — /events Nur persönliche redefined as created_by=me (deadlines + appointments) 2026-05-04 19:53:02 +02:00
m
9919e04657 feat(t-paliad-128): /events 'Nur persönliche' = items I created
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.
2026-05-04 19:49:37 +02:00
m
c1ff631257 Merge: t-paliad-127 — Fristenrechner third view (Proactive | Court | Reactive columns)
Conflict resolution: combined fritz's t-126 auto-calc plumbing (procCalcSeq + scheduleProcCalc + selectProceeding's auto-trigger) with turing's t-127 view-state (procedureView + initViewToggle). Both additive, both wired in DOMContentLoaded.
2026-05-04 19:44:27 +02:00
m
bf80c167ba Merge: t-paliad-126 — Fristenrechner auto-calculate on tab open + on input change 2026-05-04 19:37:02 +02:00
m
63e5fb0b86 feat(t-paliad-127): Fristenrechner columns view (Proactive/Court/Reactive)
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.
2026-05-04 19:34:14 +02:00
m
04d034af81 feat(t-paliad-126): Fristenrechner auto-calc on tab open + input change
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.
2026-05-04 19:33:47 +02:00
m
371a38a194 Merge: t-paliad-125 — Project picker dropdown sorts by tree path + indents by depth (5 pickers) 2026-05-04 19:33:19 +02:00
m
4d7c74994a feat(t-paliad-125): sort project pickers by tree path with depth indent
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.
2026-05-04 19:30:37 +02:00
m
062630ca38 Merge: t-paliad-123 — /events Status filter for appointments (date buckets) 2026-05-04 18:58:09 +02:00
m
8123d71d08 Merge: t-paliad-124 — project filter walks descendants (Client filter → all child rows) 2026-05-04 18:58:04 +02:00
m
a69fff73e9 feat(t-paliad-124): project filter includes descendant projects
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.
2026-05-04 18:57:06 +02:00
m
1bba9cb3ce feat(t-paliad-123): apply date-bucket Status filter to appointments
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).
2026-05-04 18:56:25 +02:00
m
d286da34d5 Merge: t-paliad-121 — UPC court vacations no longer shift deadlines (Court continues to operate) 2026-05-04 18:52:57 +02:00
m
7461c4af49 fix(t-paliad-121): stop shifting deadlines for UPC court vacations
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.
2026-05-04 18:48:23 +02:00
m
0587fc2296 Merge: t-paliad-119 — Fristenrechner shift-reason explainer (UPC vacation, holiday name, weekend) 2026-05-04 18:37:17 +02:00
m
d688ebde90 feat(t-paliad-119): explain WHY a Fristenrechner deadline was shifted
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.
2026-05-04 18:31:55 +02:00
m
6940c1e030 Merge: t-paliad-120 — /events Termin-Typ chip dark-mode color fix 2026-05-04 18:28:01 +02:00
m
f79dbdba4a fix(t-paliad-120): /events Termin-Typ chip — dark-mode text invisible
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).
2026-05-04 18:25:59 +02:00
m
ecfd62e330 Merge: t-paliad-118 — /admin/email-templates Verfügbare Variablen single-column stack 2026-05-04 18:13:12 +02:00
m
7581444cd4 Merge: t-paliad-117 — /events filter polish (i18n leak + label position + panel anchoring) 2026-05-04 18:08:20 +02:00
m
e9e445fddf fix(t-paliad-118): /admin/email-templates Verfügbare Variablen — single-column stack
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.
2026-05-04 18:08:06 +02:00
m
5875a62aba fix(t-paliad-117): /events filter polish — i18n leak, label position, panel anchoring
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.
2026-05-04 18:02:26 +02:00
m
0ead001811 fix(dashboard,events): drop "Termine auf einen Blick" rail + fix multi-panel overflow leak
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.
2026-05-04 17:08:34 +02:00
m
c09150e744 Merge: t-paliad-116 — event_deadlines i18n follow-up (title_de + notes_en) 2026-05-04 17:04:19 +02:00
m
4e1213fbd1 fix(t-paliad-116): event_deadlines i18n follow-up — title_de backfill + notes_en
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)
2026-05-04 17:03:58 +02:00
m
df2b2114df Merge: t-paliad-114 follow-up — SMEFee=2015 (reduced fee per Rule 7a(2)(a-d))
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.
2026-05-04 16:58:49 +02:00
m
2f44461275 fix(t-paliad-114): EPA Beschwerdegebühr 2.255€ → 2.925€ (2024-04-01 EPO restructure)
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).
2026-05-04 16:57:58 +02:00
m
fe8ba15477 fix(t-paliad-114): update EPA Beschwerdegebühr to 2.925€ (post 2024-04-01)
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.
2026-05-04 16:32:38 +02:00
m
53f7eae665 fix(t-paliad-111): renumber colliding migration 032→034 — production was down
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).
2026-05-04 14:57:54 +02:00
m
c554e865eb Merge: t-paliad-111 — bug bundle correctness (UPC GESAMTKOSTEN, court-set dates, REGEL save) 2026-05-04 14:42:51 +02:00
m
0be2dfb5a0 fix(t-paliad-111): bug bundle (correctness) — UPC GESAMTKOSTEN, court-set dates, REGEL save flow
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
2026-05-04 14:42:29 +02:00
m
eb6e194684 Merge: t-paliad-115 PR-2 — canonical /events URL + redirect old paths 2026-05-04 14:40:57 +02:00
m
56522adffe feat(t-paliad-115): canonicalise list URL on /events; redirect old paths
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 ./...`.
2026-05-04 14:40:53 +02:00
m
b88afe8ddd Merge: t-paliad-112 — i18n bug bundle (deadline_notes_en, trigger-event DE, Checkliste PROJECT) 2026-05-04 14:36:57 +02:00
m
341fa6c26f fix(t-paliad-112): i18n leaks — deadline_notes_en, trigger-event DE, Checkliste header
Three i18n bugs from the t-paliad-101 QA sweep, fixed together:

B2 — Fristenrechner deadline notes leaked German into the EN locale.
Migration 032 adds paliad.deadline_rules.deadline_notes_en (TEXT NULL)
and backfills English translations for all 30 rules that carry a
deadline_notes value (UPC RoP / EPC / ZPO terminology). The frontend
prefers _en when locale=EN and falls back to deadline_notes (DE) when
the column is NULL, so future seeds without an EN translation render
in DE rather than empty. UIDeadline DTO gains notesEN. The bulk
"Als Frist(en) speichern" CTA now stores the locale-matched note text
so EN users get an EN note alongside the EN title.

B8 — trigger-event picker labels were English-only when DE locale was
active (102 rows, name_de defaulted to '' in 028, frontend already had
the locale switch but no data). Migration 033 backfills name_de for
all 102 trigger events using standard German UPC RoP terminology
(Klageschrift, Klageerwiderung, Replik, Duplik, Nichtigkeitswiderklage,
Verletzungswiderklage, Berufungsschrift/-begründung, Anschlussberufung,
Schutzschrift, Beweissicherung, etc.).

S3 — frontend/src/client/checklists-instance.ts:154 had a hardcoded
"Project" label in both branches of the locale ternary; the DE branch
now reads "Projekt", matching the surrounding meta-item labels' pattern
(Court / Authority → Gericht / Behörde, Reference → Rechtsgrundlage).
2026-05-04 14:36:50 +02:00
m
45979caf81 Merge: t-paliad-115 PR-1 — events view ⊥ filter + unified calendar view 2026-05-04 14:34:52 +02:00
m
1dad1c7371 feat(t-paliad-115): events view ⊥ filter — view selector + unified calendar view
PR-1 of t-paliad-115. Separates the view axis (cards / list / calendar)
from the filter axis (event type, status, project) on EventsPage. Closes
the duplicate "Kalenderansicht" button that t-paliad-110 left behind on
the Beides view.

Bug source: frontend/src/events.tsx:53-68 rendered TWO separate
"Kalenderansicht" anchors (events-action-deadline-cal +
events-action-appointment-cal) and frontend/src/client/events.ts:533-534
unhid both when type=all. Reproduced live on paliad.de — screenshot at
.playwright-mcp/paliad-115-duplicate-kalenderansicht-before.png.

Architectural change.
- Removed the per-type calendar buttons from the page header.
- Added a 3-button segmented view selector (Karten / Liste / Kalender)
  next to the type chips. The two controls now sit on a shared
  events-axis-row that flexes side-by-side and stacks on narrow viewports.
- View state lives in `currentView`, defaults to "cards", reads from
  ?view= on init, persists to URL on change. Default ("cards") stays
  out of the URL so existing bookmarks don't change.
- Cards view = original (5-card summary + table). List view = table
  only (cards hidden). Calendar view = month grid; cards + table both
  hidden.

Calendar view.
- Plots both deadlines (due_date) and appointments (start_at local
  date) on a Mo–So month grid. Reuses the existing .frist-cal-* CSS
  scaffold from /deadlines/calendar; only new addition is
  .events-cal-dot-appointment for the appointment hue (--bucket-next-week).
- Inherits the page's filters — `?view=calendar&type=appointment` shows
  appointment-only; `?view=calendar&status=all` shows everything; etc.
  Status, type, project, event-type filters apply orthogonally to view,
  matching the spec's "two axes combine" requirement.
- Click a day with items → existing modal pattern lists them with type
  chip + project ref; clicking an item navigates to its detail page.
- Month nav (prev / next / today) is purely client-side — no refetch,
  cheap pagination over the already-loaded items.

Out of scope (per task brief).
- Standalone /deadlines/calendar + /appointments/calendar pages stay
  untouched. PR-2 (URL canonicalisation) handles that surface.
- Custom time-window controls — future iteration per t-110 spec.

Build clean: `cd frontend && bun run build` + `go build/vet/test ./...`.
2026-05-04 14:34:44 +02:00
m
9dbc55638a Merge: t-paliad-113 — bug bundle polish (save modal + Streitwert + EPO + EN dates) 2026-05-04 14:32:16 +02:00
m
2c346672bb fix(t-paliad-113): bug-bundle polish — save modal court-set + project prefix, negative Streitwert, EPO en label, ISO en dates
5 small polish bugs from the t-paliad-101 QA sweep, bundled as one PR.

B4 — Fristenrechner save modal pre-checked court-determined entries:
treat dl.party === "court" as court-determined alongside dl.isCourtSet
so Zwischenverfahren / Mündliche Verhandlung / Entscheidung pre-uncheck
+ disable in the modal. Their meta now reads "vom Gericht bestimmt"
(deadlines.court.set) instead of the urgency placeholder.

B5 — Save modal project dropdown empty-code prefix: render "(reference)
— title" only when reference is non-empty; bare title otherwise. No
more leading "— Title" rows.

B7 — Negative Streitwert in Kostenrechner: blur handler now snaps
negative entries by clearing the input + resetting the slider to its
min and re-running calc, so a "-50000" entry no longer leaves stale
positive results onscreen. Input handler also drops the slider to min
on negatives so the visual state stays in sync mid-typing.

S2 — Courts EN page filter chip "EPA" → "EPO": added
gerichte.filter.epa i18n key (DE: EPA, EN: EPO) and wired it on the
courts.tsx pill.

S4 — Fristenrechner EN date format: switch from en-GB (dd/mm/yyyy,
ambiguous for US readers) to ISO ("Tue, 2026-09-01"). DE format
unchanged.
2026-05-04 14:31:43 +02:00
m
7463831932 Merge: t-paliad-109 — design doc for Fristen/Termine unification 2026-05-04 14:23:01 +02:00
m
165f0c1717 Merge: t-paliad-110 fix — hide Überfällig on Beides view 2026-05-04 13:57:47 +02:00
m
82421b3c86 fix(t-paliad-110): hide Überfällig card on Beides view
Spec is explicit that Überfällig is deadline-view-only — only showing
it on type=deadline matches the rationale that 'overdue' is a deadline-
specific concept (appointments don't go overdue, they happen). The
previous logic also surfaced the card on Beides whenever the
deadline-side overdue count > 0, which would have rendered an alarm
card on a mixed view.

Caught during live verification on paliad.de.
2026-05-04 13:57:40 +02:00
m
b2dfc87f57 Merge: t-paliad-110 PR-4 — Dashboard Termine rail + Fristen rail refactor 2026-05-04 13:52:56 +02:00
m
57237a55a3 feat(t-paliad-110): refactor Dashboard rails — drop Erledigt card, add Später + Termine rail
PR-4 of the Fristen+Termine unification, closing out t-paliad-110.

Fristen rail (was 5 cards):
- Erledigt card removed (status=completed stays reachable via the EventsPage
  filter dropdown — no card on the rail per the new model)
- Später card added (pending deadlines past Mon-week-after, click filters
  to /deadlines?status=later)
- 4+1 final shape: Überfällig (conditional alarm) · Heute · Diese Woche ·
  Nächste Woche · Später

Termine rail (new): 3 cards — Heute · Diese Woche · Später. No Überfällig
(past appointments aren't urgent), no Nächste Woche (low-value distinction
for appointments per the design rationale). Cards click through to
/appointments?status=… so users land in the matching EventsPage view.

Backend (DashboardService.loadSummary):
- DeadlineSummary.CompletedThisWeek dropped, .Later added
- AppointmentSummary added (Today / ThisWeek / Later)
- One CTE-based query computes both rails alongside MatterSummary; bucket
  cutoffs share computeDeadlineBucketBounds with /api/events/summary +
  /api/deadlines/summary so all three surfaces stay in lockstep

Frontend:
- dashboard.tsx: Erledigt card removed, Später card + Termine section added
- client/dashboard.ts: types updated, renderAppointmentSummary added
- 4 new i18n keys (DE+EN): dashboard.summary.later +
  dashboard.appointment_summary.heading
- CSS: .dashboard-card-later (muted blue) + 3 .dashboard-card-appt-* rules
  reusing the existing --bucket-* tokens

go build/vet/test ./... clean. bun run build clean (1396 keys).
2026-05-04 13:52:49 +02:00
m
88af8d3487 Merge: t-paliad-110 PR-3 — mount EventsPage on /deadlines + /appointments 2026-05-04 13:48:58 +02:00