Commit Graph

466 Commits

Author SHA1 Message Date
m
bc47d78d97 feat(t-paliad-138): pending pills on /events and /agenda
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.
2026-05-06 16:05:00 +02:00
m
07a1c17861 feat(t-paliad-138): /inbox page + sidebar bell badge
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).
2026-05-06 16:00:17 +02:00
m
93c4453ce5 Merge remote-tracking branch 'origin/main' into mai/cronus/inventor-dual-control 2026-05-06 15:53:46 +02:00
m
a42322de3f Merge: t-paliad-140 — editable project on /deadlines/{id} + /appointments/{id} 2026-05-06 15:43:20 +02:00
m
457af2f6c4 fix(t-paliad-140): editable project on /deadlines/{id} + /appointments/{id}
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.
2026-05-06 15:42:22 +02:00
m
abc395fcfa Merge: i18n fix — drop 'informieren' from Verfahrensablauf pathway label 2026-05-06 15:38:45 +02:00
m
747d85fe49 fix(i18n): drop nonsensical 'informieren' from Verfahrensablauf pathway label 2026-05-06 15:38:42 +02:00
m
fb6a07f4b7 feat(t-paliad-138): approval API endpoints (policy CRUD + inbox + decisions)
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.
2026-05-06 15:30:28 +02:00
m
10b3426086 feat(t-paliad-138): wire ApprovalService into deadline + appointment paths
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.
2026-05-06 15:27:40 +02:00
m
4ebbf2c1af feat(t-paliad-138): ApprovalService core + tests
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.
2026-05-06 15:21:47 +02:00
m
b3401ec8ac feat(t-paliad-138): migration 054 — dual-control approvals schema
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.
2026-05-06 15:13:26 +02:00
m
7d1ddb9b84 docs(t-paliad-138): inventor design — dual-control approvals (4-eye)
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.
2026-05-06 14:58:01 +02:00
m
c1ceab7f4b Merge: t-paliad-122 — courts entity + per-country holidays (migration 053 + CourtService + HolidayService refactor) 2026-05-06 12:54:15 +02:00
m
733917aae2 feat(t-paliad-122): GET /api/tools/courts + Fristenrechner court picker
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.
2026-05-06 12:50:59 +02:00
m
d72990ad1b feat(t-paliad-122): country+regime aware HolidayService + CourtService
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.
2026-05-06 12:47:12 +02:00
m
a9d3695719 feat(t-paliad-122): migration 053 — courts entity + countries lookup + regime split
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.
2026-05-06 12:37:08 +02:00
m
bf06499d9c docs(t-paliad-122): inventor design — courts entity + per-country holidays
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.
2026-05-05 23:51:47 +02:00
m
98cb65f2cc Merge: t-paliad-136 Phase B — card-click → calc panel → add-to-project (v4 complete) 2026-05-05 14:09:02 +02:00
m
b54e938bdf feat(t-paliad-136): Phase B — card-click → calc panel → add to project
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.
2026-05-05 14:04:54 +02:00
m
6c3a6efc34 Merge: t-paliad-136 Phase C — RoP-rigorous tree taxonomy revision (migration 052 + Go coverage test) 2026-05-05 13:38:59 +02:00
m
d22ace1019 feat(t-paliad-136): Phase C — RoP-rigorous tree taxonomy revision
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.
2026-05-05 13:29:47 +02:00
m
7f292e5fa5 Merge: t-paliad-136 Phase A — filter narrowing carries (concept, proc) tuples end-to-end 2026-05-05 13:09:03 +02:00
m
b7470d7d77 fix(t-paliad-136): Phase A — filter narrowing carries (concept, proc) tuples
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.
2026-05-05 13:02:09 +02:00
m
30ac337a78 docs(t-paliad-136): Fristenrechner v4 inventor design
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.
2026-05-05 12:11:36 +02:00
m
25b4491681 Merge: t-paliad-135 — Print stylesheet (hide chrome/forms/buttons, show only result content) 2026-05-05 12:09:39 +02:00
m
3d905a0694 Merge: t-paliad-137 — Decision tree B1 remove Skip button + fix lime-on-light Step back contrast 2026-05-05 12:09:02 +02:00
m
19a1b8c942 fix(t-paliad-137): remove B1 "Skip step" + fix step-back contrast
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)
2026-05-05 12:07:06 +02:00
m
acaab22ad7 feat(t-paliad-135): print stylesheet — hide chrome, forms, buttons; show only result content
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.
2026-05-05 11:57:09 +02:00
m
931673337a Merge: t-paliad-134 v2 — pill ordering + name standardisation + chip dedup + legal_source fix 2026-05-05 11:54:13 +02:00
m
63eb5bde6f feat(t-paliad-134): pill ordering + name standardisation + chip dedup
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.
2026-05-05 11:53:13 +02:00
m
cc0059d050 Merge: t-paliad-134 — Fristenrechner v3 B1 result cards (browse-all + narrow-on-tree-click) 2026-05-05 11:42:54 +02:00
m
b32cfed37d feat(t-paliad-134): B1 surface — render concept cards beneath decision tree
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.
2026-05-05 11:39:30 +02:00
m
ff36528148 fix(t-paliad-133): add reply-to-cross-appeal to coverage exempt list
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
2026-05-05 11:22:14 +02:00
m
f40b652d01 Reapply "Merge: t-paliad-133 — Fristenrechner v3 (Pathway A/B fork + B1 decision tree + B2 forum filter + retire legacy tabs)"
This reverts commit 5bd17de732.
2026-05-05 11:18:38 +02:00
m
5bd17de732 Revert "Merge: t-paliad-133 — Fristenrechner v3 (Pathway A/B fork + B1 decision tree + B2 forum filter + retire legacy tabs)"
This reverts commit f7d72ff1d3, reversing
changes made to 1ea983f0c7.
2026-05-05 11:17:58 +02:00
m
f7d72ff1d3 Merge: t-paliad-133 — Fristenrechner v3 (Pathway A/B fork + B1 decision tree + B2 forum filter + retire legacy tabs) 2026-05-05 11:15:46 +02:00
m
568bc99a36 feat(t-paliad-133): Phase E — retire legacy mode tabs
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.
2026-05-05 11:07:41 +02:00
m
c399caff75 feat(t-paliad-133): Phase D-1 — B2 forum filter chip UI + URL state
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.
2026-05-05 11:05:37 +02:00
m
7141f4a954 feat(t-paliad-133): Phase C — B1 decision tree cascade + search extension
Wires the v3 Pathway B / B1 decision-tree cascade end-to-end. The
existing Phase D search backend gains two new query params, and the
frontend gets a data-driven button-cascade UI that walks
paliad.event_categories step-by-step.

Backend extension:
- internal/services/deadline_search_service.go
  - SearchOptions gains EventCategorySlug + Forums fields.
  - DeadlineSearchService gains an EventCategoryService dependency
    via SetEventCategoryService(); wired in main.go after both
    services exist (cross-link order).
  - ForumToProceedingCodes map (10 buckets per m's spec lock §10 Q8)
    translates v3 forum slugs to proceeding_type codes. Lives in Go
    so rebucketing = code change, not migration.
  - browseRanks() new query path: when q is empty AND
    EventCategorySlug is set, synthesise rank rows from the slug's
    reachable concept_ids — no trigram, just sort by
    concept_sort_order. Drives B1 narrowing.
  - rankConcepts() + loadPills() gain optional concept_id allow-list
    + forum_codes filters via UNIQUE NULLS NOT DISTINCT-shaped IS-NULL-OR
    PARAM clauses. Trigger pills (kind='trigger') always pass forum
    filter — they're cross-cutting by design.

- internal/handlers/fristenrechner_search.go
  - Reads new ?event_category_slug= and ?forum= (comma-separated)
    query params. Forwards to SearchOptions.
  - parseCSV() helper.

Frontend B1 cascade:
- frontend/src/client/fristenrechner.ts
  - loadEventCategoryTree(): one-shot fetch + in-memory cache of
    /api/tools/fristenrechner/event-categories.
  - renderB1Cascade(slug): renders breadcrumb + step question +
    button row + skip-step + step-back. Buttons walk down, breadcrumb
    walks back. Empty path = root question + 6 root buttons.
  - runB1Search(slug): hits /api/tools/fristenrechner/search?event_category_slug=
    and reuses Phase D's renderSearchResults() for the card list.
    Empty-result path shows "Schritt zurück" link (m's spec lock §10 Q6
    rephrase from "Pfad lockern").
  - URL state ?b1=<dot-path> round-trips. popstate restores cascade.
  - Pathway B default mode flips from filter → tree (B1 is now the
    discovery surface; B2 is for power users).

Frontend i18n: +1 key (deadlines.pathway.b.tree.start_question).

Frontend CSS: .fristen-b1-breadcrumb, .fristen-b1-crumb,
.fristen-b1-question, .fristen-b1-buttons, .fristen-b1-button (with
--leaf modifier border-left accent), .fristen-b1-skip,
.fristen-b1-step-back rules.

Frontend build clean (1473 keys). go build + vet + tests clean.
2026-05-05 11:03:34 +02:00
m
1182771fed feat(t-paliad-133): Phase B — landing fork UI + URL state
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.
2026-05-05 10:56:58 +02:00
m
2c770ef02f feat(t-paliad-133): Phase A — EventCategoryService + handler + route
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.
2026-05-05 10:51:58 +02:00
m
4d820892e8 feat(t-paliad-133): Phase A — event taxonomy schema + seed + bilateral flag
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.
2026-05-05 10:49:18 +02:00
m
7e363ac01d design(t-paliad-133): lock v3 design with m's answers (10:33)
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.
2026-05-05 10:37:44 +02:00
m
2ed476dc64 design(t-paliad-133): Fristenrechner v3 — Pathway A vs Pathway B fork
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.
2026-05-05 10:21:20 +02:00
m
1ea983f0c7 Merge: t-paliad-131 Phase D — concept-card UI on /tools/fristenrechner (FINAL — full unified Fristenrechner shipped) 2026-05-05 05:13:10 +02:00
m
1e5df8201b feat(t-paliad-131): Phase D — concept-card UI on /tools/fristenrechner
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.
2026-05-05 05:04:53 +02:00
m
7bd223ecd9 Merge: t-paliad-131 Phase C — search backend (matview + service + handler) 2026-05-05 04:42:40 +02:00
m
b45278b060 feat(t-paliad-131): Phase C — search backend (matview + service + handler)
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).
2026-05-05 04:32:50 +02:00
m
16c991288f Merge: t-paliad-131 Phase B6 — cross-cutting concepts (Wiedereinsetzung × 4 + Versäumnis + Schriftsatznachreichung + Weiterbehandlung) 2026-05-05 03:57:41 +02:00
m
53d5e5306c feat(t-paliad-131): Phase B6 — cross-cutting concepts (Wiedereinsetzung × 4 + Versäumnis + Schriftsatznachreichung + Weiterbehandlung)
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.
2026-05-05 03:46:45 +02:00