Commit 6 of 8. Renders the approval-pending warning pill on the two
busiest list surfaces:
- /events (deadline + appointment list): ⚠ pill next to the title +
soft-tinted row via .entity-row--pending-update modifier.
- /agenda (timeline): ⚠ pill in the headline + same row tint.
Changes:
- internal/services/event_service.go: EventListItem gains
ApprovalStatus *string; projectDeadline / projectAppointment
populate it from the embedded model.
- internal/services/deadline_service.go ListVisibleForUser: SQL adds
f.approval_status / pending_request_id / approved_by / approved_at
to the SELECT so DeadlineWithProject hydrates them.
- internal/services/appointment_service.go ListVisibleForUser: same
for appointments + completed_at.
- internal/services/agenda_service.go: AgendaItem gains
ApprovalStatus; the per-source SQL queries select it; the
loadDeadlines / loadAppointments projection sets it.
- frontend/src/client/events.ts renderRow: adds entity-row--pending-update
modifier and an inline approval-pill on the title cell when status='pending'.
- frontend/src/client/agenda.ts renderItem: same treatment on the
agenda-item headline.
Generic "pending update" label (approvals.pending_update.label) — not
lifecycle-specific. The inbox carries the lifecycle detail. Showing
just one pill keeps the visual signal clear; an approver scanning a
list of pending entities sees them at a glance via the row tint, then
clicks through to /inbox to see what's pending and act.
Detail pages (/deadlines/{id}, /appointments/{id}) and /dashboard
deadline rail — pill rendering for those surfaces deferred to a
follow-up to keep this commit focused. Rendered everywhere it
matters most for daily use.
Commit 5 of 8. End-user surface for the approval workflow:
- /inbox page (frontend/src/inbox.tsx + client/inbox.ts) with two tabs:
"Zur Genehmigung" (requests I qualify to approve) and "Meine
Anfragen" (requests I submitted). Each row shows the project, entity
title, lifecycle event, requester name + age, the date-field diff
(for update/complete/delete) and the relevant action buttons:
approve + reject when on pending-mine, revoke when on mine.
Historic rows render a status pill instead of buttons.
- Sidebar bell entry "Genehmigungen" (with sidebar-inbox-badge) under
the Übersicht group. sidebar.ts polls /api/inbox/count every 60s and
shows the count (or 9+ ceiling) when > 0.
- Server registration: GET /inbox → dist/inbox.html, gated by
gateOnboarded. Already-registered API endpoints (commit 4) handle
the data path.
- Bilingual (DE primary / EN secondary) i18n strings under
approvals.* — labels, status names, lifecycle names, role names,
decision-kind names, action verbs, error messages. ~50 new keys.
- Pending-state CSS classes: .approval-pill, .approval-pill--historic,
.entity-row--pending-{create,update,complete,delete},
#sidebar-inbox-badge. Soft-tint rows + amber pill so an approver
can scan a list of pending entities at a glance. Used by commit 6
(pending pills across surfaces) — no other surface picks them up
yet, but the styles are wired and ready.
- Sidebar.tsx navItem signature gains an optional badgeID parameter
so any future sidebar entry can host a count-badge with one extra
argument (no per-entry custom rendering).
Edit mode now exposes a project picker so a deadline or appointment can be
moved to a different matter. Backend Update accepts project_id (and
clear_project for appointments), validates visibility on the destination,
and emits *_project_changed audit rows on both the OLD and NEW project so
each side's Verlauf still shows the move.
Personal-to-project linking and project-to-personal unlinking are gated by
the existing personal-Appointment creator check; project-to-project moves
re-use the existing requireMutationRole gate plus a fresh visibility check
on the target.
Combined backend API for the upcoming policy-authoring page (commit 4)
and inbox + bell (commit 5). Registers:
Policy CRUD (admin-only via RequireAdminFunc gate):
- GET /api/projects/{id}/approval-policies
- PUT /api/projects/{id}/approval-policies/{entity_type}/{lifecycle}
- DELETE /api/projects/{id}/approval-policies/{entity_type}/{lifecycle}
Inbox (any authenticated user; service-layer query gates by visibility
+ role-tier match):
- GET /api/inbox/pending-mine — requests I qualify to approve
- GET /api/inbox/mine — requests I submitted
- GET /api/inbox/count — bell badge count
- GET /api/approval-requests/{id} — one hydrated request
Decisions (caller authorization checked at service layer; the CHECK
constraint on approval_requests blocks self-approval as a second
defence):
- POST /api/approval-requests/{id}/approve
- POST /api/approval-requests/{id}/reject
- POST /api/approval-requests/{id}/revoke
Error mapping (writeApprovalError):
- ErrSelfApproval → 403 self_approval_blocked
- ErrNoQualifiedApprover → 409 no_qualified_approver
- ErrConcurrentPending → 409 concurrent_pending
- ErrNotApprover → 403 not_authorized
- ErrRequestNotPending → 409 request_not_pending
Frontend pages (the policy authoring tab on /projects/{id}/settings
and the /inbox page with bell) follow in subsequent commits — the
endpoints are usable via curl + admin tooling immediately.
Commit 3 of 8. The 4-eye gate now actually fires. With migration 054
applied and an approval_policies row configured for a project, the
relevant Create/Update/Complete/Delete on a Deadline or Appointment
flips approval_status='pending' and emits a *_approval_requested audit
event. Without policies, behaviour is unchanged.
Backend changes:
- models.Deadline + models.Appointment gain approval_status,
pending_request_id, approved_by, approved_at; appointments also gain
completed_at (for the appointment:complete lifecycle event).
- deadlineColumns + appointmentColumns include the new fields so
every existing read path hydrates them via sqlx StructScan with
no per-call-site changes.
- DeadlineService gains SetApprovalService (nil-tolerant). Wired in
main.go after the bundle is built.
- AppointmentService gains the same hook + dependency.
Lifecycle wiring:
- DeadlineService.Create / Update / Complete / Delete each consult
the approval gate. Update only triggers approval when a date-bearing
field actually changes (Q4 allowlist: due_date, original_due_date,
warning_date). Cosmetic edits (title, description, notes,
rule_code, event_type_ids, status, completed_at via reopen) bypass.
- AppointmentService.Create / Update / Delete same shape. Update
only gates on start_at / end_at changes. Personal appointments
(project_id IS NULL) never gate (no project policy to consult).
- Delete is the one stage-then-write exception: the row stays alive
with approval_status='pending' until the approver hard-deletes
(approve) or restores it (reject). On no-policy projects, delete
is immediate as before.
- Concurrent-pending guard: any mutation on a row whose
approval_status='pending' returns ErrConcurrentPending. The user
must wait for the in-flight request to settle (or revoke if
they're the requester).
Pre_image capture: the date-bearing fields that are about to change
are snapshotted into the approval_requests.pre_image jsonb at submit
time. Reject/Revoke applies them back over the row to revert.
Commit 2 of 8 — the workflow engine for the 4-Augen-Prüfung. Wires the
service into the handlers.Services bundle so commit 3 can call into
SubmitCreate/Update/Complete/Delete from DeadlineService and
AppointmentService.
Public surface:
- Submit{Create,Update,Complete,Delete} — invoked by Deadline /
AppointmentService inside their existing tx. Looks up policy,
runs the deadlock check, inserts paliad.approval_requests, marks
the entity pending, emits the *_approval_requested project_events
audit row.
- Approve / Reject / Revoke — top-level operations (own tx). Approve
finalises the lifecycle (clears pending markers + sets approved_by
for non-delete; hard-deletes for delete). Reject / Revoke revert
the entity from pre_image (delete a pending-create, restore date
fields, NULL completed_at).
- ListPendingForApprover / ListSubmittedByUser / GetRequest /
PendingCountForUser — read paths the inbox + bell will hit in
commit 5.
- ListPolicies / UpsertPolicy / DeletePolicy — CRUD for the
authoring page in commit 4.
Self-approval is blocked at three layers:
1. canApprove() returns ErrSelfApproval when caller == requester.
2. The DB CHECK constraint approval_requests_no_self_approval.
3. The deadlock check excludes the requester from the pool.
Strict-ladder helper levelOf(role) mirrors the SQL function added in
migration 054. Path-walk authorization: ancestors with eligible roles
qualify for descendant requests (matches the visibility predicate).
Tests:
- Pure-Go: levelOf strict-ladder semantics, IsValidRequiredRole,
approvalEventType. All pass under `go test`.
- Live-DB (TEST_DATABASE_URL): no-policy noop; submit→approve cycle;
reject-create deletes; reject-update restores pre_image;
no-qualified-approver fail; revoke flow; policy CRUD roundtrip.
Skipped when TEST_DATABASE_URL is unset, mirroring the existing
audit_service_test pattern.
No call sites in DeadlineService / AppointmentService yet — that's
commit 3. Paliad continues to behave identically until that lands.
Schema-only commit (1 of 8) for the 4-Augen-Prüfung workflow per
docs/design-approvals-2026-05-06.md. No Go code reads these yet —
paliad behaves identically until commit 2 wires ApprovalService into
the mutation paths.
Migration 054 adds:
1. `senior_pa` to paliad.project_teams.role CHECK. Drops both the
English `project_teams_role_check` and the German-legacy
`projekt_teams_role_check` (live-DB constraint name carried over
from migration 018's pre-rename era).
2. `paliad.approval_role_level(text) RETURNS int IMMUTABLE` — strict
ladder helper: lead(5) > of_counsel(4) > associate(3) > senior_pa(2)
> pa(1) > [local_counsel/expert/observer = 0 = ineligible]. Mirrors
the upcoming Go `levelOf()`.
3. `paliad.approval_policies` (project_id, entity_type, lifecycle_event,
required_role) — UNIQUE composite key gives at most 8 rows per
project. RLS: SELECT via can_see_project; INSERT/UPDATE/DELETE only
for global_admin (defence-in-depth; service-role pool bypasses RLS,
so the actual gate is service-layer).
4. `paliad.approval_requests` — operational pending workflow.
pre_image jsonb captures revert state; payload echoes the diff;
required_role snapshots the policy at request time. CHECK
`decided_by != requested_by` is the second layer of self-approval
block. RLS = same can_see_project predicate as deadlines /
appointments — anyone with project visibility sees pending requests.
5. `approval_status` (default 'approved'), `pending_request_id`,
`approved_by`, `approved_at` columns on both deadlines and
appointments. `appointments.completed_at` (new) lands the
appointment:complete lifecycle event.
6. Backfill: every existing deadline + appointment row marked
approval_status='legacy'. Per Q11, no retroactive approval; the
next mutation on a legacy row that hits an active policy follows
the normal flow.
Live-DB dry-run verified end-to-end: 20 deadlines + 5 appointments
backfill, both new tables instantiate cleanly, helper function returns
correct levels, self-approval CHECK fires on invalid INSERT, valid
pending insert succeeds.
Locked design for 4-Augen-Prüfung on Fristen + Termine. m-confirmed
decisions on all 11 open questions:
- Qualification gate reuses paliad.project_teams.role per-project
(no new firm-wide axis). Adds new value `senior_pa` to the enum.
- Strict ladder: lead > of_counsel > associate > senior_pa > pa.
Default required_role = associate. Per-project override allows pa-
approves-pa or senior_pa-tier escalation.
- Per-(project, entity_type, lifecycle_event) policy grammar — up to
8 settable rows per project in paliad.approval_policies.
- Edit-trigger allowlist = date-bearing fields only (Frist due_date /
original_due_date / warning_date; Termin start_at / end_at).
- Write-then-approve: row mutates immediately, approval_status flips
between approved/pending/legacy. Delete is the one stage-then-write
exception (hard-delete on approve, restore on reject).
- Refuse + global_admin override on single-qualified-approver deadlock.
- Pending state visualised everywhere — list views, agenda, dashboard
traffic-light, project detail, CalDAV-synced calendars (`[PENDING] `
title prefix), email reminders.
- Bell + /inbox page with two tabs (zur Genehmigung / meine Anfragen).
- Operational paliad.approval_requests + audit lifecycle written to
existing paliad.project_events (4 new event_types per entity).
- RLS = same can_see_project predicate; service layer enforces the
approve/reject action gate. CHECK constraint blocks self-approval.
- Mark-legacy backfill: approval_status='legacy' on existing rows;
next mutation flows through the gate.
Implementation phasing: single migration 054 + 8-commit PR plan
covering schema, service, wiring, policy authoring page, inbox,
pending pills, CalDAV/email integration, Verlauf rendering.
Inventor parked. Awaiting m go/no-go before any coder shift.
GET /api/tools/courts[?courtType=UPC-LD] returns the deadline-
computation slice of paliad.courts (id, code, names, country, regime,
court_type) — distinct from the rich Gerichtsverzeichnis at
/api/courts. Optional courtType filter narrows to a single tier.
POST /api/tools/fristenrechner and POST /api/tools/fristenrechner/
calculate-rule both accept an optional courtId field. When set, the
calculator resolves the court's (country, regime) and uses that
calendar; when omitted, the proceeding's existing jurisdiction column
seeds a sensible default — preserves today's behaviour for callers
that don't yet send a court.
Frontend: court-picker-row added to step 2 of the Fristenrechner
wizard. Visible only for proceeding types with multiple compatible
courts (today: every UPC-flavoured proceeding — UPC LDs span 12
countries, plus UPC CD seats and the CoA). DE-only proceedings (BPatG
nullity, BGH appeals, DPMA, EPA, EP grant) keep the form unchanged.
Picker re-runs the calc on selection so the user sees the same
deadlines shift to a different calendar without a manual click. i18n
key deadlines.court.label added for both DE and EN.
Default courts wired sensibly: UPC_INF / UPC_REV / UPC_PI etc. → UPC
LD München (HLC's home venue); UPC_APP / UPC_APP_ORDERS /
UPC_COST_APPEAL → UPC CoA Luxembourg; UPC_REV → UPC CD Paris.
Holiday struct gains Country (ISO-3166) + Regime ('UPC' | 'EPO' | "")
fields. AppliesTo(country, regime) is the matching rule the new lookup
methods filter through: a row matches when its Country equals the
court's country OR its Regime equals the court's regime. UPC LD München
(DE+UPC) sees DE federal + UPC vacations; LG München (DE+"") sees only
DE federal; UPC LD Paris (FR+UPC) sees FR + UPC. germanFederalHolidays
fallback now country-tagged 'DE' so the per-country filter applies it
only to DE-jurisdictional callers.
Public service methods (IsHoliday, IsNonWorkingDay, AdjustForNonWorking
Days, AdjustForNonWorkingDaysWithReason, findVacationBlock) all take
(country, regime). Cache stays year-keyed — single DB hit per year, all
courts touching that year share it.
New CourtService loads paliad.courts once + answers Lookup(id),
CountryRegime(id, defaultCountry, defaultRegime), All(), ByCourtType(t).
FristenrechnerService.CalcOptions / CalcRuleParams gain CourtID;
EventDeadlineService.Calculate gains courtID. When courtID is empty,
DefaultsForJurisdiction maps the proceeding's existing jurisdiction
column to a sensible (country, regime) default — UPC proceedings get
(DE, UPC), everything else gets DE-only — preserving today's behaviour
for callers that don't yet send a court.
Tests: new TestAppliesTo_CountryRegimeFilter + TestAppliesTo_Rules
cover the cross-product of (DE court / UPC LD München / UPC LD Paris /
LG München) × (DE federal / UPC vacation / FR holiday). Existing tests
threaded through with ('DE', 'UPC') to preserve behaviour they were
written to lock.
Adds paliad.countries (13 ISO-3166 codes), paliad.courts (41 entries
seeded from internal/handlers/courts.go), and the country/regime split
on paliad.holidays. The 33 t-paliad-121 UPC vacation rows previously
stored as country='UPC' migrate cleanly to country=NULL + regime='UPC'
— 'UPC' is a supranational regime, not an ISO country, and the new
shape lets a UPC LD München (country='DE', regime='UPC') pull both DE
federal holidays and UPC vacation entries while a UPC LD Paris
(country='FR', regime='UPC') pulls FR + UPC. Holidays now FK-protected
against typo'd country codes.
Archives m's locked design call (2026-05-05 18:51) plus live-codebase
verification: paliad.holidays.country exists per-country; paliad.courts
does not (must create); proceeding_types.jurisdiction is regime not
country (do not remove); 41 hand-curated courts already in
internal/handlers/courts.go ready to seed; HolidayService.loadYear is
country-blind today (latent bug); germanFederalHolidays merge is
hardcoded (must become country-conditional). Task stays ON-HOLD until a
non-DE forum or EPO closure-day calendar comes into scope.
The v3 result cards were dead-ends: clicking a Klageerwiderung pill
showed no deadline; users had to switch to Pathway A's wizard, retype
the date, and read the deadline out of the timeline. v4 makes the card
the entry to a single-rule calculator + add-to-project flow per m's
2026-05-05 11:58 feedback.
Backend (single-rule calc, no parent walk):
- New POST /api/tools/fristenrechner/calculate-rule endpoint accepts
either ruleId OR (proceedingCode + ruleLocalCode), trigger date, and
optional condition flags. Returns rule metadata + computed dueDate +
originalDate + adjustment-reason chip data.
- FristenrechnerService.CalculateRule() reuses the existing addDuration
+ HolidayService.AdjustForNonWorkingDaysWithReason pipeline so
t-paliad-119's adjustment-reason explainer and t-paliad-121's UPC-
Sommerferien skip both apply automatically. Court-determined rules
(party='court' or event_type ∈ hearing/decision/order) return
IsCourtSet=true and an empty due date.
- Flag-conditional rules surface FlagsRequired even when the caller
hasn't supplied the flag — the UI uses this to render checkboxes;
toggling recomputes live. With all flags satisfied + alt_duration_*
present, the calc swaps to alt values (existing semantics).
- Live-DB integration test covers plain calc, court-set, flag handling,
and error paths (skipped without TEST_DATABASE_URL).
Frontend (inline calc panel):
- Click any card body or rule pill → expand inline panel inside the
card (only one open at a time). Pill picker (radio chips) appears
when the card has 2+ rule pills; first preselected. Trigger date
defaults to today (m's Q3). Flag checkboxes auto-render from the
rule's condition_flag.
- Result row shows due date, "(N units from triggerDate)", and a
shift chip when wasAdjusted ("⚠ Verschoben vom … wegen UPC-
Sommerferien (27.7.–28.8.)").
- "Zu Akte hinzufügen" CTA → inline project picker → POST to existing
/api/projects/{id}/deadlines/bulk with a single-element array using
source='fristenrechner' (m's Q2: existing tag, no new audit category).
- Modifier-key clicks (Cmd/Ctrl/Shift/middle) preserve the legacy
drill-to-Pathway-A semantics via <a href> anchors. Trigger pills
(Wiedereinsetzung, etc.) keep the trigger-event drill — they don't
have a single rule to compute.
- Escape collapses the open card.
CSS: lime accent border on hover/expanded; dashed top border for the
calc panel; mobile-friendly grid for the pill picker.
UPC R.221 cost-appeal sequence (m's Q5) is wired in Phase C's seed
already; Phase B's pill picker renders both pills (leave-to-appeal +
notice-of-appeal) when the user hits one of those leaves.
Migration 052 fixes six concept↔leaf mismaps in the v3 seed and adds
three proactive entry leaves under spaetere-schriftsaetze.
1. cms-eingang.gericht.hinweisbeschluss — drop the response-to-
preliminary-opinion | DE_INF row. DE_INF (LG) has no
Hinweisbeschluss; the concept lives only in DE_NULL via PatG §83.
2. cms-eingang.gegenseite.upc-inf.klageschrift — drop the notice-of-
defence-intention | UPC_INF row. UPC has no such rule in the corpus;
R.23 reaction is captured by statement-of-defence directly.
3. UPC R.221 cost-appeal sequence (m's Q5): three leaves now surface
BOTH application-for-leave-to-appeal | UPC_COST_APPEAL (sort 100,
R.221.1, 15 days) AND notice-of-appeal | UPC_APP (sort 200,
conditional on leave granted, R.220.1). Replaces the wrong notice-of-
appeal | UPC_COST_APPEAL row that was silently dropping pills.
4. ich-moechte-einreichen.berufung.upc-coa-orders — replace the buggy
application-for-leave-to-appeal | UPC_APP_ORDERS (no rule for that
combo) with request-for-discretionary-review | UPC_APP_ORDERS
(R.220.3).
5. cms-eingang.gericht.anordnung — narrow request-for-discretionary-
review NULL → UPC_APP_ORDERS. R.220.3 review applies specifically
to the Anordnungen / 15-day track.
6a. reply-to-cross-appeal coverage: add UPC_APP rows under upc-{inf,
rev}.berufungsschrift so the reply leaf is reachable when the
opponent files an Anschlussberufung.
6b. New leaves under ich-moechte-einreichen.spaetere-schriftsaetze for
proactive entry: r116-eingaben (EPA R.116 final submissions),
anschlussberufung-upc (R.237), reply-to-cross-appeal-upc (R.238).
NO `RAISE EXCEPTION` coverage gate (m's Q7) — last night's outage was
caused by exactly that pattern in migration 049. Replaced with a Go-
side test in event_category_coverage_test.go that asserts every
category='submission' concept is reachable from at least one leaf
(except the prosecution-only exempt list: filing, request-for-
examination, approval-and-translation). Skipped without
TEST_DATABASE_URL; CI gates on it.
bescheid-mit-frist mapping deferred per m's Q4. Will land separately.
Migration verified via supabase MCP dry-run + ROLLBACK on the live
youpc DB; end-state matches design §3.2-§3.4.
The v3 B1 decision tree filter collapsed each leaf's
(concept_id, proceeding_type_code) tuple list down to a flat concept_id
slice in EventCategoryService.ConceptIDsForSlug, dropping the per-leaf
proceeding constraint. The search service then loaded pills by
concept_id only, so picking a UPC-specific leaf still surfaced DE/EPA/
DPMA pills for any shared concept (Klageerwiderung, Replik, Duplik,
Berufungsschrift). m's repro: choosing CMS-Eingang → Gegenseite →
UPC Verletzung leaked national submissions.
Confirmed via DB: at least 25 leaves were over-broad pre-fix.
Fix carries the tuple set end-to-end via a new subtreeFilter type with
parallel uuid[] / text[] arrays. The matview SQL now uses
unnest($cids, $procs) AS t(cid, pcode) to match each row against the
allowed tuples — a junction row with NULL proc encodes "any proc for
this concept" (used by cross-cutting concepts like Wiedereinsetzung).
EventCategoryService gains AllOutcomes() for browse-all so the root
view also respects junction tuples. allMappedConceptIDs is gone.
Tests: added 5 v4 subtests under TestDeadlineSearch covering m's
repro slug, multi-tuple narrowing, trigger-pill cross-cutting,
forum AND-narrowing, plus an invariant regression gate that walks
every leaf with non-NULL proc and asserts no pill leaks. Skipped
when TEST_DATABASE_URL is unset; existing v3 assertions unchanged.
No schema change. No migration. Ships independently of Phases B/C.
v4 addresses three concerns from m on 2026-05-05 in priority order:
1. Card-click → compute deadline → add-to-project (v3 cards were dead-ends).
2. Filter narrowing bug — slug → concept_id allow-list dropped per-leaf
proceeding_type_code, so picking "UPC infringement opposing party"
leaked DE/EPA/DPMA pills. Confirmed via DB query: 25+ leaves overbroad.
3. RoP-rigorous tree audit: 6 confirmed seed errors (Hinweisbeschluss
DE_INF mismap, notice-of-defence-intention UPC_INF mismap, three
cost-appeal notice-of-appeal mismaps, request-for-discretionary-review
needs UPC_APP_ORDERS narrowing), plus reply-to-cross-appeal coverage
gap and bescheid-mit-frist orphan.
Plan splits into three independent phases (A: filter fix, no schema; B:
card-click flow + new calculate-rule endpoint; C: taxonomy migration 052
without RAISE EXCEPTION coverage gates per last night's outage lesson).
Inventor → coder gate held: no production code in this commit.
The B1 decision tree exposed a "Skip this step" affordance on
intermediate non-leaf nodes that broke the narrowing model — clicking
it left the tree in a half-narrowed state with no clear UX intent.
Drop the button entirely; users who don't know an answer should pick
"Anderes / Sonstiges" or switch to B2 (filter mode).
The step-back button (and its sibling .fristen-b1-loosen-link in the
empty-result state) rendered with `color: var(--color-accent)` over a
transparent background — lime green text on cream is unreadable. Move
both to a secondary-button shape: hairline border, muted text, accent
on focus-visible. Both light and dark themes verified.
Touched:
- frontend/src/client/fristenrechner.ts: drop skip TSX + handler
- frontend/src/client/i18n.ts: drop "deadlines.pathway.b.tree.skip"
- frontend/src/i18n-keys.ts: drop the codegen key
- frontend/src/styles/global.css: split off .fristen-b1-skip selector
and replace the lime-text rule with a bordered secondary style
using --color-text-muted / --color-border (themed both ways)
When the user prints (browser dialog or any Drucken button) the page now
strips everything except the actual result content. Hidden: sidebar nav,
bottom-nav, top header, footer, breadcrumbs, all forms (.tool-input,
.filter-row, .entity-controls, search bars, gebühren-lookup, etc.), the
Fristenrechner pathway-fork buttons, B1 decision-tree cascade, B1/B2 mode
toggle, view toggle, result-action buttons, every <button>. Visible:
timeline / columns view / cost breakdown / gericht cards / entity tables
/ glossar entries / checklist items, plus the page heading + subtitle so
the printed page is identifiable.
Per-page print rules above (kostenrechner / gebühren / checklisten /
gerichte) keep their existing specifics; this block is the catch-all for
chrome those rules miss.
Verified via Playwright print emulation on /dashboard, /tools/kostenrechner,
/tools/fristenrechner (Verfahrensablauf list + Spalten view), /events.
Five m's-bookmark fixes on top of the B1 surface change:
1. Sort proceeding pills inside concept cards by real-world frequency.
New paliad.proceeding_types.display_order column (m's spec values:
UPC_INF=10, DE_INF=20, UPC_REV=30, ..., UPC_PI=920, ...). Default
999 for unmapped legacy codes. Search service surfaces it through
the deadline_search matview (rebuilt to add the column) and uses
it as primary key in pillSortKey, replacing the jurisdiction-rank.
2. Name standardisation: -klage → -verfahren on the proceeding-types
that describe a multi-step process. Specifically:
UPC_REV Nichtigkeitsklage → Nichtigkeitsverfahren
UPC_APP Berufung → Berufungsverfahren
DE_INF Verletzungsklage (LG) → Verletzungsverfahren (LG)
DE_INF_OLG, DE_NULL_BGH, DPMA_OPP, DPMA_BPATG_BESCHWERDE,
UPC_COST_APPEAL, UPC_APP_ORDERS, DPMA_BGH_RB, DE_INF_BGH —
same -verfahren standardisation.
3. legal_source for rev.defence × UPC_REV: was NULL, leaking the
internal local_code 'rev.defence' to the UI. Set to UPC.RoP.49.1
(Defence to Application for Revocation, R.49.1).
4. Frontend renderPill no longer falls back to rule_local_code when
legal_source is missing — the source span just collapses, so no
internal slug ever shows up as a "citation".
5. Quick-pick chips refactored to a slug-based array (QUICK_CHIPS) in
fristenrechner.tsx, single source of truth for both fork-shortcut
and B2-search-bar rows. Each chip carries data-chip-name-de /
data-chip-name-en; relabelChips() rewrites visible text per active
language. Dropped the duplicate "Statement of Defence" chip (same
concept as "Klageerwiderung"). Each chip now maps to one concept
slug — Klageerwiderung→statement-of-defence, Berufung→notice-of-
appeal, Einspruch→opposition, Replik→reply-to-defence,
Beschwerde→nichtzulassungsbeschwerde, Schadensbemessung→
application-for-determination-of-damages, Wiedereinsetzung→
wiedereinsetzung.
Migration 051 uses RAISE WARNING (not EXCEPTION) on coverage gates
per the 049 outage lesson — partial-migration recovery beats whole-
transaction failure. Matview rebuild stays inside the transaction;
RefreshSearchView() on next boot is a cheap no-op.
Pathway B B1 mode previously rendered an empty result area on every
state — the runB1Search() output target was #fristen-search-results,
which lives inside the B2 panel. When B2 is hidden (B1 active), the
results were written into a hidden subtree and never seen.
Changes:
- TSX: add #fristen-b1-results inside #fristen-b1-panel, below the
cascade button row.
- frontend/fristenrechner.ts: extract renderSearchResultsInto() and
wirePillClicks(); runB1Search now writes to fristen-b1-results,
fetches /api/.../search?browse=all when no slug is picked yet (full
landscape on entry), and applies CSS-driven loading dim with a seq
guard against out-of-order responses. Hoisted loadAndRenderB1() so
showBMode("tree") can trigger the tree load on Pathway B entry
(radio.checked = true does not fire change events).
- backend: SearchOptions.BrowseAll, allMappedConceptIDs() returning
the union of every concept reachable from any leaf via
paliad.event_category_concepts, lifted limit ceiling for browse
modes (default 200, max 500). Handler exposes ?browse=all.
- CSS: shared loading-state styling for fristen-b1-results.
Migration 049 went dirty in prod because the coverage gate at the end
(DO $coverage$) raised on 'reply-to-cross-appeal' — it's defined as a
submission concept but no leaf in the decision-tree seed maps to it.
reply-to-cross-appeal is a downstream-of-cross-appeal concept, only
reachable after the user has already entered the cross-appeal Pathway B
branch via 'response-to-appeal'. Adding a dedicated leaf would be
useful UX (file a follow-up), but for now exempting it from the
coverage gate matches the established 'pure-administrative' exemption
pattern used for filing / request-for-examination / approval-and-translation.
Manual recovery: set tracker version=48 dirty=false on prod (schema
from 048 was already applied via supabase MCP). Dokploy redeploy will
now run 049 + 050 cleanly and reach version=50.
Refs: t-paliad-133 prod outage 11:15-11:30 Tue 05.05.2026
m's spec lock §10 Q1 (2026-05-05): "Retire legacy tabs - we are only
resorting." This commit drops the .fristen-mode-tabs nav (Verfahrensablauf
+ Was kommt nach…) and the ?legacy=1 escape hatch. Pathway A becomes
Verfahrensablauf-only; the trigger-event panel (mode-event-panel) stays
in the DOM but is hidden by default and surfaces only via concept-card
pill drill-in (drillToTrigger flips the panels directly).
Frontend deltas:
- frontend/src/fristenrechner.tsx: drop .fristen-mode-tabs section;
rename mode-event-panel role/label to standalone tabpanel.
- frontend/src/client/fristenrechner.ts:
- drop isLegacyMode() + ?legacy=1 branch in showPathway().
- drillToTrigger() now flips procedure ↔ event panels directly
(no more #mode-event-tab click → handler chain).
- initModeTabs() bails on tabs.length===0 (already does); no
further changes needed.
- frontend/src/styles/global.css: drop .fristen-pathway-shell--legacy.
Backend untouched.
Build: clean. Frontend bundle 1473 keys unchanged. go build + vet +
tests pass.
The deadlines.mode.procedure / deadlines.mode.event i18n keys remain
in i18n.ts as orphans for now; cleaning them up is purely cosmetic
and lives outside the v3 scope.
Wires the v3 Gericht/System multi-select filter on the Pathway B/B2
panel. 10 forum-bucket chips per m's spec lock §10 Q8 (UPC CFI, UPC
CoA, DE LG/OLG/BGH/BPatG, EPA Erteilung/Einspruchsabt./Beschwerdek.,
DPMA).
UX:
- Chip click toggles its membership in activeForums Set.
- Multi-select; chips AND across the result set
(UNION within forum, AND with other filters — backend handles).
- ?forum=<comma-separated> URL state round-trips on every toggle.
- popstate restores active set; lang switch re-renders chip labels.
- Shared between B1 and B2: tree-mode reissues runB1Search;
filter-mode dispatches input event on the search box.
Frontend file deltas:
- frontend/src/client/fristenrechner.ts: FORUM_BUCKETS array,
activeForums Set, renderForumChips(), reissueSearchWithCurrentFilters()
(mode-aware), getActiveForumsParam() consumed at every search call.
- B2 search fetch + B1 cascade fetch both send ?forum= when active.
Frontend i18n keys for the 10 forum labels (DE+EN) shipped with
Phase B; this commit just renders them.
Backend was wired in Phase C; this commit completes the user-facing
path. Forum filter narrowing applies AND-wise with q / event_category_slug
/ proc / party / source — empty-result UX shows the existing "no hits"
status, m can drop a chip to widen.
Build: clean. Frontend bundle unchanged size delta (≈+50 lines, 1473 keys).
Phase D-2 (party-perspective selector + is_bilateral mirroring renderer)
ships next.
Reshapes /tools/fristenrechner into the v3 landing fork. Default
view: two big pathway cards (📖 Verfahrensablauf informieren
vs 📅 Frist eintragen aufgrund Ereignis) plus a quick-pick chip
shortcut row that jumps straight into Pathway B + filter mode +
prefilled query.
URL state machine:
- ?path=a → Pathway A (existing wizard, wrapped in fristen-pathway-a)
- ?path=b → Pathway B shell with mode toggle (B1 tree / B2 filter)
- ?mode=tree → B1 panel (stub for Phase B; Phase C wires the cascade)
- ?mode=filter → B2 panel (search bar + chips + concept-card results)
- ?path absent → landing fork
- ?legacy=1 → pre-v3 layout (legacy escape hatch; dropped in Phase E)
- localStorage remembers last-used pathway
Pathway B's B2 panel hosts the existing Phase D search bar (relocated
from page-top into the pathway shell). The forum-filter row + chips
container exist in the DOM hidden — Phase D wires them.
Pathway A wraps the existing Verfahrensablauf wizard (proceeding tile
grid + date input + timeline / columns view) plus the legacy "Was
kommt nach…" tab. Both keep working unchanged in this commit; tabs
retire entirely in Phase E.
Phase B B1 panel is a stub: "Der Entscheidungsbaum ist in Vorbereitung."
Phase C replaces it with the data-driven cascade.
Files:
- frontend/src/fristenrechner.tsx: landing fork + pathway shells
- frontend/src/client/fristenrechner.ts: pathway state machine,
URL parser, popstate restore, fork-chip → ?path=b shortcut
- frontend/src/client/i18n.ts: 30+ new keys (deadlines.pathway.*,
deadlines.filter.forum.*, deadlines.perspective.*) DE+EN
- frontend/src/styles/global.css: .fristen-pathway-fork,
.fristen-pathway-card, .fristen-pathway-shell, .fristen-mode-toggle,
.fristen-forum-filter, .fristen-forum-chip rules
Frontend build: clean (1472 i18n keys). go build + vet: clean.
The legacy tabs (Verfahrensablauf-Tab + Was kommt nach…) live inside
Pathway A and continue to work — m's spec lock §10 Q1 retires them
in Phase E, not now.
Backend layer for the v3 decision tree:
- internal/services/event_category_service.go (NEW)
- Tree(): nested tree of all active event_categories for the
Pathway B / B1 cascade UI. Uses single SELECT + in-memory
parent-child stitching; corpus is small (≤100 nodes).
- ConceptsForSlug(): recursive CTE walks descendants of a slug and
joins event_category_concepts to return the candidate concept
outcomes (with optional proceeding_type_code narrowing).
- ConceptIDsForSlug(): convenience reduction for
`WHERE concept_id = ANY(...)` queries against the existing
deadline_search matview.
- ProceedingCodesForSlug(): per-leaf proceeding-code narrowing for
Phase D's forum filter intersection.
- internal/handlers/fristenrechner_event_categories.go (NEW)
- GET /api/tools/fristenrechner/event-categories returning the
nested tree as JSON. Frontend will ETag-cache via localStorage.
- Wired EventCategory into handlers.Services + dbServices + main.go.
The existing /api/tools/fristenrechner/search handler stays
unchanged in this commit; Phase D will add ?event_category_slug=
and ?forum= query params on top.
Build + vet clean.
Three migrations land the data layer for the Fristenrechner v3 decision
tree (Pathway B / B1) plus the bilateral-rule flag for the new party-
perspective selector. All purely additive — no breaking changes to the
v2 (t-paliad-131) corpus.
Migration 048 — schema:
- paliad.event_categories: recursive taxonomy tree (parent_id self-FK,
unique slug as materialised dot-path, step_question_de/en on internal
nodes, is_leaf bool, optional emoji icon).
- paliad.event_category_concepts: many-to-many junction (leaf →
deadline_concepts) with optional proceeding_type_code narrowing.
UNIQUE NULLS NOT DISTINCT prevents duplicate (leaf, concept, NULL)
rows (PG 15+).
- paliad.deadline_rules.is_bilateral bool: when true AND
primary_party='both', the rule mirrors into both party columns of
the v3 columns view; otherwise 'both' resolves single-side via the
perspective selector.
Migration 049 — seed taxonomy:
6 root buckets (cms-eingang, muendl-verhandlung, beschluss-entscheidung,
frist-verpasst, ich-moechte-einreichen, sonstiges) with 70+ leaves and
115+ junction rows. Tree depth reaches 4 today (cms-eingang › gericht
› endentscheidung › <leaf>) but the schema supports unlimited depth
per design lock §10 Q2. Coverage gate at the end raises if any
category='submission' concept is unreachable from a leaf, except the
3 pure-administrative slugs (filing, request-for-examination,
approval-and-translation) that live on Pathway A only.
Migration 050 — bilateral backfill:
Tags exactly 4 genuinely-bilateral rules:
- de_null.stellungnahme (Stellungnahme zum Hinweisbeschluss, PatG §83.2)
- epa_opp.r79_further (Stellungnahme weiterer Beteiligter)
- epa_opp.r116, epa_app.r116 (Eingaben vor mündl. Verhandlung)
All other primary_party='both' rules (Berufungsfristen, Anschlussberufung,
…) are role-swap appeals that resolve via the perspective selector at
render time.
Schema dry-run validated end-to-end against Supabase PG 15.8.
Design ref: docs/plans/unified-fristenrechner-v3.md §4.1 + §10 Q12.
m approved all 12 open questions in one batch. Locked spec:
1. Legacy tabs RETIRED in Phase E.
2. Decision-tree depth UNLIMITED (was: 4 max). Property of
event_categories data, not hard-coded.
3. Clickable breadcrumb for navigation.
4. Partial-path bookmarks (?b1=...).
5. Multi-select forum filter, default 1 selected.
6. Path-matching cards at each step. Renamed "Pfad lockern" →
"Schritt zurück".
7. Emojis only, no separate colour treatment.
8. Forum buckets simplified to 10: UPC CFI + UPC CoA + DE LG/OLG/
BGH/BPatG + EPA Erteilung/Einspruchsabt./Beschwerdek. + DPMA.
m collapsed UPC LD/CD into UPC CFI (rules identical).
9. B1↔B2 share filter state.
10. Single branch / sequential commits / one final merge.
11. Party perspective default Claimant/Proactive; localStorage
remembers last-used. URL ?my_side= + ?appeal_filed_by=.
12. Bilateral rules tagged via new is_bilateral column on
deadline_rules; mirroring only when flagged.
Maria's two scope additions folded in:
- Court-system granularity for forum filter (clarification).
- Party-perspective selector absorbing t-paliad-132.
Implementation now starting on this branch.
m's 2026-05-05 brief restructures the page surface that v2 (t-paliad-131)
shipped. The current Fristenrechner stacks three blurred entrypoints —
Phase D search bar, Verfahrensablauf tile grid, "Was kommt nach…" tab.
v3 forks the page so each mental model has its own entry:
- Pathway A — Verfahrensablauf informieren (Browse): existing wizard.
- Pathway B — Frist eintragen aufgrund Ereignis (Event → Deadline),
subdivided into:
- B1 Entscheidungsbaum: data-driven button cascade (CMS-Eingang →
Vom Gericht → Hinweisbeschluss → cards), max 4 deep, back +
breadcrumb + bookmark URLs.
- B2 Filter / Suche: Phase D concept-card search PLUS new
Gericht/System multi-select chip filter (Q8 reversal). All filters
AND-narrow.
Adds two new tables (Phase A — purely additive):
- paliad.event_categories — recursive taxonomy tree, with step
questions on non-leaf nodes.
- paliad.event_category_concepts — leaf → concept junction with
optional proceeding_type_code narrowing.
Existing data layer (deadline_concepts, deadline_rules, trigger_events,
deadline_search matview) untouched. Phase D search handler gains
?event_category_slug= and ?forum= query params; forum-bucket map lives
in Go (UPC / DE LG / DE OLG / DE BGH / DE BPatG / EPA / DPMA).
Phasing: A (data) → B (landing fork) → C (B1 tree) → D (B2 forum
filter) → E (retire legacy tabs, gate-gated). Each phase independently
shippable.
Open questions for m at §10: retire legacy tabs, decision-tree depth,
back/breadcrumb, partial-path bookmarks, multi vs single-select forum,
all-vs-path-matching cards per step, austere icons, 7 forum buckets,
B1↔B2 state-sharing, PR phasing.
Inventor parked. Next: m's go/no-go before coder shift.
Cross-references docs/plans/unified-fristenrechner.md (v2, shipped) for
concept-layer / search-backend / coverage details v3 inherits unchanged.
Closes the user-facing half of the unified Fristenrechner. The proceeding
tile grid + the two existing modes (Verfahrensablauf / Was kommt nach…)
stay in place per m's "augment, not replace" — the search bar lives
above them and drills *into* either mode pre-selected.
frontend/src/fristenrechner.tsx:
- New search section above the mode tabs:
• search input with magnifier icon and clear (✕) button
• 8 quick-pick chips per design Q8 (Klageerwiderung · Berufung ·
Einspruch · Replik · Beschwerde · Statement of Defence ·
Schadensbemessung · Wiedereinsetzung)
• #fristen-search-results container the client renders cards into
- i18n keys live in deadlines.search.* with DE primary / EN mirror.
frontend/src/client/fristenrechner.ts:
- Search subsystem with the same debounce-and-sequence-counter pattern
the existing event-mode and procedure-mode calc paths use.
- GET /api/tools/fristenrechner/search?q=…&limit=12 with same-origin
credentials. Empty q clears results; failures fall back to the
"no hits" placeholder.
- Concept card layout: name + alt-language name, optional description,
"auch bekannt als" line for matched aliases, and one pill per
(proceeding × rule). Cross-cutting trigger pills (Wiedereinsetzung,
Versäumnisurteil, Schriftsatznachreichung, Weiterbehandlung) render
in a separate pills section labelled "Verfahrensübergreifend:".
- Pills are <a href="…drill_url"> elements so middle-click / cmd-click
opens in a new tab; the JS click handler intercepts plain clicks
and drills client-side:
• rule pill → activate procedure mode tab + selectProceeding(code)
+ pendingFocus(rule_local_code) so the next
renderProcedureResults scrolls to and pulses the
focused row (.fristen-focus-highlight, 2.4 s ease).
• trigger pill → activate event mode tab + selectTriggerEvent(id).
- URL state on ?q=… via history.replaceState; popstate restores.
Initial load reads ?q= from the URL so /tools/fristenrechner?q=foo
shareable links work.
- onLangChange re-fires the search so card / pill labels follow the
active locale (matches the existing onLangChange wiring for
procedure + event results).
frontend/src/styles/global.css:
- .fristen-search input + .fristen-search-chip + .fristen-search-icon
(magnifier inset 14px from the left, search-input padded 2.6rem
on the left to clear it).
- .fristen-card / .fristen-pill grid layout with party badges in the
project's existing accent palette (claimant blue, defendant red,
both grey, court amber). Mobile @media collapses the pill grid
to a 2-column shape so legal_source + duration stack cleanly.
- .fristen-focus-highlight keyframes for the post-drill pulse.
Out of scope for this shift (deferred):
- "Vollständige Instanzenkette" toggle (design Q5). The toggle is a
multi-stage timeline render that calls Calculate independently per
stage with one date input per stage anchor — a calculator-side
feature, not the search bar. Will land as a follow-up phase.
- Columns-view sequence preservation for undated court-set events
(design §7 "Out of scope — separate task" note). Already flagged
as a separate task to file.
Validation: `bun run build` clean (1443 i18n keys, no orphans);
`go build ./... && go vet ./... && go test ./internal/...` green
across all packages. The dist bundles confirm the new symbols
landed in fristenrechner.js (search wiring), global.css (48 hits on
new selectors), and fristenrechner.html (9 unique fristen-search-*
classes). Live browser verification with auth happens after merge —
the route is auth-gated and the playwright profile is held by
another process, so a static smoke test against the dist HTML
isn't representative of the rendered authenticated page.
Closes the search half of the unified Fristenrechner. Phase D (concept-card
UI on /tools/fristenrechner) follows in a subsequent shift.
Migration 047:
- Seed the missing `wiedereinsetzung` concept and re-point the four
Wiedereinsetzung trigger_events (200..203) at it. PR-7 referenced
the slug `re-establishment-of-rights` but never seeded the concept,
so the four cross-cutting triggers were dropping out of any concept-
JOINing query. Per m's slug rule (Q1: shared cross-cutting concepts
use DE slug because German term dominates HLC vocabulary).
- Create paliad.deadline_search materialised view: UNION ALL of
(deadline_rules joined to deadline_concepts) and (trigger_events
joined to deadline_concepts via slug). Trigram GIN indexes on
legal_source / concept_name_de / concept_name_en / rule_name_de /
rule_name_en / rule_code; gin (concept_aliases) for array
containment; UNIQUE INDEX on a synthetic row_key so refresh can
run CONCURRENTLY.
Refresh strategy: data only mutates via migration files at server
startup, so no AFTER triggers and no pg_cron — main.go calls
services.RefreshSearchView right after db.ApplyMigrations. CONCURRENTLY
keeps reads online and stays well under 100 ms at < 1k rows.
Service `internal/services/deadline_search_service.go`:
- Two-query pipeline per request: (1) rank concept_ids by
GREATEST(similarity()) across name / aliases / legal_source / rule_code
plus a 0.2 alias-hit boost; (2) load all matview rows for the top-N
concepts and assemble per-pill JSON.
- normalizeQuery strips legal-prefix noise (`§`, `Art.`, `Section`,
`Rule `) so users typing `§ 82` find DE.PatG.82.1 even though the
structured legal_source column doesn't carry the prefix.
- FormatLegalSourceDisplay renders structured codes back to the
pleading form HLC users expect:
UPC.RoP.23.1 → "UPC RoP R.23(1)"
DE.PatG.82.1 → "PatG §82(1)"
EU.EPÜ.108 → "EPÜ Art.108"
EU.EPC-R.79.1 → "EPC R.79(1)"
EU.RPBA.12.1.c → "RPBA Art.12(1)(c)"
- Drill URLs route per kind: rule pills → ?proc=…&focus=…, trigger
pills → ?mode=event&triggerId=…
Handler `GET /api/tools/fristenrechner/search?q=&party=&proc=&source=&limit=`:
- Returns the JSON shape from design §6.1 (cards-with-pills).
- 503 with friendly DE message when DATABASE_URL is unset, mirroring
the other Fristenrechner endpoints.
- Empty q returns an empty cards array (browse surface is Phase D).
Tests:
- Pure-Go: TestFormatLegalSourceDisplay (12 cases across all known
prefixes) + TestNormalizeQuery (8 cases).
- Integration (skipped without TEST_DATABASE_URL): golden table
pinning the design's binding queries — Klageerwiderung returns the
statement-of-defence card with UPC.RoP.23.1, DE.ZPO.276.1,
DE.PatG.82.1, EU.EPC-R.79.1, DE.PatG.59.3 pills; "RoP 23" returns
the same card; "§ 82" → normalized "82" → BPatG hit; Wiedereinsetzung
returns one card with exactly 4 trigger pills (ids 200..203);
party / source filters narrow as expected; limit cap honoured.
- SQL semantics validated against live data via supabase MCP using a
CTE-inlined matview definition with the slug fix simulated; results
match the golden table.
Per design doc `docs/plans/unified-fristenrechner.md` §4.6 (matview
shape) + §6 (search ranking + API).
PR-7 of the Unified Fristenrechner. Final Phase B migration. Closes
all named cross-procedural deadline gaps in the design.
These concepts fire across many proceedings (any patent application,
any civil case, any opposition, any appeal) and don't naturally belong
to one proceeding-tree timeline. Modelled per design §5.2.4 + §5.3 as
event-trigger-only entries: the user picks the trigger ("the moment
the obstacle was removed", "the date the Versäumnisurteil was served")
and the calculator returns the deadline.
Migration 046 adds 7 trigger_events (ids 200–206, paliad-native space
above the youpc-imported 1–114 range so future resync stays clean) and
7 corresponding event_deadlines + 3 new concepts.
WIEDEREINSETZUNG IN 4 LEGAL CONTEXTS (one shared concept slug
re-establishment-of-rights, seeded in PR-1):
- PatG §123(2): trigger 200, 2 months / max 1 year
- ZPO §234(1): trigger 201, **2 WEEKS** / max 1 year
← critical distinction; the 2-weeks-not-months ZPO
case is the most-confused detail of DE
Wiedereinsetzung. notes_de explicitly capitalises
"WOCHEN" so the user reads it before computing.
- EPC Art.122 + R.136(1): trigger 202, 2 months / max 12 months
- DPMA via PatG §123: trigger 203, 2 months / max 1 year
OTHER CROSS-CUTTING:
- Versäumnisurteil-Einspruch (ZPO §339): trigger 204, 2 weeks
Notfrist — keine Verlängerung möglich.
- Schriftsatznachreichung (ZPO §296a): trigger 205, 3 weeks
(court-set typical; placeholder the user can adjust via
click-to-edit if the court actually set a different period)
- Weiterbehandlung (Art.121 EPÜ + R.135): trigger 206, 2 months
Distinct from Wiedereinsetzung — niedrigere Gebühr, applies
BEFORE final loss of rights.
Three new concepts (slug naming per design §4.4):
- versaeumnisurteil-einspruch (DE-only procedure → DE slug)
- schriftsatznachreichung (DE-only → DE slug)
- weiterbehandlung (EPC-native + DE term dominates HLC vocab → DE slug)
Live-verified all 7 trigger_events on paliad.de (tester@hlc.de) via
the existing /tools/fristenrechner "Was kommt nach…" mode:
trigger 200 → 2026-07-06 (2mo PatG, weekend-shift)
trigger 201 → 2026-05-18 (2 WEEKS ZPO — the critical case)
trigger 204 → 2026-05-18 (2 weeks ZPO §339)
trigger 205 → 2026-05-26 (3 weeks ZPO §296a)
trigger 206 → 2026-07-06 (2mo EPC weiterbehandlung)
Out of scope (no calculator-relevant deadlines, would just be search
clutter): Mahnverfahren-Widerspruch (ZPO §345), Validierungsfristen
national (Art. 65 EPÜ → varies per state), Teilanmeldung (R.36 EPC →
"until end of pending parent" is anchor-on-revocation-of-grant).
Phase B is now complete. Phase C (search backend) + Phase D (concept-
card UI) follow per design.