Both surfaces now show the same buckets with the same labels and the same
cutoffs: Überfällig (conditional, alarming) · Heute · Diese Woche ·
Nächste Woche · Erledigt. Single-source bucket math via
computeDeadlineBucketBounds — Heute = today, Diese Woche = tomorrow
through the upcoming Sunday inclusive, Nächste Woche = next Monday
through next Sunday inclusive, all disjoint. Items past next Sunday
are visible only via "All open"/"Upcoming" filters; the Überfällig
card stays hidden when count == 0 and switches to a saturated red
pulse + bold white text when count > 0.
Filter dropdown on /deadlines gains today / next_week entries; old
"upcoming" filter still works as a back-compat alias for everything
pending past this Sunday so legacy bookmarks don't 4xx.
Tests: 8 deterministic table cases for the bucket pivots (every
weekday + a 21-day disjointness walk).
Überfällig is an emergency category — never a normal-state tile. Replace the
prior `.dashboard-card-quiet` dim-but-visible behavior with two states:
- overdue === 0 → card removed from the layout (`*-overdue-hidden`).
The Dashboard summary grid and the Fristen summary cards now use
`repeat(auto-fit, minmax(180px, 1fr))` so the row re-flows to 3 cards
instead of leaving an empty 4th column.
- overdue > 0 → saturated red surface, white text, soft pulsing red ring
via `paliad-alarm-pulse`. Honors `prefers-reduced-motion`. Dark theme
uses a slightly lighter red so the alarm still pops on midnight.
Applied on both surfaces (`#dashboard-card-overdue` and the Fristen
`.frist-summary-card[data-status="overdue"]`).
Reverses the t-paliad-083 carve-out that kept the sidebar on midnight in
both themes. m's feedback: in light mode the dark midnight column read as
a separate panel bolted onto the cream body. The brand lime stripe on the
active item is preserved (decoupled from --sidebar-text-active onto
--color-accent) so the active cue stays anchored across themes.
Light mode: --sidebar-bg = --color-bg-subtle (one step up from body
cream), --sidebar-text/-muted track the body palette, --sidebar-text-active
= midnight (full contrast against muted-grey inactive items),
hover/input/scrollbar tints switch to midnight-channel alphas.
Dark mode: original midnight + cream + lime palette restored inside the
:root[data-theme="dark"] override block. No regression there.
Append a sibling note to the .entity-table contract (t-paliad-099)
explaining that pointer-event overlays for whole-card click break
text selection. Steer future hands at the row-handler pattern from
t-098/099/102/103 instead.
The t-102 ::before overlay (`inset: 0` on `.entity-event-link`) made the whole
card clickable but also captured pointer events on the title and description
text — users couldn't select-to-copy. Same trap noted in brunel's t-102
debrief: when text-selection matters, switch to a row-level click handler that
skips inner <a>/<button>, matching the .entity-table pattern from t-098/099.
CSS (global.css):
- drop `.entity-event-link::before` overlay
- drop `position: relative` from `.entity-event` and `position: static` from
`.entity-event-link` (no longer anchoring an absolute pseudo-element)
- keep cursor: pointer + hover-lift on `.entity-event:has(.entity-event-link)`
so the affordance still telegraphs "clickable"
- card hover-lift now keys off the card itself, not the link's :hover, so the
lift triggers from anywhere on the card (matching the new click surface)
- mirror `.dashboard-activity-item`: cursor + lime-tint hover row-highlight
TS:
- projects-detail.ts:renderEvents: after innerHTML, attach a row-level click
handler that reads `.entity-event-link.href` and skips clicks on inner
<a>/<button>. Cards without a link have no `.entity-event-link` and stay
non-clickable (cursor stays default via the `:has()` selector).
- dashboard.ts:renderActivity: same handler reading `.dashboard-activity-project.href`.
Acceptance:
- Title and description text on /projects/{id} → Verlauf cards and on
/dashboard activity rows is selectable again.
- Click anywhere on a card still navigates (no regression from t-102).
- Title link still navigates; Cmd-/Ctrl-click opens in new tab; keyboard
tabbing still hits the inner link.
- `cd frontend && bun run build` clean; `go build/vet/test ./...` clean.
Extends the t-paliad-097 metadata pattern from checklist_* events to the
remaining audit families. Project Verlauf and Dashboard activity feed now
deep-link each event to its originating entity:
- deadline_{created,updated,completed,reopened} → /deadlines/{id}
- appointment_{created,updated} → /appointments/{id}
- note_created → /appointments/{id} | /deadlines/{id} | /projects/{id}
(most-specific parent — notes have no standalone page)
Backend (Go):
- deadline_service.go / appointment_service.go: switch single-entity
mutation events from insertProjectEvent to insertProjectEventWithMeta
carrying {"deadline_id"|"appointment_id": uuid}.
- note_service.go:insertWithAudit: derive metadata from noteParent so
the audit row records {note_id, deadline_id|appointment_id|project_id}.
Frontend (TS):
- projects-detail.ts: extract eventDetailHref(); wrapEventTitleLink
delegates to it. Comment block lists every wired event family.
- dashboard.ts:activityHref: same routing rules as the project Verlauf.
- global.css: .entity-event becomes position:relative; the
.entity-event-link::before pseudo expands the link's hit area to the
full card so a click anywhere on the row navigates (matches what m
expected from "die Karte ist verlinkt"). Hover lifts border + shadow.
Excluded by design (mirrors checklist_deleted exclusion):
- *_deleted events — entity is gone.
- deadlines_imported — bulk event with no single deadline_id; would
need an aggregate target the product doesn't have today.
Pre-metadata rows stay non-clickable (no backfill — same precedent as
t-paliad-097).
m's call: most of yesterday's backfill entries weren't worth surfacing.
Removed: Checklisten von überall öffnen, Admin-Verwaltung der Event-Typen,
Mehr Verfahrensarten im Fristenrechner, Dunkler Modus, Rollen-Filter im
Team-Verzeichnis, Checkboxen in Formularen ausgerichtet.
Kept: Event-Typen für Fristen (2026-04-30, Feature) and UPC-Fristen
genauer berechnet (2026-04-30, Fix) — both have direct user impact on
the deadline workflow.
Eight new entries cover the user-facing work landed since the 2026-04-20
Settings entry: dark mode, /team Role filter, event types + admin
moderation panel, UPC RoP fixes, Tier 2 Fristenrechner ports, checklist
"Vorhandene Instanzen" tab, and the checkbox row-alignment fix.
Folded as siblings (per task hint): t-paliad-082 light-mode contrast
into the dark-mode entry; t-paliad-098 row-click into the t-paliad-097
checklist entry — both are small fixes on the same surface as their
sibling feature.
Internal/refactor merges in the window were skipped (t-080/091/092/093/
095/099 plus doc/dead-code/audit/smoke merges).
Tests: go test ./internal/changelog/... green (date-desc invariant
still holds). go build ./... + go vet ./... + bun run build clean.
Anchor the convention surfaced by t-paliad-098/099 so the next
hand on .entity-table sees it before adding a new table:
- frontend/src/styles/global.css: contract comment block above the
default cursor:pointer rule explaining the navigate-or-readonly choice
- .claude/CLAUDE.md: new "Frontend conventions" section pointing at
the CSS and the row-handler pattern in client/checklists.ts +
client/projects-detail.ts
No code changes; pure docs.
Audit follow-up to t-paliad-098. The global `.entity-table tbody tr`
rule set `cursor: pointer` and a hover background on every row, but six
tables across the admin and settings surfaces don't navigate on row
click — actions live in inline buttons (admin-team, admin-event-types,
admin-partner-units) or the rows are pure read-only summaries (admin
audit log, CalDAV sync log). The cursor lied and the hover invite was
empty.
- Add `.entity-table--readonly` modifier in global.css that resets
cursor and neutralises the hover background, including a dark-theme
override since the existing `:root[data-theme="dark"] .entity-table
tbody tr:hover` rule outranks the base modifier on specificity.
- Apply the modifier to the six table instances that don't navigate.
The eight tables that DO navigate (projects, deadlines, appointments,
checklists templates+instances, project-detail's deadlines/appointments
/checklists) already have row click handlers and keep the default
clickable affordance.
The .entity-table tbody tr CSS rule sets cursor: pointer on every row,
but the three checklist-instance tables only wired navigation to the
name cell's <a>. Clicks on Vorlage / Fortschritt / Angelegt cells looked
clickable yet did nothing.
Added row-level click handlers (skipping inner <a>/button so the
project-link cell and delete button still work) in:
- checklists.ts (Vorhandene Instanzen tab)
- projects-detail.ts (Checklisten tab on project detail)
- checklists-detail.ts (instance list on template detail)
Search-result anchors (handlers/search.go) already worked since they
are real <a> elements, no row handler needed.
Two related checklist UX gaps:
1. Checklist events in a project's Verlauf tab were unclickable — and
nothing in the project_events row carried the originating instance ID.
Add an `insertProjectEventWithMeta` helper, write
{"checklist_instance_id": <uuid>} as project_events.metadata for
checklist_created / _renamed / _linked / _unlinked / _reset (skipped
for _deleted — instance is gone). Surface metadata on
/api/projects/{id}/events and on dashboard recent_activity. The
Verlauf renderer wraps the title in <a href="/checklists/instances/{id}">
when metadata.checklist_instance_id is present, and the dashboard's
activity feed deep-links the project ref to the instance directly for
checklist_* events. Existing rows (metadata `{}`) stay non-clickable —
no migration backfill needed.
2. /checklists previously demanded a template pick before any existing
instance was reachable. Add a tab nav (Vorlagen / Vorhandene Instanzen)
using the existing entity-tab pattern. New endpoint
GET /api/checklist-instances and ChecklistInstanceService.ListAllVisible
return every visible instance across templates + projects, joined with
project ref/title and sorted by created_at DESC. Rows show template,
instance name (linked), project link (or "Persönlich"), progress bar,
and created date. URL state (?tab=instances) keeps the active tab
shareable. EN + DE i18n covered for tab labels and column headers.
Also adds event.title.checklist_* localizations for the Verlauf header
that translateEvent looks up.
`.form-field input` set `width: 100%` and `padding: 0.55rem 0.75rem` on
all inputs — that includes checkboxes and radios. The visual checkbox
visibly stretched to fill the form column on /settings (Benachrichtigungen
tab) and inside the Fristenrechner "Fristen übernehmen" save modal,
pushing the label text out of place.
Add a targeted override that restores natural sizing for type=checkbox
and type=radio inside `.form-field`. Also bump `.caldav-toggle-label`
specificity (selector → `label.caldav-toggle-label`) so its
`inline-flex; align-items: center; gap: 0.5rem` actually wins over the
generic `.form-field label { display: block }` rule — without that the
checkbox + label kiss with no gap.
Surfaces verified via Playwright on paliad.de:
- /settings?tab=benachrichtigungen — Frist-Erinnerungen master + 3 sub-toggles
- /tools/fristenrechner — "Fristen übernehmen" save modal rows
F-7 of the t-paliad-074 architecture audit. Sweeps the last German-named
CSS leftovers — purely a class-name change, no behaviour or styling
delta. 466 references across global.css and ~30 TSX/TS files.
Naming rules applied:
- Generic table/tabs/form/empty/controls/detail/events/status/type/
suggestion/chip/col/ref/search-wrap/select/soon/loading/muted/
unavailable/row/header-row/title-input -> .entity-*
- Truly generic widgets dropped the prefix: .multi-* (multi-select
panel), .filter-*, .collab-* (collaborator picker; bare class is
now .collab-picker), .firmwide-*, .office-*, .back-link
- Project-specific names kept specific: .party-form/-controls/-table
(parties on a project), .checklists-hint, .netdocs-link
- Page-scoped IDs in projects.tsx -> projects-search/-count/-body;
projects-new.tsx -> project-new-form/-msg
- German content "akten-bezogen" tightened to "aktenbezogen" (one-word
form is also valid German) so the strict grep stays clean
F-6 from t-paliad-074 architecture audit. The Gitea repo was renamed
m/patholo → mAi/paliad → m/paliad, but go.mod still declared
`mgit.msbls.de/m/patholo` and every internal import echoed the
pre-rebrand name.
Sweep:
- go.mod: module path → mgit.msbls.de/m/paliad
- All *.go files: imports rewritten via sed
- README.md, docs/design-kanzlai-integration.md: mAi/paliad → m/paliad
- Frontend issue-reference comments (mAi/paliad#N → m/paliad#N) in
i18n.ts, theme.ts, sidebar.ts, app.ts, Sidebar.tsx, PWAHead.tsx,
global.css
Verified: go build/vet/test ./... clean, bun run build clean,
no remaining mgit.msbls.de/m/patholo or mAi/paliad references
outside docs that intentionally describe the rename history.
Per audit recommendations §6.2 (rec 5-9). Ports 14 RoP rules from
youpc's deadline calc into paliad.deadline_rules so they're surfaceable
in the timeline (course-of-proceedings) Fristenrechner mode in addition
to the existing trigger-event mode.
Adds 4 new proceeding types and extends 2 existing trees:
- UPC_DAMAGES (new) — Schadensbemessung (R.137.2, R.139 reply, R.139 rejoin)
- UPC_DISCOVERY (new) — Bucheinsicht (R.142.2, R.142.3 reply, R.142.3 rejoin)
- UPC_COST_APPEAL (new) — Berufung Kostenentscheidung (R.221.1)
- UPC_APP_ORDERS (new) — 15-day order-flavor (R.220.2, R.220.3, R.237 b, R.238.2)
- UPC_INF extended — R.151 chained off inf.decision
- UPC_APP extended — decision-flavor cross-appeal pair (R.237 a, R.238.1)
Total: 14 RoP rules across 5 families. New types appear in the
proceeding-type picker via deadlines.upc_damages / upc_discovery /
upc_cost_appeal / upc_app_orders i18n keys (DE + EN).
Design notes in the migration explain why some rules live in their own
proceeding type (when the legal anchor differs from UPC_INF/UPC_APP's
trigger date) vs being chained off existing rules.
Q6 of t-paliad-088 left firm-wide event_type creation open to any user; this
ships the moderation surface admins use to dedupe and clean up the resulting
drift.
Service layer (internal/services/event_type_service.go):
- ListAllForAdmin(filter) — firm-wide rows with usage_count and
author_display_name, optionally including archived (single query, scalar
subquery + LEFT JOIN paliad.users). Sorted live-first, then category +
label_de.
- ListPrivatePendingPromotion — every private non-archived row across all
users, sorted by usage_count DESC.
- ArchiveBulk(ids) — UPDATE archived_at=now() WHERE is_firm_wide AND NULL.
- Promote(id) — flip is_firm_wide=true; surfaces ErrEventTypeSlugTaken on
collision so the admin can merge instead.
- Restore(id) — flip archived_at back to NULL; same slug-collision surface.
- MergeIDs(winner, losers) — tx-scoped INSERT … SELECT … ON CONFLICT
redirect of deadline_event_types from losers → winner, then DELETE on the
loser junction rows, then archive the losers. Refuses if the winner is
archived or private. Junction PK does the dedup.
- requireAdmin gate runs at every method (defence-in-depth on top of the
handler-level RequireAdminFunc).
Handlers (internal/handlers/admin_event_types.go):
- GET /api/admin/event-types[?include_archived=1]
- GET /api/admin/event-types/private
- POST /api/admin/event-types/archive { ids:[…] }
- POST /api/admin/event-types/merge { winner_id, loser_ids:[…] }
- POST /api/admin/event-types/{id}/promote
- POST /api/admin/event-types/{id}/restore
- GET /admin/event-types page shell.
All wrapped behind auth.RequireAdminFunc at registration time.
Frontend:
- New /admin/event-types SPA (admin-event-types.tsx + client/admin-event-types.ts):
search, "Archivierte anzeigen" toggle, per-row archive/restore, bulk
archive, merge modal (winner picker defaults to highest-usage row),
separate table for private types pending promotion.
- Sidebar entry under Verwaltung; admin landing card.
- ~50 i18n keys DE+EN under admin.event_types.* + nav.admin.event_types.
- CSS for archived badge, merge option list, bulk-actions bar.
Out of scope (deferred): public "merge request" workflow for non-admins.
F-3 from t-paliad-074 architecture audit. NoteService used to call
ProjectService.GetByID and AppointmentService.GetByID just for the
visibility bit (~6 cross-service full-row reads in note_service.go).
Each was a full SELECT on the parent row when only a boolean was needed.
Add CanSee(ctx, userID, id) (bool, error) on the two parent services:
single EXISTS round-trip, no projection. Personal Appointments stay
visible only to their creator; project-anchored Appointments inherit
the project's visibility predicate (global_admin shortcut + team-walk).
NoteService gains two private helpers — requireProjectVisible and
requireAppointmentVisible — that wrap CanSee + ErrNotVisible. All
visibility-only sites in note_service.go (ListForProject /
ListForDeadline / ListForAppointment / ListForProjectEvent /
CreateForProject / CreateForDeadline / requireVisible) now go through
the helpers.
CreateForAppointment keeps appointment.GetByID — it legitimately needs
the appointment's project_id for the audit-event row.
DeadlineService.CanSee was not added: note_service never reaches into
the deadline service for visibility (it does its own SELECT project_id
FROM paliad.deadlines and gates via the project predicate).
Test: cansee_test.go covers the gate level for both new methods —
admin sees everything (global_admin shortcut), team member sees their
team's, non-member sees nothing, missing IDs are invisible to all,
personal appointments are private to creator.
Shared client module client/event-types.ts exposes three surfaces:
1. attachEventTypePicker — multi-tag chip cluster with typeahead suggest
and an inline "+ Neuen Typ hinzufügen…" affordance. Mounted on
/deadlines/new and the /deadlines/{id} edit modal.
2. attachEventTypeMultiSelectFilter — listbox-panel filter (search + Alle
+ Ohne Typ + grouped checkbox list, click-outside / Escape dismiss).
Mounted on /deadlines and /agenda. Trigger styled like the existing
<select>s; serialises to ?event_type=<uuid>,<uuid>,none.
3. openAddEventTypeModal — modal with label_de/label_en/category/
jurisdiction/firm_wide. Live duplicate-warning fed by
/api/event-types/suggest (Q6 mitigation). Firm-wide checkbox is
only rendered for global_admin (per the design's permission model).
Added Typ column on /deadlines (hidden when no visible row carries an
event_type — matches the t-paliad-073 hide-on-uniform pattern).
Added Typ display + edit on /deadlines/{id}; PATCH now sends
event_type_ids when the picker is mounted.
i18n: 36 new keys (DE+EN) under event_types.* + deadlines.field/col/
filter.event_type + agenda.filter.event_type + common.cancel.
CSS in global.css: .event-type-picker / .event-type-chip /
.akten-multi-trigger / .akten-multi-panel / .akten-event-type-pill /
.event-type-add-modal. Mobile (<640px) collapses the panel into a
bottom sheet.
bun run build clean (1302 i18n keys regenerated, data-i18n scan clean).
go build / go vet / go test ./... clean (PR-1 still green after rebase).
Migration 030 adds paliad.event_types and paliad.deadline_event_types
junction. ~43 firm-wide seeds biased toward submissions (25 UPC
submissions + 8 UPC decisions/orders/hearings + 5 EPO + 4 DPMA/DE + 1
cross-jurisdiction). UPC-seeded rows carry a loose trigger_event_id
column (no FK constraint per Q2: event_types leads, trigger_events
follows). RLS policies are defense-in-depth — primary enforcement is
in the Go service layer. Per Q6, any authenticated user can create
firm-wide types; admins moderate via the soft-delete archive lever.
EventTypeService: List (firm-wide ∪ own-private), GetByID, Create
(slug auto-derived, supports diacritics → ASCII), Update (author OR
admin-on-firm-wide), SuggestSimilar (powers the duplicate-warning in
the add modal), AttachToDeadlineTx + ValidateForUser + ListForDeadlines
for the junction.
DeadlineService gains an EventTypeService dependency and now:
- accepts event_type_ids on Create / Update / CreateBulk
- attaches them in the same transaction as the deadline insert
- hydrates EventTypeIDs on every Get / List / ListForProject
- supports the multi-select Typ filter via ListFilter.EventTypeIDs +
IncludeUntyped (UNION semantics within types, AND-intersected with
Status/Project)
AgendaService gets the same Typ filter on its deadline side;
appointments are unaffected.
API:
- GET /api/event-types?category=&jurisdiction=
- GET /api/event-types/suggest?q=
- POST /api/event-types
- PATCH /api/event-types/{id} (set archive=true to hide)
- GET /api/deadlines?event_type=<uuid>,<uuid>,none
- GET /api/agenda?event_type=<uuid>,<uuid>,none
- POST/PATCH /api/deadlines accept event_type_ids: [uuid]
go build / go vet / go test ./... clean.
Frontend (picker + custom-add modal + multi-select filter) follows in
PR-2. Admin moderation panel deferred to t-paliad-089 follow-up.
m greenlit all 7 open questions on 2026-04-30 12:23. Notable changes
from the initial draft:
- Submissions are explicitly the primary Event-Type use case, not a
secondary discriminator. m: "those are the event types I mean,
mainly". Deferring a separate paliad.submissions table stands.
- /deadlines + /agenda Typ filter is MULTI-SELECT (UNION across
selected types, AND-intersected with Status/Projekt). New
EventTypeMultiSelect component spec'd in §4: trigger button styled
like the existing <select>s, popover with search + grouped checkbox
list. Status/Projekt stay single-select.
- Firm-wide Event-Type creation OPEN to any authenticated user. RLS
insert policy simplified to created_by=self. Admins moderate via
archive. Mitigation: duplicate-warning in the add modal. Follow-up
t-paliad-089 flagged for admin moderation panel.
- Broader-scope seeds confirmed (UPC + EPO + DPMA + DE + contract).
- §12 rewritten as a resolution table.
The first PR caught all `var(--color-bg-muted, #fallback)` sites. This catches
the 5 remaining `var(--color-bg-muted)` sites *without* fallback in the admin
email templates page (.admin-et-card-key, .admin-et-card-lang-btn:hover,
.admin-et-variable-type, .admin-et-preview-subject, .admin-et-version-row:hover).
Without fallback, an undefined custom property resolves to `unset` →
`transparent`, so these elements rendered with no visible background in
either mode (rather than light-grey in light mode like the fallback-form
variant). Same root cause though: the `--color-bg-muted` token name was
never defined anywhere.
All 10 sites (5 with hex fallback + 5 without) now use `--color-surface-muted`.
Build clean.
Standalone paliad.event_types table with nullable FK on paliad.deadlines,
seeded from a curated subset of paliad.trigger_events (UPC submissions +
decisions) plus hand-written EPO/DPMA/DE-national/contract entries.
Picker on /deadlines/new + edit modal with grouped options + inline
custom-add modal (private types for any user, firm-wide gated to
global_admin). Filter <select> on /deadlines (matching existing
Status/Projekt pattern, not pills) and pill-row on /agenda. Submissions
are NOT a separate entity — category='submission' on event_types carries
the discrimination until a real Schriftsatz-Verwaltung is built.
Awaiting m's go/no-go on §12 before any implementation.
`/team` count pills (member counts per office / per partner unit) rendered with
`var(--color-bg-muted, #f4f4f7)` — but `--color-bg-muted` was never defined, so
the literal `#f4f4f7` always won and the pills stayed light-grey in dark mode.
Sweep across `frontend/src/styles/global.css`:
- 5x `--color-bg-muted, #f3f4f6|#f4f4f7` → `--color-surface-muted` (the actual
themed chip-bg token; light: #f3f4f6, dark: 5% cream over midnight).
Sites: `.team-group-count`, `.team-dept-tag`, `.admin-soon-badge`,
`.admin-audit-event`, `.admin-audit-source`.
- Trigger-event Fristenrechner block (added in PR-2 / t-paliad-086) used a
parallel set of fictional tokens (`--surface-color`, `--surface-soft`,
`--accent-soft`, `--text-color`, `--text-muted`, `--border-color`,
`--accent`, `--accent-text`, `--border-light`) that were never defined,
so the entire panel rendered in fallback hex literals — white card,
light-grey duration chip, pale-lime rule-code, dark `#111` body text.
In dark mode the bg/border/divider stayed light while text stayed dark,
on a midnight body — unreadable.
Re-pointed all 27 sites onto the project's `--color-*` token system.
- `rgba(0, 0, 0, 0.0X)` literal overlays (hover/active states for
`.search-result`, `.palette-action`, `.quick-add-row/-cancel`,
`.pwa-install-dismiss`, `.termin-type-chip/-badge`, `.termin-personal-tag`,
`.caldav-status-card`) → `--color-overlay-faint|subtle` (the existing
tokens that flip to white-channel alpha in dark mode).
- Removed redundant hex fallbacks on already-themed tokens
(`var(--color-surface, #ffffff)` → `var(--color-surface)` etc) — the
`:root[data-theme="dark"]` block already defines all of them.
Acceptance:
- `cd frontend && bun run build` → clean.
- Sweep-greps from the task brief now return 0 hits (excl. one comment).
- No new tokens introduced — reuses the t-paliad-083 / t-paliad-082 palette.
Refs t-paliad-087.
Implements the four audit recommendations from §6.1 of
docs/audit-fristenrechner-completeness-2026-04-30.md plus a holiday-
adjustment cap fix surfaced by PR-2's smoke test.
(1) UPC_INF CCR-conditional rejoinder
Public Fristenrechner now flips inf.reply (RoP.029.b → RoP.029.a) and
inf.rejoin (1mo / RoP.029.c → 2mo / RoP.029.d) when the user ticks
"Mit Widerklage auf Nichtigkeit." Implemented via a new
`condition_flag` column on paliad.deadline_rules: when the rule names
a flag and the API request's flags array contains it, the calculator
substitutes alt_duration_value/unit and alt_rule_code. Independent of
the existing `condition_rule_id` mechanism (which references a real
rule in the same proceeding tree — only useful for matter-attached
trees that already seed the CCR rule).
(2) UPC_APP / internal APP grounds anchoring
`app.grounds` is now anchored on the trigger date (the appealed
decision) with a 4-month duration, not chained 2mo after `app.notice`.
Per RoP 220.1 the legal rule is "4 months from notification of the
decision," independent of when the notice itself was filed. The chain
only happened to give the right answer when both legs landed on a
working day; under holiday rollover (e.g. notice deadline pushed to
Monday) the grounds deadline drifted off the 4mo legal target.
(3) EP_GRANT publish anchor on priority date
New `anchor_alt` column on paliad.deadline_rules. ep_grant.publish
carries `anchor_alt='priority_date'`. The Fristenrechner UI surfaces
an optional "Prioritätstag" input (visible only when EP_GRANT is
selected) that, when populated, anchors the publish-A1 calculation on
the priority date instead of the filing. Falls back to filing date
when the priority field is empty (the case for purely-EP applications
with no foreign priority claim).
(4) Rule-code format normalisation
Migration 029 normalises 'RoP 23' → 'RoP.023', 'RoP 29b' / 'RoP.029b'
→ 'RoP.029.b', 'RoP 220.1' → 'RoP.220.1', etc. across deadline_rules.
Matches the canonical youpc format already used by the PR-1 imported
event-deadline rule codes.
(+) AdjustForNonWorkingDays cap bumped 30 → 60
Surfaced by the PR-2 smoke test: SoD on 2026-04-30 (3mo from trigger)
landed on Sat 2026-08-29 instead of Mon 2026-08-31. The 30-iteration
safety bound on AdjustForNonWorkingDays cannot walk past the 33-day
UPC summer vacation plus flanking weekends. Bumped to 60. Pure-Go
one-liner, locked by a follow-up production smoke (real
paliad.holidays seed has the UPC vacation).
Schema (migration 029): two new nullable text columns on
paliad.deadline_rules — `condition_flag` and `anchor_alt`. Both ignored
by every existing rule; only the rows updated above carry values.
Models: DeadlineRule gains ConditionFlag + AnchorAlt (nilable strings).
Service: FristenrechnerService.Calculate now takes a CalcOptions struct
(PriorityDateStr, Flags). API handler accepts optional priorityDate and
flags fields on POST /api/tools/fristenrechner.
Frontend: TSX surfaces the priority-date row + CCR checkbox conditionally
on selectedType (only EP_GRANT / UPC_INF respectively). Client TS reads
them and threads through the API call. New i18n keys for both DE+EN.
Migration 029 dry-run validated on prod Supabase (BEGIN/ROLLBACK):
schema + UPDATEs apply cleanly, rule states match expected post-fix
shape. Tests + go build/vet + bun build all clean.
Adds the second Fristenrechner mode (mirrored from youpc.org's deadline
calc): pick a UPC trigger event + date, see all deadlines that flow
from it. Coexists with the existing course-of-proceedings timeline mode
via a tab toggle on /tools/fristenrechner.
Backend:
- internal/services/event_deadline_service.go — EventDeadlineService.
ListTriggerEvents (alphabetical), Calculate (resolves all deadlines
flowing from a trigger). Routes through HolidayService for weekend +
holiday rollover. Honours the new working_days unit. Resolves
composite rules (alt_* + combine_op) by computing both legs and
picking max/min. Used by R.198/R.213 ("31d OR 20wd, whichever is
longer") imported in PR-1.
- internal/services/event_deadline_service_test.go — covers
addWorkingDays (forward, backward, zero, holiday-skip), composite
rule semantics, before-timing.
- internal/handlers/fristenrechner.go — two new endpoints:
GET /api/tools/trigger-events, POST /api/tools/event-deadlines.
- handlers.Services / dbServices: new EventDeadline / eventDeadline
field; wired in cmd/server/main.go from the same HolidayService.
Frontend:
- frontend/src/fristenrechner.tsx — tab strip + second wizard panel
(3 steps: trigger picker → date → flat result list).
- frontend/src/client/fristenrechner.ts — initEventMode wiring,
typeahead filter over the 102 trigger events, Calculate flow,
bilingual rendering, composite-rule labels, lang-change refresh.
- frontend/src/client/i18n.ts — 27 new keys (DE+EN) under
deadlines.mode.* and deadlines.event.* (incl. units, timing).
- frontend/src/styles/global.css — fristen-mode-tabs, mode-panel,
event-list, event-result-row visual style.
Working-day arithmetic detail: the new addWorkingDays helper steps
one day at a time and skips runs of non-working days (Sat/Sun + DE
federal + UPC vacations seeded via paliad.holidays). Day-zero is the
caller's job — addWorkingDays(0) returns the input unchanged so
callers can decide whether to roll forward via AdjustForNonWorkingDays.
Composite-rule resolution: when a row carries alt_duration_value +
alt_duration_unit + combine_op, Calculate computes both legs,
picks max/min, and surfaces a compositeNote like
"max(31 days, 20 working_days) → working_days leg" so the UI can
explain which leg won.
PR-3 will land Tier 1 bug fixes from the audit (CCR adaptive,
UPC_APP grounds anchoring, EP_GRANT priority, rule-code normalisation).