b516201110065b18854e6a066e559080a0d49dee
104 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b516201110 |
feat(t-paliad-144 A1): backend substrate + Custom Views API
Phase A1 of the data-display-model rethink (m/paliad#5). Backend-only; no user-visible change in A1. A2 (frontend) lands separately. What's new: - Migration 056: paliad.user_views table with RLS scoped to caller (user_views_owner_all on auth.uid()=user_id). Composite UNIQUE (user_id, slug). No is_system flag — system defaults stay code- resident per Q8 lock-in. - internal/services/filter_spec.go (+test): structured FilterSpec with Sources / Scope / Time / Predicates. Server-side validator rejects unknown sources, duplicate sources, conflicting scope modes, horizon=all without explicit projects (Q26 clamp), and every per-source enum (deadline.status, appointment_types, project_event kinds, approval_request status / viewer_role). - internal/services/render_spec.go (+test): RenderSpec with three shapes (list / cards / calendar — Q4 lock-in 2026-05-07). Per-shape config kept separately so flipping shapes preserves tweaks. Validator over column / sort / density / group_by / default_view enums. - internal/services/system_views.go (+test): code-resident SystemView definitions for dashboard / agenda / events / inbox / inbox-mine. Reserved-slug list (Q23) prevents user-views from colliding with top-level URLs. Case-folded matching. - internal/services/view_service.go: extends EventService with RunSpec — runs a FilterSpec across all four substrate sources (deadline + appointment + project_event + approval_request) and merges into []ViewRow sorted by event_date. ViewRow is a discriminated projection (kind + common header + per-source Detail json.RawMessage). Q17 fail-open attribution: returns inaccessible_project_ids for explicit-scope queries where the caller can't see some IDs. - internal/services/user_view_service.go (+test): CRUD on paliad.user_views — Create (server-assigns sort_order MAX+1 in tx), GetBySlug, GetByID, Update (partial), Delete, Touch (last_used_at), MostRecent. Reserved-slug + slug-format validators on every write. - internal/handlers/views.go: nine HTTP handlers wiring the endpoints (GET/POST/PATCH/DELETE /api/user-views/..., POST /api/user-views/{id}/touch, POST /api/views/run, POST /api/views/{slug}/run, GET /api/views/system). - main.go + handlers.go + projects.go: wire UserViewService into the bundle; conditional route registration when both UserView + Event services are present. Pure-Go tests (no DB): 32 cases pass — filter spec validators, render spec validators, system view registry, reserved slugs. Live-DB tests (skip when TEST_DATABASE_URL unset): 12 cases covering create / list / get / uniqueness / update / delete / touch / most-recent / reserved-slug / bad-slug / empty-name / invalid-spec. Coexists with t-139 (in-flight on noether's other branch) and t-138 (shipped) without coordination commits — RunSpec uses the existing visibility predicate that t-139's migration 055 will extend with derivation. Approval-request source delegates to ApprovalService.ListPendingForApprover / ListSubmittedByUser (both already extended for derived_peer authority in t-139 Phase 3). Files: 15 changed, 3134 insertions. Build clean. Tests green. |
||
|
|
a61c1490e3 |
feat(t-paliad-139): Phase 3 — derived_peer authority extension to t-138 approval gate
Wires DerivationService.EffectiveProjectRole into the t-paliad-138
approval ladder so partner-unit-derived members with derive_grants_authority=true
can act as approvers (per design §4.2). When they sign off, the audit row
records decision_kind='derived_peer' — a third value alongside the existing
'peer' and 'admin_override' — so the chronology discloses the derivation
chain.
Schema (migration 055 update)
-----------------------------
- paliad.approval_requests.decision_kind CHECK extended to accept
'derived_peer'. Down migration restores the t-138 two-value CHECK.
Live SQL dry-run confirmed the new value is accepted.
Service layer
-------------
- approval_levels.go: new constant DecisionKindDerivedPeer.
- approval_service.go (4 sites widened with the derivation EXISTS branch):
1. canApprove — third resolution step after global_admin + direct/
ancestor team membership: matches partner-unit-derived members
on path with derive_grants_authority=true and a unit_role whose
approval_role_from_unit_role mapping meets the threshold.
Returns DecisionKindDerivedPeer when this branch is the one that
passed.
2. hasQualifiedApprover (the deadlock-check at submit time) —
widened so a project with no direct approvers but an authority-
granting unit attachment is still submittable.
3. ListPendingForApprover (the /inbox query) — third UNION ALL
branch so derived authority sees their queue.
4. PendingCountForUser (the bell-badge query) — same widening so
derived authority sees the count tick.
All four queries reuse paliad.approval_role_from_unit_role(text) added
by Phase 2 of migration 055.
Frontend
--------
- 2 i18n keys (DE+EN): approvals.decision_kind.derived_peer →
"Genehmigt durch abgeleitetes Mitglied (Partner Unit)" / "Approved by
derived member (Partner Unit)". Verlauf rendering of the third
decision_kind value works through the existing translateEvent /
decision_kind switch with no other change. 1606 keys total.
Strict-default unchanged
------------------------
Derived members are visibility-only by default. Authority requires the
project lead/admin to explicitly flip derive_grants_authority=true on the
project_partner_units row (UI on /projects/{id} Team tab, Phase 2). This
preserves the m-locked Q12 stance.
Phase 3 closes the t-paliad-139 implementation. m's bug closes (Phase 1),
the derivation schema is in place (Phase 2), and approval authority
flows through the new ladder (Phase 3).
|
||
|
|
544bb63684 |
feat(t-paliad-139): Phase 2 — partner-unit derivation schema + Team-tab subsections
Migration 055 adds the structural pieces the issue's PA-derivation premise
needed (the design-§1.3 verify-before-trust check found all three were
missing today):
- paliad.partner_unit_members.unit_role text DEFAULT 'attorney'
CHECK ('lead'|'attorney'|'senior_pa'|'pa'|'paralegal') — per-unit role
distinction so derivation can target specific tiers without re-
introducing a firm-wide rank column. The same human can be 'attorney'
in one unit and 'lead' in another.
- paliad.project_partner_units junction (project_id, partner_unit_id,
derive_unit_roles[] DEFAULT {pa,senior_pa}, derive_grants_authority bool
DEFAULT false, attached_at, attached_by) with composite PK and RLS
(read = can_see_project; write = global_admin OR project lead).
- paliad.approval_role_from_unit_role(text) helper used by Phase 3 when
derived authority is consulted by the t-138 ladder.
- paliad.can_see_project extended with one EXISTS branch — derivation
walks the path: a user is visible on P if any (ancestor of P) is
attached to a unit they are a member of with a matching unit_role.
No RAISE EXCEPTION (Maria's build constraint). Day-1 deploy = zero
behaviour change because every existing unit member defaults to
unit_role='attorney' and the default derive_unit_roles is {pa,senior_pa},
so until both diverge no derivation happens.
Backend services
----------------
- DerivationService (new, internal/services/derivation_service.go):
AttachUnitToProject, DetachUnitFromProject, ListAttachedUnits,
ListDerivedMembers (path-walking dedupe by closest attachment),
ListDescendantStaffed (descendant-direct rows excluding ancestor-
already-staffed), EffectiveProjectRole (returns role + source ∈
{direct, ancestor, derived} for the t-138 approval gate in Phase 3).
- PartnerUnitService extensions:
PartnerUnitMemberDetail gains UnitRole (db:"unit_role"). Constants
UnitRoleLead/Attorney/SeniorPA/PA/Paralegal + isValidUnitRole.
SetMemberRole(callerID, unitID, userID, role) with admin gate, prior-
role read in tx, audit emit 'member_role_changed'. ListMembers and
ListWithMembers SELECT projection now includes pum.unit_role.
Handlers
--------
- GET /api/projects/{id}/partner-units → ListAttachedUnits
- POST /api/projects/{id}/partner-units → AttachUnitToProject
- DELETE /api/projects/{id}/partner-units/{unit_id} → DetachUnitFromProject
- GET /api/projects/{id}/team/derived → ListDerivedMembers
- GET /api/projects/{id}/team/from-descendants → ListDescendantStaffed
- PATCH /api/partner-units/{id}/members/{user_id}/role → SetMemberRole
- Services bundle gains Derivation; cmd/server/main.go wires it.
Frontend (Team-tab on /projects/{id})
-------------------------------------
Three new subsections rendered after the existing direct+ancestor table:
- "Aus Unterprojekten" — descendant-direct rows with attribution arrow.
- "Abgeleitet (Partner Unit)" — derived rows with [Sicht] / [Sicht & 4-
Augen] badge per the m-locked honesty rule (§3.5).
- "Partner Units" — attached-unit list with attach/detach controls
(lead/admin only) and a form picker for derive_unit_roles +
derive_grants_authority.
Each subsection is hidden when its data is empty (Partner Units block
also surfaces for managers when empty so they can attach).
Loaders + state in projects-detail.ts; renderTeam orchestrates all
four subsections; renderAttachedUnits owns the unit list + detach
handlers; initAttachUnitForm wires the picker + checkbox role-set.
canManagePartnerUnits gates the attach UI on global_admin OR direct
'lead' on the current project.
i18n keys (DE+EN, ~30 new) under projects.team.section.*,
projects.team.derived.*, projects.team.units.*, unit_role.*. Codegen now
emits 1605 keys (was 1494).
CSS additions: .entity-section-heading (subsection h3),
.derived-badge / .derived-badge--authority, .form-checkbox.
Phase 3 (approval extension to honour derived_peer decision_kind) stacks
on top — gates on EffectiveProjectRole returning ('role','derived') being
wired into the t-138 canApprove + inbox SQL.
|
||
|
|
f8d8ea591d | Merge remote-tracking branch 'origin/main' into mai/noether/inventor-project | ||
|
|
d41fc49809 |
feat(t-paliad-139): Phase 1 — /projects/{id} aggregation bug fix
m's bug: /projects/{client_id} renders "Keine Fristen" / "Keine Termine" /
"Noch keine Ereignisse" even when descendant Cases carry deadlines, appts,
and audit events. Live verification on Siemens AG client
(61e3fb9e-29fb-44aa-867e-a89469e2cacb): 9 descendant projects, 19
deadlines, 37 project_events, 4 appointments — none on the Client row,
all invisible until now.
Root cause: 3 legacy per-project read paths used WHERE project_id = $1
(exact match), bypassing the projectDescendantPredicate primitive that
internal/services/visibility.go:68 already provides and that the t-124
union endpoints (DeadlineService.ListVisibleForUser etc.) already use.
Backend
-------
- DeadlineService.ListForProject(..., directOnly bool): subtree by
default via WHERE project_id IN (SELECT pp.id FROM paliad.projects pp
WHERE $1 = ANY(string_to_array(pp.path, '.')::uuid[])); collapses to
WHERE project_id = $1 when directOnly=true.
- AppointmentService.ListForProject: same shape.
- ProjectService.ListEvents(..., directOnly bool): same shape, plus
LEFT JOIN paliad.projects to surface project_title for the Verlauf
attribution chip on /projects/{id}. Inner subquery aliased pp to
avoid shadowing the outer join's p.
- models.ProjectEvent: new optional ProjectTitle string for the Verlauf
enrichment. Other readers leave it nil and the JSON serialiser omits
it (json:"project_title,omitempty").
- handlers/{deadlines,appointments,projects}.go: handler reads
?direct_only=true|false and passes through to the service. New
handlers.parseDirectOnly helper centralises the parse.
- project_filter_descendants_test.go: extended to also pin
DeadlineService.ListForProject + AppointmentService.ListForProject
+ ProjectService.ListEvents (live-DB test, skipped without
TEST_DATABASE_URL).
Frontend
--------
- projects-detail.ts: switched the deadline + appointment fetches from
/api/projects/{id}/deadlines + /appointments (legacy narrow) to
/api/events?type=deadline|appointment&project_id={id} (the union
endpoints, already aggregating + enriching with project_title). The
Verlauf still uses /api/projects/{id}/events but with the new
direct_only flag wiring.
- New subtreeMode state machine + URL param ?subtree=false. Default =
subtree (true). persistSubtreeMode replaceState keeps back-button
friendly.
- 3 new .subtree-toggle buttons in /projects/{id} History, Deadlines,
Appointments sections. Shared state across the three; clicking any
toggle reloads all three sections at once.
- attributionChip(rowProjectID, rowProjectTitle): inline chip "auf:
Case 14-vs-Müller" rendered when row.project_id !== currentProjectID.
Suppressed for direct rows.
- Deadline / Appointment / ProjectEvent interfaces gained an optional
project_title for the chip data path.
- 3 new i18n keys: aggregation.toggle.subtree (Inkl. Unterprojekte /
Incl. sub-projects), aggregation.toggle.direct_only (Nur direkt /
Direct only), aggregation.attribution.on (auf / on). DE+EN.
- global.css: .subtree-toggle, .subtree-toggle--active,
.aggregation-chip — small additive styling.
No schema. No migration. Phases 2 + 3 stack on top per design §7.
|
||
|
|
deef5aaff5 |
feat(t-paliad-138): CalDAV [PENDING] prefix + reminder digest pending banner
Commit 7 of 8. Outbound surfaces honour the pending-approval state instead of going silent on it. CalDAV (caldav_ical.go formatAppointment): when an appointment is approval_status='pending', the iCal SUMMARY line is prefixed with "[PENDING] ". External clients (Outlook, Apple Calendar, etc.) thus display the unverified state honestly. Approved entries sync clean. Email reminder digest (reminder_service.go): - digestRow gains ApprovalStatus, sourced from f.approval_status in the SELECT. - Each pending row's Title is rewritten to "[PENDING] <title>" before it lands in the template — visible in every email-rendered list. - Template data carries PendingCount (count of pending rows in this digest) + InboxURL so future template revisions can render a banner like "Hinweis: N Frist(en) wartet auf 4-Augen-Genehmigung — /inbox" without further code changes. Existing templates unchanged for backwards compat; the prefix on row titles already conveys the signal. - IsPending flag on each item map for future per-row template conditionals. Rationale: silence on a pending change is the worst outcome for a 4-eye system. The user's external calendar and reminder mail must reflect "this exists but isn't verified" so they can act before the deadline lapses. |
||
|
|
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.
|
||
|
|
93c4453ce5 | Merge remote-tracking branch 'origin/main' into mai/cronus/inventor-dual-control | ||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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 |
Revert "Merge: t-paliad-133 — Fristenrechner v3 (Pathway A/B fork + B1 decision tree + B2 forum filter + retire legacy tabs)"
This reverts commit |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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).
|
||
|
|
cc68ab2873 |
feat(t-paliad-131): Phase B1 — UPC counterclaim cross-flows
Closes m's primary complaint: today's `with_ccr` flag on UPC_INF only
swaps the Replik / Duplik durations. Per UPC RoP R.29 the with-CCR flow
ALSO adds 5–7 new submissions across the claimant / defendant exchange.
Same gap on UPC_REV: Application to amend (R.49.2.a → R.55 = R.32 m.m.)
and Counterclaim for infringement (R.49.2.b → R.50, R.56 cycle) were
entirely missing.
UPC_INF gets a nested `with_amend` flag under `with_ccr` (R.30 amend
is only available with a CCR). UPC_REV gets two parallel independent
flags `with_amend` + `with_cci`; both can be on. Citations verified
against data.laws_contents (youpcdb, UPCRoP).
Migration 041 (waved INSERTs because each subsequent rule references
the prior wave's parent_id):
- Wave 0: 11 new concept rows (counterclaim-for-revocation,
defence-to-counterclaim-for-revocation, defence-to-application-to-amend,
reply-to-defence-to-counterclaim-for-revocation,
reply-to-defence-to-application-to-amend,
rejoinder-on-reply-to-defence-to-ccr, rejoinder-on-reply-to-amend,
counterclaim-for-infringement, defence-to-counterclaim-for-infringement,
reply-to-defence-to-counterclaim-for-infringement,
rejoinder-on-counterclaim-for-infringement). counterclaim-for-revocation
also seeded for the search bar even though its rule lives implicitly
in inf.sod (the with_ccr flag captures it).
- UPC_INF + UPC_REV sequence_orders renumbered to leave gaps (10/20/30…)
so new cross-flow rows interleave chronologically with the backbone.
- 7 new UPC_INF rules: inf.def_to_ccr (R.29.a), inf.app_to_amend (R.30.1),
inf.def_to_amend (R.32.1), inf.reply_def_ccr (R.29.d),
inf.reply_def_amd (R.32.3), inf.rejoin_reply_ccr (R.29.e),
inf.rejoin_amd (R.32.3).
- 8 new UPC_REV rules: rev.app_to_amend (R.49.2.a), rev.def_to_amend
(R.43.3), rev.reply_def_amd (R.32.3 m.m.), rev.rejoin_amd (R.32.3 m.m.),
rev.cc_inf (R.49.2.b), rev.def_cci (R.56.1), rev.reply_def_cci (R.56.3),
rev.rejoin_cci (R.56.4).
Calculator (services/fristenrechner.go):
- Zero-duration rules now split into 4 buckets, not 2:
1. parent=nil + non-court → IsRootEvent (existing)
2. parent=nil + court → IsCourtSet (existing, e.g. inf.oral when stand-alone)
3. parent set + court → IsCourtSet (existing, waypoints)
4. parent set + non-court → "filed-with-parent" — inherit parent's
date. NEW. Used by rev.app_to_amend / rev.cc_inf which per
R.49(2) are filed AS PART OF the Defence to revocation.
- AnchorOverrides on a zero-duration rule short-circuits to the user's
date, propagating downstream as before.
Frontend:
- New checkboxes inf-amend-flag (UPC_INF, nested under ccr-flag),
rev-amend-flag, rev-cci-flag (UPC_REV). Visibility per proceeding
type; inf-amend disabled until ccr is on (R.30 dependency).
- Three new i18n keys (DE+EN). Small CSS for nested-checkbox indent
and disabled-state colour.
Live-verified via curl on paliad.de against tester@hlc.de:
UPC_INF + with_ccr+with_amend, trigger 2026-05-04 → all 7 new rules
render at correct dates (R.29.a 2mo, R.30.1 2mo, R.32.1 2mo from
app_to_amend, R.29.d 2mo from def_to_ccr, R.32.3 1mo, R.29.e 1mo,
R.32.3 1mo).
UPC_REV + with_amend+with_cci → rev.app_to_amend / rev.cc_inf show
rev.defence's date (filed-with-parent), R.43.3 2mo / R.56.1 2mo /
R.32.3 + R.56.3 1mo / R.32.3 + R.56.4 1mo all line up.
|
||
|
|
78966ec098 |
feat(t-paliad-131): Phase A — concept layer + AnchorOverrides + click-to-edit dates
PR-1 of the Unified Fristenrechner. Purely additive: new search-grouping layer + per-rule date override capability. No coverage changes yet (those land in PR-2 = Phase B1 UPC counterclaim cross-flows). Migrations: - 037: paliad.deadline_concepts (id, slug, name_de/en, aliases text[], party, category, sort_order). Trigram + GIN indexes for the search bar. - 038: deadline_rules.concept_id (uuid FK), legal_source (text); event_deadlines.legal_source; trigger_events.concept_id (text slug, soft-link — youpc imports keep their bigint PK). - 039: deadline_rules.condition_flag text → text[] (USING ARRAY[old]). Semantic: rule renders iff every element is in CalcOptions.Flags. Single-element arrays preserve the legacy with_ccr swap exactly. - 040: seed 30 concept rows + backfill all 74 fristenrechner deadline_rules with concept_id; backfill legal_source from existing rule_code (e.g. 'RoP.023' → 'UPC.RoP.23.1', '§ 276 ZPO' → 'DE.ZPO.276.1', 'Art. 108 EPÜ' → 'EU.EPÜ.108', 'R. 79(1) EPÜ' → 'EU.EPC-R.79.1'). Calculator (services/fristenrechner.go): - ConditionFlag is now pq.StringArray (matches text[] schema). New allFlagsSet() helper gates rule rendering; rules with multi-element flags require ALL of them set (prep for Phase B1 with_amend ∧ with_cci). - CalcOptions.AnchorOverrides map[string]string (rule_code → YYYY-MM-DD). The tree-walk consults overrideDates[parent.code] before reading the computed-date map; lets a downstream rule re-anchor on a user-set date. - IsCourtSet rows that get an override stop being placeholder and emit the user's date as a real anchor (so downstream cost_app etc. compute off it). New IsOverridden flag in UIDeadline so the UI can highlight user-edited rows. - LegalSource surfaced on UIDeadline for future search-card display. UI (frontend/src/client/fristenrechner.ts + global.css + i18n): - Each timeline / column rule date is click-to-edit. Click → inline date input → blur or Enter → POST with anchorOverrides → re-render. Empty value clears the override. Escape cancels. Root-event rows (the trigger anchor) stay non-editable — that's the trigger-date input. - Override map cleared on proceeding switch / reset; persists across trigger-date / flag toggle changes within the same proceeding. - New CSS: subtle hover underline on .frist-date-edit; lime border on .timeline-date--overridden + .frist-date-edit-input. - New i18n key deadlines.date.edit.hint (DE + EN). Handler (handlers/fristenrechner.go): - POST body gains optional anchorOverrides map<string,string>; passed through to CalcOptions. Tests: - TestAllFlagsSet covers single-flag legacy semantic, two-flag AND semantic, empty-required unconditional, extra-flags-no-effect. - Existing TestIsCourtDeterminedRule unchanged. Phase A ships standalone — Phase B1 (UPC counterclaim cross-flows) and Phase C/D (search backend + concept-card UI) follow. |
||
|
|
9919e04657 |
feat(t-paliad-128): /events 'Nur persönliche' = items I created
Redefines the "Nur persönliche" filter on /events from "appointment with NULL project_id" to "items where created_by = me", applied uniformly to deadlines and appointments. Before: client-side filter dropped every deadline row because the type guard was `x.type === "appointment"`. m saw zero deadlines under "Nur persönliche" even though he created plenty. After: - /api/events?personal_only=true (and /api/events/summary?personal_only=true) narrow BOTH rails to f.created_by / t.created_by = current user. ProjectID is ignored when personal_only is set (the two are contradictory). - DeadlineService.ListFilter and AppointmentService.AppointmentListFilter gain CreatedBy *uuid.UUID — composes with existing visibility (AND), so a row created on a team the user has since left still won't leak. - Frontend drops the client-side filter; sends personal_only=true when projectFilter === PERSONAL. URL ?personal_only=true also accepted on initial load (bookmark-friendly alias for ?project_id=__personal__). Personal option now shows for type=Fristen too — applies uniformly. - 3 new live subtests covering personal_only across type=deadline / appointment / all, with mixed-creator + multi-project + null-project fixtures. |
||
|
|
4d7c74994a |
feat(t-paliad-125): sort project pickers by tree path with depth indent
The /events Project filter dropdown was sorted by `updated_at DESC`, so a recently-touched Case appeared above its parent Client and cousins interleaved unrelated branches — m's report (2026-05-04): "Siemens cases come directly after 'mandant vs Gegner' and are not under 'Siemens-AG'". Backend: switch ProjectService.List to ORDER BY p.path so every descendant immediately follows its ancestor — the same ordering BuildTree produces. Both callers (handleListProjects, searchProjects) gain a stable, hierarchical default that matches user expectation. Frontend: add project-indent.ts shared helper and apply NBSP indent prefix in every <select> picker fed by /api/projects: events filter, /deadlines/new, /appointments/new, checklist new-instance modal, Fristenrechner save modal. NBSP avoids browser whitespace collapse inside <option> labels. Multi-parent repetition is out of scope (data model has singular parent_id today). Tests: project_list_order_test pins the path-order contract against a seeded mixed-recency tree. |
||
|
|
062630ca38 | Merge: t-paliad-123 — /events Status filter for appointments (date buckets) | ||
|
|
8123d71d08 | Merge: t-paliad-124 — project filter walks descendants (Client filter → all child rows) | ||
|
|
a69fff73e9 |
feat(t-paliad-124): project filter includes descendant projects
Selecting a Client in the project filter now returns rows attached to that Client AND every Litigation / Patent / Case below it (and so on down the tree). Previously the filter was exact-match: picking a Client hid every item in the subtree, which was the opposite of what users expect when they pick a parent in a hierarchical picker. The descendant set comes from paliad.projects.path - every project's path always contains its own id and every ancestor's id, so any project whose path includes the filter UUID is either that project or a descendant. Pattern matches the existing visibility predicate (which walks the path UPWARD for inheritance); the new helper just inverts the direction. Filter sites updated: - DeadlineService.ListVisibleForUser (/deadlines, /events) - DeadlineService.SummaryCounts (deadline summary cards) - AppointmentService.ListVisibleForUser (/appointments, /events) - EventService.deadlineBuckets (/events deadline rail) - EventService.appointmentBuckets (/events appointment rail) ListForProject (deadline/appointment/checklist/note) is unchanged - it fetches items for ONE specific project on the project detail page, not a filter. Visibility predicate (paliad.can_see_project) untouched - that walks upward and is a different concern. |
||
|
|
1bba9cb3ce |
feat(t-paliad-123): apply date-bucket Status filter to appointments
Until now, /events hid the Status dropdown when Type=Termine. The
date-bucket filters (Heute, Diese Woche, Nächste Woche, Später) only
worked on the deadline rail — m wanted them on appointments too, even
without a "completed" dimension.
Frontend (events.ts):
- New populateStatusFilter() rebuilds the Status <select> options based
on currentType: deadlines get the full 8-option set, appointments
narrow to 5 (Alle + 4 buckets). The "completed/pending/overdue"
options drop because they have no appointment analogue.
- applyTypeVisibility() no longer hides the Status filter for
appointments; it calls the populator instead. The populator runs on
type-chip click and on language change so labels translate live.
- When switching type while a now-invalid status is selected (e.g.
Termine + status=completed via URL), the populator falls back to the
per-type default (deadline → pending, appointment → all) and updates
URL params.
- syncURLParams + isFilterPristine + initFilters use a per-type default
so the appointment view treats `all` as pristine and stays out of the
URL until the user picks a bucket.
- loadList always sends `status` to /api/events; backend already
applies bucket-aware appointment filtering via
bucketAppointmentWindow().
events.tsx:
- The static <option> list collapses to a single placeholder; the
populator owns the option set at hydration.
i18n:
- New `events.filter.status.all` ("Alle"/"All") for the appointment-only
case — `deadlines.filter.all` says "Alle (offen & erledigt)" which is
wrong for appointments (they don't have a completed/pending state).
Backend (event_service_test.go):
- Three new live subtests confirming type=appointment + status=today
narrows to today's appointments, status=later narrows to far-future,
and status=completed collapses the appointment rail (defensive vs.
URL-hacking — the dropdown excludes that value for appointments).
|
||
|
|
7461c4af49 |
fix(t-paliad-121): stop shifting deadlines for UPC court vacations
Per UPC AC decision 2023-05-26, the UPC has summer + winter judicial vacations but the Court continues to operate during them — they do NOT extend procedural deadlines. paliad's HolidayService was treating every paliad.holidays row as a non-working day, including vacation entries, so a deadline landing on Tue 2026-08-04 (a regular working Tuesday) was incorrectly shifted to Mon 2026-08-31 by walking the entire summer- vacation run. Fix: gate IsNonWorkingDay on Holiday.IsClosure (true for public_holiday and closure types, false for vacation). IsHoliday still returns the row regardless of type — UI surfaces that want to flag "this date is inside UPC vacation" can still ask. paliad.holidays data is unchanged: the UPC vacation rows stay as informational metadata. The Kind="vacation" branch of AdjustForNonWorkingDaysWithReason is now unreachable in practice (every vacation entry is IsClosure=false, so the walk loop never enters with a vacation as the cause). Left in place as defensive code for any future vacation type that should shift. Tests: replaced TestAdjustForNonWorkingDaysWithReason_Vacation (asserted the old wrong behaviour) with TestVacationDoesNotShiftDeadlines covering m's reproduction (Tue 2026-08-04 → no shift), winter-vacation no-shift (Mon 2026-12-28), Christmas/Neujahr regression (still shift correctly, and walk through informational vacation entries to land on the next real working day), and a Karfreitag regression to lock public-holiday shifts. |
||
|
|
d688ebde90 |
feat(t-paliad-119): explain WHY a Fristenrechner deadline was shifted
The current "Wochenende/Feiertag" / "weekend/holiday" label hides the cause
of long shifts — m's reproduction had a deadline jump from 4.8.2026 to
31.8.2026 (+27 calendar days) across UPC Summer Vacation, and the UI made
it look like a bug. The math was correct; the explanation was lying.
Backend:
- AdjustForNonWorkingDaysWithReason returns an AdjustmentReason alongside
the adjusted date. Walks the same 60-iter loop, classifies the dominant
cause (vacation > public_holiday > weekend), collects every named
holiday hit, and for vacations scans outward to report the contiguous
block boundary (27.7.–28.8., not the 25 individual rows).
- AdjustForNonWorkingDays now wraps the new method, preserving its
3-tuple signature for existing callers (deadline_calculator,
event_deadline_service).
- UIDeadline gains an AdjustmentReason field; FristenrechnerService
populates it on every shifted deadline.
- Date fields serialise as YYYY-MM-DD strings (HolidayDTO + string
vacation span) — the Fristenrechner client already speaks that format.
Frontend:
- AdjustmentReason → human-readable phrase via renderAdjustmentReason:
vacation → "{vacation_name} ({span})"
public_holiday → "Feiertag ({first_holiday_name})" / "{name} holiday"
weekend → "Wochenende" / localised weekday
- Surrounding format becomes "Verschoben wegen X: A → B" (DE) or
"Shifted (X): A → B" (EN). Falls back to the legacy reason string
when the backend hasn't sent a structured reason.
- Vacation names render verbatim from paliad.holidays — no hardcoded
i18n mapping for individual closures (those rotate via the seed, not
via i18n.ts).
Tests cover the three Kind paths plus the no-shift case; UPC vacation
test injects the migration-010 seed into the cache so the assertion
runs without a live DB.
Out of scope (raised in conversation, deferred):
- Whether "UPC Summer Vacation" / "UPC Winter Vacation" are the right
names for the seeded rows, and whether the winter block belongs in
paliad.holidays at all (m flagged this as BS while reviewing the
task — needs a data-side decision before renaming/removing).
- holidays.country isn't filtered by proceeding-type jurisdiction, so
UPC vacation currently shifts EP_GRANT / EPA_APP / German national
deadlines too. Bigger fix; flagged for a follow-up issue.
|
||
|
|
4e1213fbd1 |
fix(t-paliad-116): event_deadlines i18n follow-up — title_de backfill + notes_en
Two adjacent i18n leaks in /tools/fristenrechner "Was kommt nach…" mode,
same pattern as t-paliad-112's deadline_rules fix but on event_deadlines:
A) title_de empty for all 70 rows. The DTO already falls back to title
silently, so DE locale rendered English titles ("Statement of Defence",
"Decision of the EPO", …). Backfilled via mig 035 with UPC RoP DE
terminology that matches the trigger_events.name_de translations from
mig 033, so the picker label and the deadline row read the same.
B) notes column carries English text on rows 50, 52, 70 (DE-named column
was DE-only in spec, but seeds slipped EN strings through). Mig 036
adds a parallel notes_en column following the t-112 mig 032 pattern,
copies the existing English into notes_en, and replaces notes with
proper DE for those three rows.
Render path:
- service: select notes_en, plumb through EventDeadlineResult.NotesEN
- frontend: getLang() === "en" ? (notesEN || notes) : notes (mirrors the
proceeding-tree timeline branch already in fristenrechner.ts)
|
||
|
|
c554e865eb | Merge: t-paliad-111 — bug bundle correctness (UPC GESAMTKOSTEN, court-set dates, REGEL save) | ||
|
|
0be2dfb5a0 |
fix(t-paliad-111): bug bundle (correctness) — UPC GESAMTKOSTEN, court-set dates, REGEL save flow
Three correctness bugs from the t-paliad-101 QA sweep, fixed together since
they all change displayed/saved numbers users rely on.
B1 — Kostenrechner UPC GESAMTKOSTEN double-count
ComputeUPCInstance was setting InstanceTotal = effectiveCourtFee +
recoverableCeiling. The R.152 recoverable-cost cap is the OPPOSING
side's worst-case loss-of-suit liability, not the user's own cost —
folding it into GESAMTKOSTEN inflated the UPC total under a label
that means "your outlay," and the DE LG/OLG/BGH branches don't add
any opponent estimate. Drop it from InstanceTotal; the ceiling
still surfaces as its own RecoverableCeiling line item.
Live pre-fix on paliad.de (Streitwert 100k, UPC 1. Instanz only):
instanceTotal = 52600 = 14600 court fee + 38000 R.152 ceiling
Post-fix:
instanceTotal = 14600 (court fee only); RecoverableCeiling stays 38000
B3 — Court-determined Termine emit trigger date as a real-looking date
Zwischenverfahren / Mündliche Verhandlung / Entscheidung all live in
paliad.deadline_rules with duration_value=0 and parent_id=NULL, so
Calculate() classified them as IsRootEvent and emitted the trigger
date as their own DueDate. Worse, RoP.151 "Antrag auf Kostenentscheidung"
parents off inf.decision and chained 1 month off the placeholder ->
bogus deadline that the UI rendered as real.
Fix: classify a zero-duration rule as IsCourtSet (not IsRootEvent)
when primary_party = 'court' or event_type ∈ {hearing, decision,
order}. Track court-set rule IDs and propagate IsCourtSet downstream
to any rule whose parent is court-set, so RoP.151 also surfaces as
court-set rather than a fabricated date. Save-modal already greys
out IsCourtSet rows so the "Gerichtsbestimmte Termine ohne Datum
werden übersprungen" footnote becomes truthful again.
Live pre-fix on paliad.de (UPC_INF, trigger 2026-04-29):
Zwischenverfahren / Oral / Entscheidung -> dueDate 2026-04-29
Antrag auf Kostenentscheidung -> 2026-05-29 (bogus, +1mo from trigger)
B6 — Fristenrechner save flow stored rule code in TITLE
Frontend was concatenating "RoP.023 — Klageerwiderung" into the
title because deadlines had nowhere else to put the citation, and
the /deadlines REGEL column ended up showing "—". Add migration 032
with a paliad.deadlines.rule_code text column, plumb it through
CreateDeadlineInput / insertTx, drop the now-redundant r.code AS
rule_code JOIN alias on the list query (the deadline owns its
citation), and render f.rule_code on the project-detail deadlines
table + /deadlines events list + deadline-detail page.
Build, vet, and tests all clean. New unit test
TestIsCourtDeterminedRule pins the B3 discriminator across the
event_type / primary_party combinations seen in migrations 012 + 031.
Repro creds: tester@hlc.de
|
||
|
|
341fa6c26f |
fix(t-paliad-112): i18n leaks — deadline_notes_en, trigger-event DE, Checkliste header
Three i18n bugs from the t-paliad-101 QA sweep, fixed together: B2 — Fristenrechner deadline notes leaked German into the EN locale. Migration 032 adds paliad.deadline_rules.deadline_notes_en (TEXT NULL) and backfills English translations for all 30 rules that carry a deadline_notes value (UPC RoP / EPC / ZPO terminology). The frontend prefers _en when locale=EN and falls back to deadline_notes (DE) when the column is NULL, so future seeds without an EN translation render in DE rather than empty. UIDeadline DTO gains notesEN. The bulk "Als Frist(en) speichern" CTA now stores the locale-matched note text so EN users get an EN note alongside the EN title. B8 — trigger-event picker labels were English-only when DE locale was active (102 rows, name_de defaulted to '' in 028, frontend already had the locale switch but no data). Migration 033 backfills name_de for all 102 trigger events using standard German UPC RoP terminology (Klageschrift, Klageerwiderung, Replik, Duplik, Nichtigkeitswiderklage, Verletzungswiderklage, Berufungsschrift/-begründung, Anschlussberufung, Schutzschrift, Beweissicherung, etc.). S3 — frontend/src/client/checklists-instance.ts:154 had a hardcoded "Project" label in both branches of the locale ternary; the DE branch now reads "Projekt", matching the surrounding meta-item labels' pattern (Court / Authority → Gericht / Behörde, Reference → Rechtsgrundlage). |
||
|
|
57237a55a3 |
feat(t-paliad-110): refactor Dashboard rails — drop Erledigt card, add Später + Termine rail
PR-4 of the Fristen+Termine unification, closing out t-paliad-110. Fristen rail (was 5 cards): - Erledigt card removed (status=completed stays reachable via the EventsPage filter dropdown — no card on the rail per the new model) - Später card added (pending deadlines past Mon-week-after, click filters to /deadlines?status=later) - 4+1 final shape: Überfällig (conditional alarm) · Heute · Diese Woche · Nächste Woche · Später Termine rail (new): 3 cards — Heute · Diese Woche · Später. No Überfällig (past appointments aren't urgent), no Nächste Woche (low-value distinction for appointments per the design rationale). Cards click through to /appointments?status=… so users land in the matching EventsPage view. Backend (DashboardService.loadSummary): - DeadlineSummary.CompletedThisWeek dropped, .Later added - AppointmentSummary added (Today / ThisWeek / Later) - One CTE-based query computes both rails alongside MatterSummary; bucket cutoffs share computeDeadlineBucketBounds with /api/events/summary + /api/deadlines/summary so all three surfaces stay in lockstep Frontend: - dashboard.tsx: Erledigt card removed, Später card + Termine section added - client/dashboard.ts: types updated, renderAppointmentSummary added - 4 new i18n keys (DE+EN): dashboard.summary.later + dashboard.appointment_summary.heading - CSS: .dashboard-card-later (muted blue) + 3 .dashboard-card-appt-* rules reusing the existing --bucket-* tokens go build/vet/test ./... clean. bun run build clean (1396 keys). |
||
|
|
fe9c1b7de2 |
feat(t-paliad-110): add shared EventsPage component + bucket-aware backend tweaks
PR-2 of the Fristen+Termine unification. Pure additive change — the existing
deadlines.tsx + appointments.tsx pages stay live; this PR introduces the new
events.tsx shell + client/events.ts runtime that PR-3 will mount onto the
two routes.
Frontend (new):
- frontend/src/events.tsx — shared shell with the 3-chip type toggle
(Fristen / Termine / Beides), the 5-card summary row (Überfällig
conditional + 4 universal cards), the union filter row, and the unified
table that renders a discriminated row per type. Two header CTAs ("Neue
Frist" + "Neuer Termin") collapse to the relevant one in single-type mode.
- frontend/src/client/events.ts — runtime. Reads window.__PALIAD_EVENTS__
(PR-3 will inject defaultType from the Go handler), derives the rest from
?type query param. Card click sets status filter; the events endpoint
takes care of bucket-aware appointment-side date windowing so both rails
stay in sync in Beides mode. Hide-on-uniform pattern applied per column
(rule, event_type, location, appointment_type, status, row-type chip).
- frontend/build.ts — emits events-deadlines.html + events-appointments.html
from one renderEvents(currentPath) so each output gets the right Sidebar
highlight; client/events.ts bundle added.
- 16 i18n keys (DE+EN): events.toggle.*, events.summary.later,
events.col.*, events.row.type.*, events.empty.*, events.unavailable plus
the new deadlines.summary.later / deadlines.filter.later pair for the
Später bucket.
- CSS: --bucket-later (#1d4ed8 light / #60a5fa dark) for the Später card,
matching events-table--hide-* column hiders, .events-row-type-chip
styling, .event-type-chip-row spacing.
Backend tweaks (small):
- DeadlineFilterLater (`later`): pending deadlines past Mon-week-after.
Click-target for the Später card.
- EventService.ListVisibleForUser now derives an appointment-side date
window from a bucket-style status (today/this_week/next_week/later) so
card clicks filter both rails consistently. Overdue/Completed exclude
appointments entirely (no appointment analogue).
- pickLater / pickEarlier helpers intersect the bucket-derived window with
any caller-supplied from/to.
go build/vet/test ./... clean. bun run build clean (1394 keys, IIFE prologue
guard passes).
|
||
|
|
2102dfd07d |
feat(t-paliad-110): add EventService + /api/events + /api/events/summary
PR-1 of the Fristen+Termine unification (t-paliad-110). Backend layer
only — no frontend changes; the existing /deadlines and /appointments
pages still render the type-specific UIs.
EventService delegates to DeadlineService + AppointmentService for the
actual reads (no duplicate visibility logic, no duplicate event_type
hydration), then projects both into the discriminated EventListItem
union and merges/sorts by event_date asc. The handler exposes:
GET /api/events?type=deadline|appointment|all&status=…&project_id=…
&event_type=…&type_filter=…&from=…&to=…
GET /api/events/summary?type=…&project_id=…
Bucket model (per t-paliad-110 spec, supersedes t-106):
- four universal cards: Heute · Diese Woche · Nächste Woche · Später
- Überfällig is deadline-only, conditional, alarm-styled when > 0
- Erledigt drops from the card row; stays available as a filter option
- appointments have no completed_at — past appointments aren't bucketed
The deadline-side cutoffs reuse computeDeadlineBucketBounds so
/api/events/summary and /api/deadlines/summary can never disagree.
Existing /api/deadlines and /api/appointments stay untouched —
calendars, project-detail panes, and CalDAV consumers still call them
directly.
|
||
|
|
37a925d3b2 |
feat(t-paliad-106): harmonize deadline summary — 5 disjoint buckets across Dashboard + Fristen
Both surfaces now show the same buckets with the same labels and the same cutoffs: Überfällig (conditional, alarming) · Heute · Diese Woche · Nächste Woche · Erledigt. Single-source bucket math via computeDeadlineBucketBounds — Heute = today, Diese Woche = tomorrow through the upcoming Sunday inclusive, Nächste Woche = next Monday through next Sunday inclusive, all disjoint. Items past next Sunday are visible only via "All open"/"Upcoming" filters; the Überfällig card stays hidden when count == 0 and switches to a saturated red pulse + bold white text when count > 0. Filter dropdown on /deadlines gains today / next_week entries; old "upcoming" filter still works as a back-compat alias for everything pending past this Sunday so legacy bookmarks don't 4xx. Tests: 8 deterministic table cases for the bucket pivots (every weekday + a 21-day disjointness walk). |
||
|
|
95a6df5b49 |
feat(t-paliad-102): link Verlauf entries to deadlines/appointments/notes
Extends the t-paliad-097 metadata pattern from checklist_* events to the
remaining audit families. Project Verlauf and Dashboard activity feed now
deep-link each event to its originating entity:
- deadline_{created,updated,completed,reopened} → /deadlines/{id}
- appointment_{created,updated} → /appointments/{id}
- note_created → /appointments/{id} | /deadlines/{id} | /projects/{id}
(most-specific parent — notes have no standalone page)
Backend (Go):
- deadline_service.go / appointment_service.go: switch single-entity
mutation events from insertProjectEvent to insertProjectEventWithMeta
carrying {"deadline_id"|"appointment_id": uuid}.
- note_service.go:insertWithAudit: derive metadata from noteParent so
the audit row records {note_id, deadline_id|appointment_id|project_id}.
Frontend (TS):
- projects-detail.ts: extract eventDetailHref(); wrapEventTitleLink
delegates to it. Comment block lists every wired event family.
- dashboard.ts:activityHref: same routing rules as the project Verlauf.
- global.css: .entity-event becomes position:relative; the
.entity-event-link::before pseudo expands the link's hit area to the
full card so a click anywhere on the row navigates (matches what m
expected from "die Karte ist verlinkt"). Hover lifts border + shadow.
Excluded by design (mirrors checklist_deleted exclusion):
- *_deleted events — entity is gone.
- deadlines_imported — bulk event with no single deadline_id; would
need an aggregate target the product doesn't have today.
Pre-metadata rows stay non-clickable (no backfill — same precedent as
t-paliad-097).
|
||
|
|
df321acb63 |
feat(t-paliad-097): clickable checklist references + Vorhandene Instanzen tab
Two related checklist UX gaps:
1. Checklist events in a project's Verlauf tab were unclickable — and
nothing in the project_events row carried the originating instance ID.
Add an `insertProjectEventWithMeta` helper, write
{"checklist_instance_id": <uuid>} as project_events.metadata for
checklist_created / _renamed / _linked / _unlinked / _reset (skipped
for _deleted — instance is gone). Surface metadata on
/api/projects/{id}/events and on dashboard recent_activity. The
Verlauf renderer wraps the title in <a href="/checklists/instances/{id}">
when metadata.checklist_instance_id is present, and the dashboard's
activity feed deep-links the project ref to the instance directly for
checklist_* events. Existing rows (metadata `{}`) stay non-clickable —
no migration backfill needed.
2. /checklists previously demanded a template pick before any existing
instance was reachable. Add a tab nav (Vorlagen / Vorhandene Instanzen)
using the existing entity-tab pattern. New endpoint
GET /api/checklist-instances and ChecklistInstanceService.ListAllVisible
return every visible instance across templates + projects, joined with
project ref/title and sorted by created_at DESC. Rows show template,
instance name (linked), project link (or "Persönlich"), progress bar,
and created date. URL state (?tab=instances) keeps the active tab
shareable. EN + DE i18n covered for tab labels and column headers.
Also adds event.title.checklist_* localizations for the Verlauf header
that translateEvent looks up.
|
||
|
|
460736ad1e |
refactor(t-paliad-092): rename Go module path patholo → paliad
F-6 from t-paliad-074 architecture audit. The Gitea repo was renamed m/patholo → mAi/paliad → m/paliad, but go.mod still declared `mgit.msbls.de/m/patholo` and every internal import echoed the pre-rebrand name. Sweep: - go.mod: module path → mgit.msbls.de/m/paliad - All *.go files: imports rewritten via sed - README.md, docs/design-kanzlai-integration.md: mAi/paliad → m/paliad - Frontend issue-reference comments (mAi/paliad#N → m/paliad#N) in i18n.ts, theme.ts, sidebar.ts, app.ts, Sidebar.tsx, PWAHead.tsx, global.css Verified: go build/vet/test ./... clean, bun run build clean, no remaining mgit.msbls.de/m/patholo or mAi/paliad references outside docs that intentionally describe the rename history. |
||
|
|
fca7143244 |
feat(t-paliad-089): admin Event-Type moderation panel
Q6 of t-paliad-088 left firm-wide event_type creation open to any user; this
ships the moderation surface admins use to dedupe and clean up the resulting
drift.
Service layer (internal/services/event_type_service.go):
- ListAllForAdmin(filter) — firm-wide rows with usage_count and
author_display_name, optionally including archived (single query, scalar
subquery + LEFT JOIN paliad.users). Sorted live-first, then category +
label_de.
- ListPrivatePendingPromotion — every private non-archived row across all
users, sorted by usage_count DESC.
- ArchiveBulk(ids) — UPDATE archived_at=now() WHERE is_firm_wide AND NULL.
- Promote(id) — flip is_firm_wide=true; surfaces ErrEventTypeSlugTaken on
collision so the admin can merge instead.
- Restore(id) — flip archived_at back to NULL; same slug-collision surface.
- MergeIDs(winner, losers) — tx-scoped INSERT … SELECT … ON CONFLICT
redirect of deadline_event_types from losers → winner, then DELETE on the
loser junction rows, then archive the losers. Refuses if the winner is
archived or private. Junction PK does the dedup.
- requireAdmin gate runs at every method (defence-in-depth on top of the
handler-level RequireAdminFunc).
Handlers (internal/handlers/admin_event_types.go):
- GET /api/admin/event-types[?include_archived=1]
- GET /api/admin/event-types/private
- POST /api/admin/event-types/archive { ids:[…] }
- POST /api/admin/event-types/merge { winner_id, loser_ids:[…] }
- POST /api/admin/event-types/{id}/promote
- POST /api/admin/event-types/{id}/restore
- GET /admin/event-types page shell.
All wrapped behind auth.RequireAdminFunc at registration time.
Frontend:
- New /admin/event-types SPA (admin-event-types.tsx + client/admin-event-types.ts):
search, "Archivierte anzeigen" toggle, per-row archive/restore, bulk
archive, merge modal (winner picker defaults to highest-usage row),
separate table for private types pending promotion.
- Sidebar entry under Verwaltung; admin landing card.
- ~50 i18n keys DE+EN under admin.event_types.* + nav.admin.event_types.
- CSS for archived badge, merge option list, bulk-actions bar.
Out of scope (deferred): public "merge request" workflow for non-admins.
|
||
|
|
909167b036 |
refactor(t-paliad-091): NoteService consolidation — push CanSee to parent services
F-3 from t-paliad-074 architecture audit. NoteService used to call ProjectService.GetByID and AppointmentService.GetByID just for the visibility bit (~6 cross-service full-row reads in note_service.go). Each was a full SELECT on the parent row when only a boolean was needed. Add CanSee(ctx, userID, id) (bool, error) on the two parent services: single EXISTS round-trip, no projection. Personal Appointments stay visible only to their creator; project-anchored Appointments inherit the project's visibility predicate (global_admin shortcut + team-walk). NoteService gains two private helpers — requireProjectVisible and requireAppointmentVisible — that wrap CanSee + ErrNotVisible. All visibility-only sites in note_service.go (ListForProject / ListForDeadline / ListForAppointment / ListForProjectEvent / CreateForProject / CreateForDeadline / requireVisible) now go through the helpers. CreateForAppointment keeps appointment.GetByID — it legitimately needs the appointment's project_id for the audit-event row. DeadlineService.CanSee was not added: note_service never reaches into the deadline service for visibility (it does its own SELECT project_id FROM paliad.deadlines and gates via the project predicate). Test: cansee_test.go covers the gate level for both new methods — admin sees everything (global_admin shortcut), team member sees their team's, non-member sees nothing, missing IDs are invisible to all, personal appointments are private to creator. |
||
|
|
04ce6a8bfa |
feat(t-paliad-088): Event Types for deadlines — schema + service + handlers (PR-1)
Migration 030 adds paliad.event_types and paliad.deadline_event_types
junction. ~43 firm-wide seeds biased toward submissions (25 UPC
submissions + 8 UPC decisions/orders/hearings + 5 EPO + 4 DPMA/DE + 1
cross-jurisdiction). UPC-seeded rows carry a loose trigger_event_id
column (no FK constraint per Q2: event_types leads, trigger_events
follows). RLS policies are defense-in-depth — primary enforcement is
in the Go service layer. Per Q6, any authenticated user can create
firm-wide types; admins moderate via the soft-delete archive lever.
EventTypeService: List (firm-wide ∪ own-private), GetByID, Create
(slug auto-derived, supports diacritics → ASCII), Update (author OR
admin-on-firm-wide), SuggestSimilar (powers the duplicate-warning in
the add modal), AttachToDeadlineTx + ValidateForUser + ListForDeadlines
for the junction.
DeadlineService gains an EventTypeService dependency and now:
- accepts event_type_ids on Create / Update / CreateBulk
- attaches them in the same transaction as the deadline insert
- hydrates EventTypeIDs on every Get / List / ListForProject
- supports the multi-select Typ filter via ListFilter.EventTypeIDs +
IncludeUntyped (UNION semantics within types, AND-intersected with
Status/Project)
AgendaService gets the same Typ filter on its deadline side;
appointments are unaffected.
API:
- GET /api/event-types?category=&jurisdiction=
- GET /api/event-types/suggest?q=
- POST /api/event-types
- PATCH /api/event-types/{id} (set archive=true to hide)
- GET /api/deadlines?event_type=<uuid>,<uuid>,none
- GET /api/agenda?event_type=<uuid>,<uuid>,none
- POST/PATCH /api/deadlines accept event_type_ids: [uuid]
go build / go vet / go test ./... clean.
Frontend (picker + custom-add modal + multi-select filter) follows in
PR-2. Admin moderation panel deferred to t-paliad-089 follow-up.
|
||
|
|
d00974424f |
fix(t-paliad-086): Tier 1 Fristenrechner bug fixes — PR-3
Implements the four audit recommendations from §6.1 of docs/audit-fristenrechner-completeness-2026-04-30.md plus a holiday- adjustment cap fix surfaced by PR-2's smoke test. (1) UPC_INF CCR-conditional rejoinder Public Fristenrechner now flips inf.reply (RoP.029.b → RoP.029.a) and inf.rejoin (1mo / RoP.029.c → 2mo / RoP.029.d) when the user ticks "Mit Widerklage auf Nichtigkeit." Implemented via a new `condition_flag` column on paliad.deadline_rules: when the rule names a flag and the API request's flags array contains it, the calculator substitutes alt_duration_value/unit and alt_rule_code. Independent of the existing `condition_rule_id` mechanism (which references a real rule in the same proceeding tree — only useful for matter-attached trees that already seed the CCR rule). (2) UPC_APP / internal APP grounds anchoring `app.grounds` is now anchored on the trigger date (the appealed decision) with a 4-month duration, not chained 2mo after `app.notice`. Per RoP 220.1 the legal rule is "4 months from notification of the decision," independent of when the notice itself was filed. The chain only happened to give the right answer when both legs landed on a working day; under holiday rollover (e.g. notice deadline pushed to Monday) the grounds deadline drifted off the 4mo legal target. (3) EP_GRANT publish anchor on priority date New `anchor_alt` column on paliad.deadline_rules. ep_grant.publish carries `anchor_alt='priority_date'`. The Fristenrechner UI surfaces an optional "Prioritätstag" input (visible only when EP_GRANT is selected) that, when populated, anchors the publish-A1 calculation on the priority date instead of the filing. Falls back to filing date when the priority field is empty (the case for purely-EP applications with no foreign priority claim). (4) Rule-code format normalisation Migration 029 normalises 'RoP 23' → 'RoP.023', 'RoP 29b' / 'RoP.029b' → 'RoP.029.b', 'RoP 220.1' → 'RoP.220.1', etc. across deadline_rules. Matches the canonical youpc format already used by the PR-1 imported event-deadline rule codes. (+) AdjustForNonWorkingDays cap bumped 30 → 60 Surfaced by the PR-2 smoke test: SoD on 2026-04-30 (3mo from trigger) landed on Sat 2026-08-29 instead of Mon 2026-08-31. The 30-iteration safety bound on AdjustForNonWorkingDays cannot walk past the 33-day UPC summer vacation plus flanking weekends. Bumped to 60. Pure-Go one-liner, locked by a follow-up production smoke (real paliad.holidays seed has the UPC vacation). Schema (migration 029): two new nullable text columns on paliad.deadline_rules — `condition_flag` and `anchor_alt`. Both ignored by every existing rule; only the rows updated above carry values. Models: DeadlineRule gains ConditionFlag + AnchorAlt (nilable strings). Service: FristenrechnerService.Calculate now takes a CalcOptions struct (PriorityDateStr, Flags). API handler accepts optional priorityDate and flags fields on POST /api/tools/fristenrechner. Frontend: TSX surfaces the priority-date row + CCR checkbox conditionally on selectedType (only EP_GRANT / UPC_INF respectively). Client TS reads them and threads through the API call. New i18n keys for both DE+EN. Migration 029 dry-run validated on prod Supabase (BEGIN/ROLLBACK): schema + UPDATEs apply cleanly, rule states match expected post-fix shape. Tests + go build/vet + bun build all clean. |
||
|
|
d78f20be8a |
feat(t-paliad-086): trigger-event Fristenrechner mode + working_days primitive — PR-2
Adds the second Fristenrechner mode (mirrored from youpc.org's deadline
calc): pick a UPC trigger event + date, see all deadlines that flow
from it. Coexists with the existing course-of-proceedings timeline mode
via a tab toggle on /tools/fristenrechner.
Backend:
- internal/services/event_deadline_service.go — EventDeadlineService.
ListTriggerEvents (alphabetical), Calculate (resolves all deadlines
flowing from a trigger). Routes through HolidayService for weekend +
holiday rollover. Honours the new working_days unit. Resolves
composite rules (alt_* + combine_op) by computing both legs and
picking max/min. Used by R.198/R.213 ("31d OR 20wd, whichever is
longer") imported in PR-1.
- internal/services/event_deadline_service_test.go — covers
addWorkingDays (forward, backward, zero, holiday-skip), composite
rule semantics, before-timing.
- internal/handlers/fristenrechner.go — two new endpoints:
GET /api/tools/trigger-events, POST /api/tools/event-deadlines.
- handlers.Services / dbServices: new EventDeadline / eventDeadline
field; wired in cmd/server/main.go from the same HolidayService.
Frontend:
- frontend/src/fristenrechner.tsx — tab strip + second wizard panel
(3 steps: trigger picker → date → flat result list).
- frontend/src/client/fristenrechner.ts — initEventMode wiring,
typeahead filter over the 102 trigger events, Calculate flow,
bilingual rendering, composite-rule labels, lang-change refresh.
- frontend/src/client/i18n.ts — 27 new keys (DE+EN) under
deadlines.mode.* and deadlines.event.* (incl. units, timing).
- frontend/src/styles/global.css — fristen-mode-tabs, mode-panel,
event-list, event-result-row visual style.
Working-day arithmetic detail: the new addWorkingDays helper steps
one day at a time and skips runs of non-working days (Sat/Sun + DE
federal + UPC vacations seeded via paliad.holidays). Day-zero is the
caller's job — addWorkingDays(0) returns the input unchanged so
callers can decide whether to roll forward via AdjustForNonWorkingDays.
Composite-rule resolution: when a row carries alt_duration_value +
alt_duration_unit + combine_op, Calculate computes both legs,
picks max/min, and surfaces a compositeNote like
"max(31 days, 20 working_days) → working_days leg" so the UI can
explain which leg won.
PR-3 will land Tier 1 bug fixes from the audit (CCR adaptive,
UPC_APP grounds anchoring, EP_GRANT priority, rule-code normalisation).
|
||
|
|
ce3227c1c0 |
refactor(t-paliad-080): service-layer naming sweep — Notiz/Termin/Frist/Projekt/Partei → Note/Appointment/Deadline/Project/Party
Mechanical rename across 8 service files plus their handler call sites and two related helpers. The English types existed already; what changed are the input-struct names, helper functions, list/create method suffixes, and parameter names so they no longer mix English types with German parameter names. Renames cover: - CreateNotizInput/UpdateNotizInput → CreateNoteInput/UpdateNoteInput, notizColumns/notizSelect → noteColumns/noteSelect, ListForProjekt/Frist/ Termin → ListForProject/Deadline/Appointment, CreateForProjekt/Frist/ Termin → CreateForProject/Deadline/Appointment, fristProjectID → deadlineProjectID - CreateTerminInput/UpdateTerminInput → CreateAppointmentInput/ UpdateAppointmentInput, terminColumns → appointmentColumns, ListForProjekt → ListForProject; parameter renames terminID → appointmentID, projektID → projectID - CreateFristInput/UpdateFristInput → CreateDeadlineInput/ UpdateDeadlineInput, fristColumns → deadlineColumns, ListForProjekt → ListForProject, isValidFristStatus → isValidDeadlineStatus; parameter renames fristID → deadlineID, projektID → projectID - CreateProjektInput/UpdateProjektInput → CreateProjectInput/ UpdateProjectInput, projektColumns → projectColumns, validateProjektStatus → validateProjectStatus, ProjektRole comment → ProjectRole - CreateParteiInput → CreatePartyInput, parteiColumns → partyColumns, ListForProjekt → ListForProject; parameter renames parteiID → partyID - OnTerminCreated/Updated/Deleted → OnAppointmentCreated/Updated/Deleted on the AppointmentCalDAVPusher interface and its CalDAVService impl - formatTermin → formatAppointment in caldav_ical - ListForProjekt → ListForProject, listWithProjekt → listWithProject, checklistInstanceWithProjektSelect → checklistInstanceWithProjectSelect, ClearProjekt → ClearProject (JSON tag clear_projekt unchanged — wire format) - insertProjectEvent helper parameter projektID → projectID, error message "insert projekt_event" → "insert project_event" - TeamService AddMember/RemoveMember/ListDirectMembers/ListEffectiveMembers parameter projektID → projectID; matching handler renames - Frontend doc-comments referencing CreateProjektInput/UpdateProjektInput updated to CreateProjectInput/UpdateProjectInput JSON wire tags (clear_projekt, etc.) and German user-facing strings (glossary entries, search.go labels, email templates, changelog, Terminsgebuehr, Fristenrechner product name) are intentionally untouched. API contract unchanged. go build/vet/test ./... clean. Frontend bun build clean. |
||
|
|
31db66e3b7 |
refactor(t-paliad-076): consolidate visibility predicate — 6 dashboard/agenda sites use helper
F-2 from t-paliad-074 audit. The inlined visibility predicate had drifted
back into 6 hot-path SQL sites despite the central helper extracted in
t-paliad-058. Consolidating now so future visibility changes (e.g.
Chinese-wall in design v2 §8) only need one edit.
**Sites converted (6):**
- dashboard_service.go:158, 214, 244, 274
- agenda_service.go:138, 204
All six replace `$N = 'global_admin' OR EXISTS (path-walk)` with the
existing `visibilityPredicatePositional("p", 1)` helper. The helper
resolves global_admin via EXISTS on paliad.users — the role string no
longer flows through positional args, removing one foot-gun (typo'd
literal mismatched against bound role) entirely. Equivalence verified
on the live youpc DB:
tester@hlc.de (global_admin, 1 team membership):
old predicate count = 11 new predicate count = 11
standard user (no team):
old predicate count = 0 new predicate count = 0
**No new helper variant added.** The audit suggested
`visibilityPredicateLateral`, but the existing positional helper drops
into the dashboard/agenda WHERE clauses unchanged — adding a redundant
variant would be technical debt. dashboard/agenda do not use LATERAL
JOIN; they use plain WHERE EXISTS in (sub-)SELECT context, which is
already what visibilityPredicatePositional emits.
**Other 4 sites flagged by audit — left intentionally:**
- reminder_service.go:312, 325 are role-restricted (`pt.role = 'lead'`)
membership checks, NOT visibility predicates. Adding a global_admin
shortcut to the lead branch would over-include rows: every global
admin would receive every project's lead-targeted reminder, even with
the `own.escalation_contact_id` override that exists precisely to
avoid that. global_admin already has its own dedicated branch in the
query (`$3 = TRUE AND own.escalation_contact_id IS NULL` at line 328).
- deadline_service.go:422 (`assertCanAdminProject`) is role-restricted
(`pt.role IN ('admin', 'lead')`) and already short-circuits global_admin
at the Go level before the SQL runs (line 413). Both halves correct;
no change needed.
- team_service.go:162 (`IsEffectiveMember`) was dead code with no callers
in the entire repo. "Is this user a structural team member?" and
"can this user see this project?" are different questions; adding a
global_admin shortcut would have conflated them. Deleted instead.
**Test:** new TestVisibilityPredicate_DashboardAgendaForGlobalAdmin in
visibility_test.go seeds a project + deadline + appointment + activity
event with project_teams empty, then asserts a global_admin sees all
four on /dashboard and /agenda while a standard user sees none. Skips
when TEST_DATABASE_URL is unset (matching the existing live-DB tests).
**Pre-existing finding (separate concern):** the live-DB test gate is
currently blocked locally by a stale `public.paliad_schema_migrations`
(version=2, dirty=t) left over from before the schema-pinned tracker
landed. Authoritative `paliad.paliad_schema_migrations` is at version
27, dirty=f. Out of scope for this task; should be filed as cleanup.
|