426b90bb88b136dedbedf942330cb6a9e1b808fa
156 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 07acf7b4a2 |
feat(litigationplanner): Berufung unification — one upc.apl + 5 appeal_target chips (Slice B1, m/paliad#124 §18.1)
Collapses the 3 UPC appeal proceeding_types (upc.apl.merits 7 rules,
upc.apl.cost 2, upc.apl.order 7 = 16 total across 3 codes) into ONE
unified upc.apl proceeding type + a per-rule applies_to_target[]
discriminator. The verfahrensablauf picker now shows one "Berufung"
tile; after picking it, the user selects which decision the appeal is
directed AT via a 5-chip group (Endentscheidung / Kostenentscheidung /
Anordnung / Schadensbemessung / Bucheinsicht) and the engine filters
rules whose applies_to_target contains the picked slug.
m's 2026-05-26 decision: Schadensbemessung-as-appeal is a NEW first-
class target with its OWN rule set (no shared inheritance from
merits). The 5 enum values are all defined + addressable; for now
schadensbemessung and bucheinsicht return empty timelines until rules
are seeded in a follow-up slice (likely via /admin/rules or pairing
with t-paliad-193 orphan-concept-seed).
Migration 134 (additive only):
- ADD proceeding_types.appeal_target text (CHECK on 5 slugs OR NULL)
- ADD deadline_rules.applies_to_target text[] (CHECK each element
in the 5 slugs)
- INSERT the unified upc.apl row (inherits sort/color from
upc.apl.merits)
- Audit-first RAISE NOTICE pass listing every row about to be
touched + a post-migration sanity check
- Reassign rule rows: merits → applies_to_target={endentscheidung},
cost → {kostenentscheidung}, order → {anordnung}
- Archive (is_active=false, NOT DELETE) the 3 old proceeding_types
so historical FKs stay intact
- Down migration restores is_active=true on the 3 old types, points
rules back by their applies_to_target stamp, drops the unified
row, drops both columns. Safe.
Package additions (pkg/litigationplanner):
- AppealTarget* constants + AppealTargets[] ordered list +
IsValidAppealTarget(s) predicate (silent no-op on unknown slugs
so a stale frontend chip doesn't break the render)
- ProceedingType.AppealTarget *string field (top-level marker;
NULL on non-appeal proceedings)
- Rule.AppliesToTarget pq.StringArray field (per-row applies-to set)
- CalcOptions.AppealTarget string (engine filter — when set,
keeps only rules whose AppliesToTarget contains the slug)
Engine filter runs after ApplyRuleOverrides but before the rule walk
so the existing condition_expr / spawn / appellant-context machinery
operates on the filtered subset transparently.
paliad-side wiring:
- deadline_rule_service.go: ruleColumns + proceedingTypeColumns
extended to scan the new columns
- handlers/fristenrechner.go: AppealTarget JSON field on the
request payload, threaded into CalcOptions
Frontend (verfahrensablauf surface only):
- Single "Berufung" tile replaces the 3 separate Berufung tiles
- New 5-chip appeal-target row, shown only when upc.apl is picked
- URL state ?target=<slug>; default endentscheidung when none set
- APPELLANT_AXIS_PROCEEDINGS updated: upc.apl.* (3 entries) →
upc.apl (1 entry)
- i18n keys (DE + EN) for the new tile + the 5 chip labels +
the "Worauf richtet sich die Berufung?" / "Appeal against:" prompt
- calculateDeadlines threads appealTarget through to the API
Acceptance:
- go build clean, go test all green (existing test suite — no new
tests on the engine filter as a follow-up; the migration's
sanity-check DO block guards the rule-reassignment count)
- Live audit before drafting confirmed: 3 active UPC appeal
proceeding_types, 16 rules total, primary_party already conforms
to 4-value vocab on all proceeding-bound rules
|
|||
| 016ac2532a | Merge: t-paliad-282 Slice A — CI/CD pre-deploy gate + snapshot-based migration smoke (m/paliad#114) | |||
| c901293c9c |
feat(cicd): Slice A — pre-deploy gate + role-split migration smoke
Adds .gitea/workflows/test.yaml that gates every push on `go build`, `bun run build`, `go vet`, the migration coordination check, and the role-split end-to-end migration smoke. On push to main + green, calls Dokploy's compose.deploy API and polls /health/ready until 200. t-paliad-282 / m/paliad#114. Design: docs/design-cicd-pre-deploy-gate-2026-05-25.md (inventor shift on mai/cronus/inventor-ci-cd-pre). Catches all three of today's outage classes: brunel (~13:20) slot collision -> TestMigrations_NoDuplicateSlot hermes (~16:05) dropped-col refs -> TestBootSmoke mig 129 (~14:56) 42501 ownership -> TestMigrations_EndToEndAsAppRole Snapshot approach. internal/db/testdata/prod-snapshot.sql is a pg_dump of youpc-supabase paliad schema + applied_migrations rows. CI restores this into a fresh `supabase/postgres:15.8.1.060` (same image, same role topology as prod) and runs ApplyMigrations as the `postgres` role (which is NOT a superuser on supabase/postgres, matching prod). Existing migrations are skipped (already in applied_migrations); only NEW migs from the PR run end-to-end. This sidesteps the fresh-DB idempotence debt in some historical migrations (mig 037 missing pg_trgm, mig 051 inner COMMIT) — those are tracked separately and don't block the gate. Sub-changes: - internal/handlers/handlers.go — new /health/ready endpoint distinct from /healthz. /healthz stays liveness (process alive, no DB); /ready is readiness (DB pool pings within 2 s). Returns 503 when svc or pool is nil (DB-less deploys are intentionally not-ready). svc.Pool added to handlers.Services, wired in cmd/server/main.go. - internal/db/migrate_test.go — TestMigrations_NoDuplicateSlot (pure unit, catches brunel) and TestMigrations_EndToEndAsAppRole (snapshot- gated, catches the 42501 class). - cmd/server/main_smoke_test.go — TestBootSmoke now also asserts /health/ready returns 503 with a nil svc. New TestHealthReady_Live asserts 200 against a live pool. - internal/db/migrations/024_rename_department_columns.up.sql and 027_rename_to_partner_units.up.sql — ALTER INDEX / ALTER POLICY exception handlers now catch undefined_object OR undefined_table OR duplicate_object. Old handler only caught undefined_object; Postgres raises undefined_table when source object never existed, and duplicate_object when destination already exists. The expanded handlers make these migrations truly idempotent across all plausible starting states. - Makefile — verify-mig-app, test-frontend, refresh-snapshot targets. refresh-snapshot pg_dumps youpc-supabase prod (needs PALIAD_PROD_DATABASE_URL), strips pg16 \restrict commands for pg15 restore compat, and filters applied_migrations rows to this branch's max on-disk version. - internal/db/testdata/README.md — explains the snapshot's purpose, refresh procedure, and how to verify locally. - docs/cicd-runner-setup-2026-05-25.md — one-time admin steps for registering a Gitea Actions runner on mriver and wiring DOKPLOY_TOKEN as a repo secret. Documents soft-launch plan per m's Q11.4 (keep Dokploy's autoDeploy=true webhook alive for one week, disable after the workflow has gated 5 successful deploys). Build clean. Full go test ./internal/... ./cmd/... green without TEST_DATABASE_URL. With TEST_DATABASE_URL + TEST_APP_DATABASE_URL set to a supabase/postgres scratch + snapshot restored: TestMigrations_NoDuplicateSlot, TestMigrations_EndToEndAsAppRole, TestBootSmoke, TestHealthReady_Live all pass. Live-DB service tests in internal/services/* fail under supabase/postgres 15.8 with a 42P08 parameter-binding error (unrelated to Slice A — tracked as a follow-up). |
|||
| 0b1653c2bf | Merge: t-paliad-284 — Wave 1 Tier 1 rule additions + Q6 archived cleanup + audit FK fix (mig 132) (m/paliad#116) | |||
| a6cf6ff4c9 |
feat: t-paliad-284 Wave 1 Tier 1 deadline-rule additions (mig 132)
Add 12 Tier 1 procedural deadline rules from curie's audit §10 (docs/research-deadlines-completeness-2026-05-25.md), backfill the UPC R.104/R.105 Interim Conference citation on upc.inf.cfi.interim (m/paliad#116 / m's 2026-05-25 report), and fold in the audit Q6 cleanup of the 40 _archived_litigation.* rows. New rules: T1.1 upc.inf.cfi.cmo_review 15d / R.333.2 T1.2 upc.inf.cfi.confidentiality_response 14d / R.262.2 (trigger 25) T1.3 upc.apl.order.grounds_orders 15d / R.224.2(b) T1.4 upc.apl.order.response_orders 15d / R.235.2 T1.5 upc.inf.cfi.cons_orders 2mo / R.118.4 T1.6 upc.inf.cfi.rectification 1mo / R.353 T1.7 upc.pi.cfi.deficiency 14d / R.207.6(a) T1.8 upc.pi.cfi.merits_start 31d OR 20wd (max) / R.213 + R.198.1 T1.9 upc.inf.cfi.translation_request 1mo BEFORE oral / R.109.1 T1.10 upc.inf.cfi.interpreter_cost 2wk BEFORE oral / R.109.4 T1.11 upc.inf.cfi.translations_lodge 2wk / R.109.5 (trigger 113) T1.12 upc.pi.cfi.response UPDATE: re-anchor on .app, court-set T1.8 uses Wave 2 Slice A primitives (mig 128: working_days unit + combine_op='max'). T1.9/T1.10 use timing='before' with the backward-snap path in deadline_calculator.go. Also drops the deadline_rule_audit.rule_id FK constraint. The mig 079 audit trigger had a latent bug — it could not log DELETEs because the FK rejected the post-delete INSERT (count(*) WHERE action='delete' was 0 across the entire history). Audit tables are append-only history and should not FK-constrain on live entity tables; before_json preserves the full row state. Unblocking this also unblocks the §13b Q6 cleanup. Verified on Supabase: 13 rows present in post-fix shape, all assertions in the DO-block pass, audit log now records 11 creates + 2 updates + 40 deletes for this migration. |
|||
| cb44b3b8cc |
mAi: #117 + #118 - t-paliad-285/-286 UPC dmgs+pi court followup (mig 133)
Adds the post-submission court phase to upc.dmgs.cfi and the appeal route to upc.pi.cfi. The Verfahrensablauf timeline currently stops at the last party submission (dmgs.rejoin / pi.order); without these rows the interim conference / oral hearing / decision / appeal sub-tree never renders, even though atlas's #96 spawn mechanism is in place. Migration 133 (single slot, coordinated with knuth's #116 on 132): Section A — UPC Damages tree end (#117): - upc.dmgs.cfi.interim court-set, R.105 - upc.dmgs.cfi.oral court-set, R.118 / R.250 - upc.dmgs.cfi.decision court-set, R.118 / R.144 - upc.dmgs.cfi.appeal_spawn 2mo, R.220.1(a) / R.224.1(a), spawn → upc.apl.merits Section B — UPC PI appeal route (#118): - upc.pi.cfi.appeal_spawn 2mo, R.220.1(a) / R.224.1(a), spawn → upc.apl.merits PI orders under R.211 dispose of the urgent question and ride the main 2-month track; the 15-day R.220.1(c) order track does not apply. Same shape as mig 095 inf.appeal_spawn and the upc.inf.cfi interim/oral/decision rows from mig 012. Court-set rows reuse the shared interim-conference / oral-hearing / decision concepts. Citations: docs/research-deadlines-completeness-2026-05-25.md §D + Tier 4 (R.144), docs/audit-upc-rop-deadlines-2026-05-08.md §D R.144 + §F R.220.1(a)/R.224.1(a). Per-row RoP citation in the migration header. Idempotent INSERT NOT EXISTS guards per row + post-insert DO block that RAISEs EXCEPTION if any expected row is missing or the spawn shape (is_spawn / spawn_proceeding_type_id / parent_id) is wrong. go build ./... clean, go test ./internal/... clean, bun run build clean. |
|||
| e4c694e01c |
mAi: #108 - t-paliad-276 submission generator language selector (DE/EN)
Per-draft `language` column drives the .docx output language for the
submission generator. The lawyer picks DE or EN on the draft editor's
sidebar; the generator selects the language-matched template variant
(falling back through {code}.{lang} → {code} → _skeleton.{lang} →
_skeleton → letterhead) and resolves language-aware variables
({{procedural_event.name}} → name_de vs name_en).
Schema (mig 130 — bumped from 129 to deconflict with atlas's #96):
- paliad.submission_drafts.language text NOT NULL DEFAULT 'de'
CHECK IN ('de','en'). Existing rows inherit 'de' via the default,
preserving every legacy draft's behaviour byte-for-byte.
Backend (Go):
- SubmissionVarsContext.Lang overrides the user's UI lang. Build()
uses it when set; falls back to user.Lang otherwise — Slice 1's
format-only /generate path keeps working unchanged.
- SubmissionDraftService.BuildRenderBag now threads draft.Language
through. Create/EnsureLatest seed from the UI lang (DE default).
- DraftPatch.Language landed; Update validates and rejects values
outside {de,en}. Project-scoped + global PATCH endpoints both
surface the field.
- resolveSubmissionTemplate(ctx, code, lang) replaces the lang-less
predecessor. Returns the matched tier (per_code_lang / per_code /
skeleton_lang / skeleton / letterhead) so the editor knows whether
to surface the "Fallback: universelles Skelett" notice.
- fileRegistry registers the EN skeleton sibling (`_skeleton.en.docx`)
alongside the DE one; per-code EN variants land in a parallel
submissionTemplateENRegistry (empty for now — EN templates land per
HLC authoring). 404s from Gitea fall through silently.
- /api/projects/{id}/submissions/{code}/generate accepts
`?language=de|en` query override (one-shot path, no draft row to
pull the column from); defaults to the user's UI lang.
Frontend (TS/JSX):
- DE/EN radio above the variables list in the draft editor sidebar.
Switching the radio PATCHes `language` and the server returns the
freshly-resolved bag + preview HTML so the lawyer sees EN values
immediately.
- Fallback notice ("Fallback: universelles Skelett (keine
sprachspezifische Vorlage)") shows when the resolved tier doesn't
match the requested language.
- 4 new i18n keys (DE + EN) + CSS for the toggle.
Tests:
- normalizeDraftLanguage covers DE/EN/case/whitespace/unknown.
- addRuleVars language-pick test pins procedural_event.name and the
rule.name alias to the language-matched value.
- languageFallback truth table covers all 10 (lang × tier) combos.
Build hygiene: go build/vet/test clean; bun run build clean.
|
|||
| c6267e4e6d | Merge: t-paliad-277 — submission party selector + import-from-project (mig 131) (m/paliad#109) | |||
| 4fc3005db8 |
mAi: #109 - t-paliad-277 submission generator party selector + import-from-project
Multi-select party picker on the dedicated submission draft editor —
lawyer picks which of the project's parties to mention in this
specific submission. Adds the t-paliad-277 variable-bag multi-party
shape ({{parties.claimants}}, {{parties.claimant.0.name}}) while
keeping the legacy flat aliases ({{parties.claimant.name}}) for every
existing .docx template authored before the rename.
Surfaces an explicit "Aus Projekt importieren" button + last-imported
timestamp at the top of the variable sidebar so the lawyer can re-pull
project-derived variables (project.*, parties.*, deadline.*,
procedural_event.*, rule.*) when the project data drifts away from the
saved draft overrides. firm.*, today.*, user.* overrides survive the
import — those values aren't sourced from the project record.
Schema: mig 131 adds two columns to paliad.submission_drafts:
- selected_parties uuid[] DEFAULT '{}'::uuid[]
Empty = include every party (legacy default).
Non-empty = restrict to the subset, grouped by role at substitution.
- last_imported_at timestamptz NULL
Bumped each "Aus Projekt importieren" click; surfaced in UI.
Backend:
- SubmissionVarsContext gains SelectedParties — filterPartiesBySelection
restricts the resolved bag before role bucketing.
- addPartyVars emits THREE coexisting forms per role: comma-joined
(parties.claimants), indexed (parties.claimant.0.name), and flat
legacy (parties.claimant.name → first selected claimant). Flat
aliases are kept forever per the issue's backward-compat contract.
- SubmissionDraftService.ImportFromProject strips overrides for
project-derived prefixes and bumps last_imported_at; rejects
project-less drafts (nothing to import from).
- New endpoint POST /api/submission-drafts/{id}/import-from-project.
- DraftPatch + PATCH handlers accept selected_parties.
- submissionDraftView now ships available_parties so the editor can
render the picker without an extra round-trip.
Frontend:
- submission-draft.tsx: new import-row + parties block in the sidebar.
- client/submission-draft.ts: paintImportRow / paintPartyPicker /
onPartySelectionChange / onImportFromProject; group parties by
role bucket (claimant / defendant / other) with DE+EN role-string
matching to mirror the backend bucketing.
- 3 new i18n keys (DE+EN): import.button, parties.title, parties.hint.
- CSS for the picker + import row in global.css.
Tests: 6 new unit tests in submission_vars_parties_test.go covering
the multi-party bag emission, German role-string bucketing, flat-alias
first-of-role resolution, empty-selection-means-all default, non-empty
restriction, and the isProjectDerivedKey policy that powers the
import path.
Build hygiene: go build/vet clean; go test -short ./internal/... pass;
bun run build clean (2876 i18n keys, scan clean).
|
|||
| dc47ea7f43 |
feat(t-paliad-265): migration 129 + EventChoiceService (Slice A foundation)
m/paliad#96 — per-event-card optional choices on the Verfahrensablauf timeline. This commit lands the schema + service layer. Migration 129: - paliad.project_event_choices table (project_id, submission_code, choice_kind ∈ {appellant, include_ccr, skip}, choice_value) with UNIQUE(project_id, submission_code, choice_kind) for idempotent re-pick, RLS via paliad.can_see_project. - paliad.deadline_rules.choices_offered jsonb — opt-in declaration of which choice-kinds each rule offers. Seeded for every decision rule (appellant), every priority='optional' rule (skip), and the two Klageerwiderung rules (upc.inf.cfi.sod + de.inf.lg.erwidg) with include_ccr. Live verification before authoring: - rule_code is NULL on every decision row → submission_code is the join key (matches AnchorOverrides plumbing in fristenrechner.go). - upc.inf.cfi.sod is the UPC Klageerwiderung, not upc.inf.cfi.def (rejected the design doc's first guess; SELECT name ILIKE 'Klageerwiderung' confirmed). Go service: - models.ProjectEventChoice + DeadlineRule.ChoicesOffered. - EventChoiceService: ListForProject / Upsert (with audit-log row to paliad.system_audit_log) / Delete. Pure-helper ToCalcOptionsAddendum + per-kind value validation + unit tests. Design: docs/design-event-card-choices-2026-05-25.md §3 + §6. |
|||
| f4dee97493 | hotfix: drop is_optional + condition_flag refs from mig 125 (both dropped in earlier mig; unblock prod) | |||
| 7aed8e4ec5 | Merge: t-paliad-271 — Tier 3 deadline-rule primitives Slice A (working_days + combine_op + before-mode, mig 128) (m/paliad#103) | |||
| b429dabf9e | hotfix: drop is_mandatory ref from mig 125 (column removed in mig 091; was blocking prod boot) | |||
| d3c28009de |
mAi: #103 - t-paliad-271 Wave 2 Tier-3 Slice A — deadline-rule primitives
Implements three Tier 3 primitives from curie's bulletproof completeness
audit (docs/research-deadlines-completeness-2026-05-25.md §10 T3.1, T3.2,
T3.5), per m's 2026-05-25 15:29 steer to build the full primitives
instead of documenting workarounds.
Primitive 1 — duration_unit='working_days':
Calculator walks day-by-day skipping weekends + court holidays via
HolidayService.IsNonWorkingDay. Event day is not counted; result is
always a working day for the (country, regime). Unlocks T1.8/T1.9
modeling and the R.198 / R.213 alt leg.
Primitive 2 — combine_op='max' (and 'min'):
When alt_duration_value + alt_duration_unit + combine_op are set, the
calculator evaluates both legs and picks the later (max) or earlier
(min) of the two adjusted end dates. The DB already had two rules
shaped this way ('31d OR 20wd, whichever is longer' — R.198 / R.213);
the calculator was silently dropping the alt leg.
Primitive 5 — timing='before' backward snap-to-working-day:
For backward rules (R.109.1: 1 month before oral hearing; R.109.4:
2 weeks before) the calculator now snaps to the PRECEDING working day
when the computed cut-off lands on a weekend/holiday. Forward snap
(the prior behavior) would push the cut-off past the statutory limit
and miss the deadline. Adds HolidayService.AdjustForNonWorkingDays-
Backward as the symmetric counterpart of AdjustForNonWorkingDays.
Migration 128 — DB schema:
Adds CHECK constraints on deadline_rules.duration_unit and
alt_duration_unit pinning the allowed set to days/weeks/months/
working_days. Live data audited and passes (no rows excluded).
Tests (12 new + 1 flipped):
- 5 working_days cases: forward over weekend, 20wd anchored on Fri,
across Karfreitag/Ostermontag, across year boundary, backward
from Friday, anchored on Saturday.
- 2 backward snap cases: Sun → preceding Fri; cluster Sun → Sat →
Karfreitag → Thu.
- 4 combine_op cases: max with primary winning, max with alt winning
over Christmas+Neujahr cluster, min with primary winning, NULL-alt
short-circuit.
- TestCalculateEndDate_BeforeTiming renamed and flipped from forward
(Sun → Mon, the prior wrong behavior) to backward (Sun → Fri).
No regression on existing rules: every pre-existing days/weeks/months
'after' rule still computes the same date. Frontend build + full
go test ./internal/... clean.
Slot 128 assigned per next-available convention (mig 127 = Wave 0
Tier-0 fixes, mig 128 = Wave 2 Tier-3 Slice A primitives).
|
|||
| ff503ffc43 | Merge: Wave 0 Tier-0 deadline-rule fixes — 13 UPDATEs + #99 SoC mapping (mig 127) from curie's #94 audit (m/paliad#94, m/paliad#99) | |||
| 05f7ea2af5 |
mAi: #99 #94 - t-paliad-263 Wave 0 - Tier 0 deadline-rule corrections
Migration 127 lands curie's audit-doc Tier 0 sweep (docs/research- deadlines-completeness-2026-05-25.md section 10) plus the UPC Statement of Claim citation backfill from m/paliad#99. 14 single-row UPDATEs touching UPC + DE-LG + DPMA + EPA proceedings: T0.1 upc.rev.cfi.defence dur 3mo -> 2mo (RoP.049.1) T0.2 upc.rev.cfi.rejoin dur 2mo -> 1mo (RoP.052) T0.3 upc.apl.merits.response dur 2mo -> 3mo (RoP.235.1) T0.4 de.inf.lg.beruf_begr parent_id berufung -> NULL (ZPO 520.2) T0.7 upc.rev.cfi.reply citation backfill RoP.051 T0.9 upc.apl.merits.notice citation RoP.220.1 -> RoP.224.1.a T0.10 upc.apl.merits.grounds citation RoP.220.1 -> RoP.224.2.a T0.12 dpma.opp.dpma.erwiderung flip is_court_set, drop PatG 59.3 T0.13 dpma.appeal.bpatg.begruendung flip is_court_set, drop PatG 75.1 T0.14 de.null.bpatg.erwidg citation PatG 82.1 -> PatG 82.3 T0.15 de.null.bgh.begruendung citation PatG 111.1 -> ZPO 520.2 (via PatG 117) T0.16 de.null.bgh.erwiderung flip is_court_set, recite as ZPO 521.2 (via PatG 117) T0.17 epa.opp.opd.erwidg flip is_court_set (EPO Guidelines D-IV 5.2) #99 upc.inf.cfi.soc backfill UPC RoP R.13(1) citation T0.5 and T0.6 (de.inf.lg.replik / .duplik) shipped separately as mig 124 (m/paliad#95). T0.8 / T0.11 dedup'd into T0.2 / T0.1 per the audit doc. Each UPDATE guarded by a WHERE clause matching only the pre-fix row state (mig 095 convention) - re-apply against a DB carrying the fix matches zero rows and no-ops, no duplicate deadline_rule_ audit entries on idempotent re-runs. Verification DO block at the end RAISE EXCEPTIONs if any row remains in inconsistent state. Applied to live youpc DB via Supabase MCP with audit_reason set (13 rows touched - T0.4 also fired; all 14 verified in post-fix shape via direct query). applied_migrations entry NOT pre-recorded; the boot-time runner inserts version=127 cleanly on next deploy because every guarded UPDATE no-ops at that point. Build hygiene: go build / go test ./internal/... / bun run build all clean (2824 i18n keys, no scan warnings). No code changes - pure data migration. Cites: UPC RoP (UPCRoP.013.1 / 049.1 / 051 / 052 / 224.1.a / 224.2.a / 235.1), PatG 82.3 / 117, ZPO 520.2 / 521.2, EPC R.79(1) + EPO Guidelines D-IV 5.2. |
|||
| e0c8401482 | Merge: t-paliad-266 — event-type modal cross-cutting filter by court system (mig 125) (m/paliad#97) | |||
| e68b800d52 | Merge: t-paliad-249 Slice A — inbox overhaul (project_event feed + read cursor + dispatch) (m/paliad#80) | |||
| 4ead2d08c1 |
feat(inbox): t-paliad-249 Slice A backend — project_event feed + read cursor (m/paliad#80)
Substrate changes that turn /inbox from approvals-only into the unified notification surface m asked for. - Migration 126: paliad.users.inbox_seen_at (high-watermark read cursor; pending approval_requests bypass it per design §3). - KnownProjectEventKinds gains note_created, our_side_changed, deadline_updated/deleted, deadlines_imported. New InboxProjectEventKinds curated subset (head's Q1=A lock). - InboxSystemView spans [approval_request, project_event]; defaults to past 30 days, newest first, row_action="inbox". - view_service.allowedProjectEventKinds drops *_approval_* audits when ApprovalRequest is also in spec.Sources (no double-count). - RunSpec resolves the caller's inbox_seen_at once and threads it through viewSpecBounds; runProjectEvents excludes self-authored events and rows older than the cursor when unread_only is set. Decided approval_requests follow the cursor; pending always survives. - ApprovalService.UnseenInboxCountForUser (unified badge count) + MarkInboxSeen + InboxSeenAt service methods. - GET /api/inbox/count returns the unified count; new POST /api/inbox/mark-all-seen advances the cursor (optional up_to=). Tests cover the InboxSystemView shape, the audit-dedup helper, the isApprovalAuditKind matcher, and the no-narrow-no-approvals nil path. |
|||
| 8c94dccf83 |
mAi: #95 - t-paliad-264 - fix de.inf.lg Replik/Duplik sequencing
Replik and Duplik had parent_id = NULL with a 4-week placeholder
duration, so the projection anchored both off the proceeding's
trigger date (Klageerhebung) - both rows rendered at the same
calendar date AND before Klageerwiderung.
Migration 124 anchors Replik on Klageerwiderung
(de.inf.lg.erwidg) and Duplik on Replik, and marks both
is_court_set = true with legal_source DE.ZPO.273. The 4-week
placeholder duration is retained so the timeline gives a sane
notional date for each row; the lawyer overrides it with "Datum
setzen" once the court issues the actual period.
Each UPDATE is guarded by parent_id IS NULL so a re-apply against
a DB that already carries the fix no-ops cleanly (mig 095
convention). No new audit-log rows on idempotent re-runs.
Slot note: originally landed as 123 in an earlier iteration;
cronus's t-paliad-246 Backup-Mode migration won slot 123 in the
parallel merge race, so this migration shifted to slot 124.
ZPO citations in the migration comment per the t-paliad-264 brief:
- Klageerhebung - section 253 ZPO
- Anzeige Verteidigungsbereitschaft - section 276 Abs. 1 S. 1 ZPO
- Klageerwiderung - section 276 Abs. 1 S. 2 + section 277 ZPO
- Replik / Duplik - vom Gericht bestimmte Frist
(section 273 ZPO Anordnungskompetenz; section 282 ZPO
prozessuale Foerderungspflicht)
Verified ordering for trigger 2026-05-25:
Klage 2026-05-25 Mon
Anzeige 2026-06-08 Mon
Klageerwidg 2026-07-06 Mon
Replik 2026-08-03 Mon
Duplik 2026-08-31 Mon
Each row strictly later than the previous; Replik and Duplik no
longer collide on the same date and no longer precede the
Klageerwiderung.
|
|||
| 90f5dd4b1b | fix: t-paliad-266 — bump migration to slot 125 (123 taken by cronus #77 backups) | |||
| 24f3baf61f |
mAi: #97 - t-paliad-266 — event-type modal: narrow cross-cutting trigger pills by court system
Cross-cutting Wiedereinsetzung sub-rows (PatG §123 / ZPO §233 / EPC Art.122 / DPMA PatG §123 / UPC R.320) used to bypass the forum-bucket chip selection by design — every chip combination returned all five rows. m/paliad#97: chip the chips through to triggers via legal_source inference. - mig 123 backfills the missing deadline_rules row for trigger 207 (UPC R.320 Wiedereinsetzung, orphaned by mig 063 because mig 092 dropped event_deadlines before that path was seeded) and rebuilds paliad.deadline_search with a LEFT JOIN on deadline_rules so cross-cutting trigger pills carry their structured legal_source. - DeadlineSearchService gains ForumToLegalSourcePrefixes (10 buckets → UPC. / DE.ZPO. / DE.PatG. / EU.EPC + EU.EPÜ) paralleling ForumToProceedingCodes. Rule pills still narrow by proceeding_code; trigger pills now narrow by legal_source LIKE prefix. Multiple chips union the prefix allow-list as expected. - Live golden-table test gains a Wiedereinsetzung×forum matrix plus a multi-chip union case, and the existing 4-pill assertion is updated to the now-5-pill state (mig 063 added trigger 207). Branch: mai/hermes/gitster-event-type-modal. |
|||
| 99c9d89daa |
feat(backups): t-paliad-246 — Backup Mode Slice A (on-demand admin org export)
m/paliad#77 Slice A. Folds the unbuilt t-paliad-214 Slice 3 (org async export) into a new "Backup Mode" surface gated by adminGate. m's calls (all 4 material picks per design §2): - Storage: local disk PALIAD_EXPORT_DIR (LocalDiskStore only) - Format: .zip bundle (xlsx + JSON + CSV + README) — no-lock-in preserved - paliadin_turns + paliadin_aichat_conversation: EXCLUDE structurally - Scheduler (Slice B): nightly 03:00 UTC, env-tunable Wiring: - mig 123 adds paliad.backups catalog table (kind/status/storage_uri/ size/row_counts/warnings/error/deleted_at + admin-only RLS). - ExportService.WriteOrg + orgSheetQueries enumerate 37 entity sheets + 12 ref sheets; REPEATABLE READ READ ONLY tx wraps the dump for snapshot consistency (design §3.3). - writeBundle + runSheetQuery refactored to take a sqlx.QueryerContext so both *sqlx.DB (personal/project paths, unchanged) and *sqlx.Tx (org snapshot path) work. - BackupRunner orchestrates: catalog INSERT → audit INSERT (event_type='backup_created') → WriteOrg → ArtifactStore.Put → patch catalog + audit on success/failure. - ArtifactStore interface + LocalDiskStore impl (defense-in-depth key validation + URI-outside-dir guard). - Sentinel actor for scheduled runs: actor_email='system@paliad', actor_id=NULL — no phantom user in paliad.users. - Admin handlers POST /api/admin/backups/run + GET list/get/download behind adminGate(users, …); /admin/backups page + sidebar entry + bilingual i18n keys. - BackupRunner only wired when PALIAD_EXPORT_DIR is set; routes return 503 otherwise (same shape as requireDB). Tests: 8 pure-function tests cover registry shape (no dups, paliadin absent both as sheet name and SQL substring, ref__* sheets unscoped, every sheet has ORDER BY) and LocalDiskStore (round-trip, bad-key rejection, URI-traversal rejection, mkdir on construction). go build ./... + go test ./internal/... clean. bun run build clean. Slice B (BackupScheduler + retention cleanup) and Slice C (UI polish) are separate follow-ups per head's instruction. |
|||
| 045accc6d9 |
mAi: #89 - deadline rule field binary Auto/Custom + canonical rule-label display
t-paliad-258. m's verdict on t-paliad-251's rule UI: "too many options"
(4 'Oral hearings' across courts, etc.). Replace the full deadline_rules
catalog dropdown + sort selector with a binary model and unify the rule
display contract across every surface that prints a rule label.
Binary Rule field on the deadline form
- Auto (default): rule_id is derived from the chosen Type. The resolved
rule renders read-only as 'Auto | <Name · Citation>' next to the
field. No catalog picker, no sort options.
- Custom: free-text input. Stored as deadlines.custom_rule_text (new
nullable column, migration 122). Mutually exclusive with rule_id at
the persistence boundary.
- Toggle link flips between modes. Re-toggling to Auto re-resolves from
the current Type — no stale state.
Schema + service (additive)
- migration 122 adds paliad.deadlines.custom_rule_text (nullable).
Existing rows: empty custom_rule_text + non-null rule_id = Auto-
equivalent. Both NULL = "keine Regel" (consistent with today).
- models.Deadline.CustomRuleText + service SELECTs include the column.
- CreateDeadlineInput accepts custom_rule_text; the service drops it
when rule_id is set (catalog wins; simple invariant at the boundary).
- UpdateDeadlineInput grows a {RuleSet, RuleID, CustomRuleText} triple.
RuleSet=true is the discriminator so absent fields don't overwrite
the row (PATCH semantics). RuleID and CustomRuleText are mutually
exclusive in one request; service rejects "both set".
- EventListItem (the /api/events union) carries CustomRuleText so list
surfaces can render it.
Frontend: deadlines-new
- Drop the rule <select>, the by_proceeding/by_court/alpha sort
dropdown, the override-warning slot, and the collapsed-by-Regel Typ
view. Strip the (Rule→Type) auto-fill machinery — direction is now
one-way (Type → Auto-resolved Rule).
- Keep Type→Rule resolution: resolveAutoRuleForType picks the canonical
rule by project's proceeding, then jurisdiction match, then first
candidate. Same logic, just re-aimed at the read-only display.
- Standardtitel preserves the chain (event type → Auto rule label →
Custom text → proceeding → fallback) so the recipe still produces a
sensible title even when Custom is used.
Frontend: deadlines-detail
- Read-only display: catalog rule → Name · Citation, else
custom_rule_text + Custom badge, else legacy rule_code, else "—".
- Edit mode: mirror the create form with the Auto/Custom toggle.
enterEdit initialises the mode from the persisted deadline; Save
PATCHes with rule_set:true + the chosen rule pointer.
Rule-label addendum (m's 14:31 follow-up)
- Canonical contract everywhere: Name primary, Citation muted secondary
("Notice of Appeal · UPC.RoP.220.1"). Custom rules render the text
with a "Custom" pill.
- New frontend/src/client/rule-label.ts exports formatRuleLabel /
formatRuleLabelHTML / formatCustomRuleLabelHTML — one helper per
shape (plain text vs muted-citation HTML).
- Wired into: deadlines-new Auto display, deadlines-detail read +
Standardtitel, events.ts ruleDisplay (REGEL column on /events),
projects-detail.ts Fristen table, views/shape-list.ts generic
rule column.
- Verfahrensablauf (views/verfahrensablauf-core.ts) already renders
name + citation chip separately and matches the canonical pattern;
no change needed. Schriftsätze table is column-shaped (name + code
in distinct columns) and out of scope per the addendum.
CSS
- New .rule-mode-auto / .rule-mode-custom / .rule-label-* family.
- Drop the dead .rule-sort-select rule and the .event-type-collapsed*
family (retired with the catalog dropdown).
i18n
- DE+EN. Remove 10 stale keys (rule.none, autofill, autofill_inline,
mismatch, override, override_warn, sort.*). Add 6 (auto_no_match,
auto_pick_type, custom_badge, custom_placeholder,
mode.toggle_to_auto, mode.toggle_to_custom).
Build hygiene
- go build + go test ./internal/... clean.
- frontend bun build clean (2803 keys, scan clean).
Out of scope (per issue)
- Promoting Custom entries back to the catalog ("save as new rule").
- Filtering/searching custom_rule_text in deadline lists.
- Touching the event-type browse modal (Part 1 of #82 — that stays).
Files
- internal/db/migrations/122_deadlines_custom_rule_text.{up,down}.sql
- internal/models/models.go
- internal/services/deadline_service.go (Create+Update+SELECT)
- internal/services/event_service.go (union projection)
- frontend/src/client/rule-label.ts (new helper)
- frontend/src/client/deadlines-new.ts (rewrite)
- frontend/src/client/deadlines-detail.ts (Auto/Custom editor + display)
- frontend/src/client/events.ts (REGEL column)
- frontend/src/client/projects-detail.ts (Fristen table cell)
- frontend/src/client/views/shape-list.ts (generic rule column)
- frontend/src/client/i18n.ts + i18n-keys.ts (DE+EN delta)
- frontend/src/deadlines-new.tsx (strip dropdown+sort, add toggle)
- frontend/src/deadlines-detail.tsx (Auto/Custom edit slots)
- frontend/src/styles/global.css (rule-mode + rule-label families)
|
|||
| 02255c4234 |
mAi: #81 - verfahrensablauf side+appellant selectors + UPC Appeal trigger label
Concerns A + B + C from m/paliad#81:
A. Browse-a-proceeding (/tools/verfahrensablauf) gains a side selector
(Kläger/Beklagter/Beide) and an appellant selector. The side selector
swaps which column labels which user-side; the appellant selector
collapses party='both' rules into the appellant's column (no mirror)
so role-swap proceedings (Appeal, etc.) stop showing every row
twice in the timeline. Both selectors are URL-driven (?side= +
?appellant=) and re-render without a backend round-trip.
The appellant row hides itself for proceedings without an appellant
axis (first-instance Inf/Rev/Opp) via a small allowlist.
B. UPC Appeal trigger-event caption now reads "Anfechtbare Entscheidung"
/ "Appealable Decision" instead of falling back to the proceeding
name ("Berufungsverfahren" / "Appeal"). Implemented as an optional
trigger_event_label_{de,en} column on paliad.proceeding_types (mig
121); the frontend prefers it over the proceedingName fallback that
fires when no rule has IsRootEvent=true. No new deadline rules, no
slug changes (hard rule from the issue).
C. Parameter contract for the column projection is unified in
bucketDeadlinesIntoColumns(deadlines, {side, appellant}) — a pure
helper extracted from renderColumnsBody so the routing behaviour
stays unit-testable without a DOM. Tests cover the default mirror,
appellant-collapse for both sides, side-swap of column ownership,
the combined case, and row alignment by dueDate.
Verification
- go build ./... clean
- go test ./... all green
- bun run build (frontend) clean
- bun test (frontend/src) 110/110 pass (12 new + 98 prior)
- Migration 121 applied to paliad schema; UPC Appeal proceeding now
carries the curated trigger label pair.
Out of scope (filed for follow-up): per-rule role tagging so
respondent-side filings (Response to Appeal, Cross-Appeal) land in
the respondent's column when an appellant is selected. The current
issue scope (one-row-per-deadline collapse) is delivered; the
realistic-per-row routing needs a deadline_rules schema bump that
the hard rules of #81 excluded.
|
|||
| a911a2d0ee |
feat(submissions): t-paliad-243 — global Schriftsätze drafts without project
Adds an end-to-end project-optional path for Schriftsatz drafts:
- Migration 120 drops NOT NULL on paliad.submission_drafts.project_id
and rewrites the four RLS policies to gate purely on user_id when
project_id IS NULL, otherwise on paliad.can_see_project. Down
refuses to run if project-less rows exist (safer than silent
data corruption).
- SubmissionDraft.ProjectID becomes *uuid.UUID end-to-end. Service
layer skips project/parties/deadline lookups when nil and exposes
DraftPatch.ProjectID for the "Projekt zuweisen" affordance.
ListAllForUser LEFT JOINs paliad.projects so project-less drafts
surface in the global index next to project-scoped ones.
- New HTTP surface:
GET /submissions/new (picker page)
GET /submissions/draft/{draft_id} (editor for any draft)
GET /api/submissions/catalog (catalog without project)
POST /api/submission-drafts (project-less or attached)
GET/PATCH/DELETE /api/submission-drafts/{draft_id}
POST /api/submission-drafts/{draft_id}/export
Existing /api/projects/{id}/submissions/... routes remain bit-
identical so the project-scoped flow keeps working unchanged.
- Frontend: /submissions/new lists the full cross-proceeding catalog
grouped by proceeding, filterable by text + chip. Each row offers
"Ohne Projekt" (instant draft) or "Mit Projekt…" (modal picker
with autocomplete over visible projects). /submissions index gains
a prominent "Neuer Entwurf" CTA and an empty-state CTA pointing at
the picker. The editor renders a banner + "Projekt zuweisen"
action when project_id is null; assigning persists project_id and
redirects to the project-scoped URL.
Audit + project-event writes detect d.ProjectID == nil; the audit
row's scope flips to 'user' (scope_root = user_id) and the
project_events row is skipped entirely.
|
|||
| d3aade5aac |
feat(submissions): t-paliad-238 Slice A — dedicated draft editor page
Adds the dedicated Submissions/Schriftsätze editor at
/projects/{id}/submissions/{code}/draft (and …/draft/{draft_id}) per
docs/design-submission-page-2026-05-22.md.
Lawyer picks (or creates) a named draft, edits placeholder variables
in a sticky sidebar, sees a read-only HTML preview of the merged
document body, and exports a .docx with project state + lawyer
overrides resolved. Drafts persist in paliad.submission_drafts
keyed on (project_id, submission_code, user_id, name) with RLS via
can_see_project; updates and deletes additionally gated on owner-only
(Q-E4 owner-scoped pick, m-confirmed).
Resurrected from git history per the design's "no rewrite" plan:
SubmissionVarsService ← commit
|
|||
| cdd27d674e |
feat(paliadin): stream + honest late-recovery (t-paliad-235)
m's 14:56 observation: long Paliadin turns showed "Verbindung verloren —
Antwort wird nachgereicht …" but never delivered. The aichat backend
finished the turn upstream; paliad's HTTP client had given up at 130 s
and the legacy filesystem janitor never ran for the aichat path.
Three intertwined fixes, all shipped together because they share the
same wire shape and the same UI states:
1. Switch the aichat backend to /chat/turn/stream
- new AichatPaliadinService.RunTurnStream relays incremental chunks
- SSE parser handles default `data:` frames (chunk/meta/done/error)
and named `event: heartbeat` frames per the upstream contract
- no more 130 s hard ceiling — stream stays open as long as data or
heartbeats flow; silenceTimeout (90 s) catches a true upstream
stall instead
2. Proof-of-life thinking events
- handler emits `event: thinking` every 5 s while the upstream is
silent (synthesised locally) AND relays aichat's `heartbeat`
events as thinking pings
- frontend renders a lime-dot pulse + monospace counter inside the
assistant bubble — the user can SEE the chat is still working
3. Honest disconnect copy + real late-recovery
- new dispatching endpoint GET /api/paliadin/turns/{id}/recover
- aichat backend: asks aichat via GET /chat/conversations and
/chat/conversations/{id}/turns whether the turn actually finished
- legacy backend: falls through to the local row read (janitor)
- frontend swaps "wird nachgereicht" → "Lade frische Antwort …"
while the recovery polls; on confirmed "lost" swaps to
"Antwort konnte nicht zugestellt werden — bitte erneut stellen"
- migration 118 adds aichat_conversation_id to paliadin_turns so
the recovery has a fast path when the done frame arrived before
the drop
Streaming + recovery are a no-op for PALIADIN_BACKEND=legacy: the
StreamingPaliadin interface is detected via type assertion, the
LocalPaliadinService stays on the one-shot RunTurn + filesystem
janitor path.
13 new unit tests cover the SSE parser, the conversation-API client,
and the match-assistant-response helper.
go build ./... + go test ./internal/... + go test ./cmd/server/...
+ bun run build all clean.
|
|||
| 6b565be830 |
feat(dashboard): t-paliad-219 Slice C — catalog expansion + firm-wide admin default
Three additions on top of Slice B's edit-mode chrome. **Catalog expansion (2 new widgets, default-hidden — opt-in via picker):** - pinned-projects: surfaces a list of the user's pinned matters via the pre-existing PinService (mig 062/063, pre-dates t-paliad-219). New DashboardService.loadPinnedProjects joins paliad.user_pinned_projects to paliad.projects under the standard visibility predicate, preserves pinned-at-DESC order, capped at PinnedProjectsCap=20. PinnedProjects []PinnedProjectRef grows DashboardData; SetPinService wired post-construction to mirror the SetApprovalService pattern. - quick-actions: pure UI affordance with three buttons linking to the existing /projects/new, /deadlines/new, /appointments/new routes. No backend payload, no settings schema. Both default-hidden — m's brief asked for "high-value adds"; injecting new widgets into every user's dashboard unannounced would be loud. Factory test relaxed: visibility now matches catalog.DefaultVisible instead of the previous "all-visible" invariant. **Firm-wide admin default (mig 117 + new service + 4 endpoints):** - paliad.firm_dashboard_default: single-row table (id smallint PK CHECK id=1) with layout_json + updated_by + updated_at. RLS: SELECT authenticated, no INSERT/UPDATE policy (writes go through the service-role connection behind the adminGate). - FirmDashboardDefaultService Get/Set/Clear. Validates against the catalog on Set so an admin can't seed an invalid layout. - DashboardLayoutService.SetFirmDefaultService wires in the firm source. Both GetOrSeed and ResetToDefault now prefer the firm default over the code-resident FactoryDefaultLayout when one is set. Nil-safe — empty firm row falls back to the factory layout, transient DB errors fall back too (a blip can't strand a user without a dashboard). - HTTP: GET / PUT / DELETE /api/admin/firm-dashboard-default (admin- gated). POST /api/me/dashboard-layout/promote: admin convenience — reads the admin's own current layout and stashes it as the firm default (saves the JSON-editor step; admins edit via /dashboard's normal editor, then click Promote). **Frontend (Slice B's edit-mode footer grew an admin button):** - "Als Firmen-Standard speichern" button in the edit footer; hidden via CSS-inline until syncPromoteButtonVisibility unhides for global_admin. Confirm() → POST /promote → toast. - The existing "Auf Standard zurücksetzen" copy stays the same — the semantics now "firm default if set, else factory", which is the desired surface: users see one canonical "Standard" link. i18n: 13 new keys × DE+EN (dashboard.pinned.*, dashboard.quick.*, dashboard.edit.promote*). i18n-keys.ts regenerated by build. m/paliad#46. go build ./... clean; go vet ./... clean go test ./internal/... clean (Slice C catalog test + factory-default test relaxation; FirmDashboardDefault round-trip tests gated on TEST_DATABASE_URL) Migration 117 dry-run: PASS (other dry-run failures are pre-existing local-DB collisions on origin/main; mig 117 itself clean) bun run build clean: dashboard.html carries new section markup + admin button; dashboard.js bundles renderPinnedProjects + promote handler + all new i18n keys |
|||
| fffddcc71a |
feat(checklists): t-paliad-225 Slice C backend — template versioning + catalog Version
m/paliad#61 Slice C backend. Schema (mig 116, idempotent): - ALTER paliad.checklists ADD COLUMN version int NOT NULL DEFAULT 1. Pre-Slice-C rows default to 1 (the column was added with DEFAULT so the UPDATE clause is a no-op safety net). - ALTER paliad.checklist_instances ADD COLUMN template_version int. NULL on existing rows — instance detail page leaves the "outdated" badge off when the snapshot version is unknown. Services: - ChecklistTemplateService.Update — version bumps on title/body changes (the meaningful edits that warrant notifying instance owners). Pure metadata tweaks (description/court/reference/deadline) update updated_at without bumping. Emits the new 'checklist.versioned' audit event with prior_version + new_version metadata. - ChecklistInstanceService.Create — captures snapshot_version alongside the body snapshot. - ChecklistCatalogService — CatalogEntry grew a Version field (1 for static; live column for authored). ListVisible / Find populate it. - Models — Checklist.Version int; ChecklistInstance.TemplateVersion *int. - /api/checklists/{slug} response now includes version so the instance detail page can compare against the snapshot. Migration verified live via BEGIN..ROLLBACK against paliad.checklists and paliad.checklist_instances. Build hygiene: go build/vet/test ./internal/... + TestBootSmoke ./cmd/server/ all green. |
|||
| c3cd51eb85 |
feat(checklists): t-paliad-225 Slice B backend — explicit sharing + admin promotion
m/paliad#61 Slice B backend. Implements the explicit-share path
(checklist_shares + visibility predicate extension) and the
global_admin-only promotion / demotion of authored templates to and
from the firm catalog.
Schema (mig 115, idempotent):
- paliad.checklist_shares (uuid id, checklist_id FK, polymorphic
recipient via xor-check: recipient_kind in {user, office,
partner_unit, project} with exactly one matching recipient_* column
populated; granted_by FK; granted_at)
- Hot-path lookup index + per-kind partial UNIQUE indexes prevent
duplicate grants
- RLS: SELECT owner OR self-recipient (user-kind) OR global_admin;
INSERT owner-only with granted_by=self; DELETE owner OR global_admin;
no UPDATE (revoke = DELETE)
- can_see_checklist CREATE OR REPLACE — adds 4 share branches; project-
share branch uses inline ltree walk over projects.path because
can_see_project reads auth.uid() (NULL on service-role connection,
same pattern as visibility.go)
- xor-check verified live: rejects kind='user' with recipient_office
set; accepts the matching kind/recipient pair
Services:
- ChecklistShareService — Grant (owner-only, validates recipient kind +
required FK target, friendly 409 on partial-unique-index conflict),
Revoke (owner or global_admin), ListGrants (owner or global_admin;
enriches recipient_label via LEFT JOINs)
- ChecklistPromotionService — Promote (global_admin → visibility=global
+ promoted_at/by + audit), Demote (global_admin → target visibility,
default 'firm', clears promoted_at/by; rejects demote of non-global
rows)
- ChecklistCatalogService.checklistVisibilityPredicate extended to
include all 5 share branches; service-role-friendly (no auth.uid())
- ChecklistTemplateService.normaliseSliceAVisibility now accepts
'shared' as an author-set value; 'global' stays admin-only
Endpoints:
- GET /api/checklists/templates/{slug}/shares — list grants (owner/admin)
- POST /api/checklists/templates/{slug}/shares — grant
- DELETE /api/checklists/shares/{id} — revoke
- POST /api/admin/checklists/{slug}/promote — promote to global
- POST /api/admin/checklists/{slug}/demote — demote (body.target default 'firm')
Audit (paliad.system_audit_log):
- checklist.shared — recipient_kind + recipient_id in metadata
- checklist.unshared — same shape, captured pre-DELETE
- checklist.promoted_global — prior_visibility + owner_id
- checklist.demoted — target_visibility
Tests: validateShareInput covers all 4 kinds (happy + missing-id);
predicate-shape test asserts all 6 visibility branches present;
pqUniqueViolation regex sniff; nullableString helper; SliceB visibility
opens 'shared' but keeps 'global' admin-only.
Hotfix-merge note: head shipped
|
|||
| a4e2f3526d |
feat(checklists): t-paliad-225 Slice A backend — user-authored templates
m/paliad#61 Slice A. Introduces paliad.checklists (mig 114) as the DB-backed companion to the static Go catalog. ChecklistCatalogService unifies both sources at read time; ChecklistTemplateService handles authoring CRUD + visibility toggle (private↔firm; Slice B opens 'shared' and 'global'). Schema (mig 114, idempotent): - paliad.checklists (uuid, slug UNIQUE, owner_id FK, title/description /regime/court/reference/deadline/lang, body jsonb, visibility CHECK ('private','shared','firm','global'), promoted_at/_by, timestamps) - paliad.can_see_checklist(uuid, uuid) STABLE SECURITY DEFINER — owner OR firm/global. Slice B extends with the explicit-share branch. - RLS: select via can_see_checklist; insert owner=self; update/delete owner OR global_admin - ALTER paliad.checklist_instances ADD COLUMN template_snapshot jsonb (snapshot semantics so per-Akte instances stay decoupled from subsequent template edits) Services: - ChecklistCatalogService — ListVisible, Find, SnapshotBody, IsStaticSlug. Reapplies visibility application-side (service-role bypasses RLS, per visibility.go pattern). Static-slug map computed once at boot for collision detection. - ChecklistTemplateService — Create (auto-generates u-<slug>-<hex> with retry), Update (changed_fields[] in audit), SetVisibility, Delete, ListOwnedBy, GetBySlug. Owner-or-global_admin gate. - SystemAuditLogService.WriteChecklistEvent — thin helper writing into paliad.system_audit_log with scope='org'. - ChecklistInstanceService.Create now captures template_snapshot via the catalog; GetByID returns it inline so the frontend can render the captured body even after the upstream template is mutated. Endpoints (all owner-gated where mutating): - GET /api/checklists — merged catalog (static + DB visible) - GET /api/checklists/{slug} — single template; static-first lookup - GET /api/checklists/templates/mine — caller's authored templates - POST /api/checklists/templates — create - PATCH /api/checklists/templates/{slug} — edit - PATCH /api/checklists/templates/{slug}/visibility — private↔firm - DELETE /api/checklists/templates/{slug} — delete - GET /checklists/new, /checklists/{slug}/edit — author wizard pages Tests: pure-helper unit tests cover slugifyTitle (umlaut → ae/oe/ue/ss normalisation + clamp), regime/lang/visibility validation, body-shape enforcement, static-slug detection, predicate shape, clamp. |
|||
| ea0715a8c7 |
feat(projects): t-paliad-222 — Client Role + auto-derived project codes
Implements m/paliad#47 (Client Role rework) + m/paliad#50 (auto-derived project codes from the ancestor tree) in one shift. Migrations: - mig 112_client_role_rework: widen paliad.projects.our_side CHECK to seven sub-roles (claimant / defendant / applicant / appellant / respondent / third_party / other); drop legacy 'court' / 'both' and backfill rows to NULL (no-op on prod, defensive on staging). - mig 113_projects_opponent_code: add paliad.projects.opponent_code text on litigation rows (slug pattern [A-Z0-9-]{1,16}); used as the middle segment when assembling auto-derived project codes. Backend: - internal/services/project_code.go — new package-level helpers BuildProjectCode (single row) + PopulateProjectCodes (bulk, one CTE-based round-trip). Walks the existing paliad.projects.path ltree; custom paliad.projects.reference on the target wins. - Wired into ProjectService.List, GetByID, ListAncestors, GetTree, LoadCounterclaimChildrenVisible, BuildTreeWithOptions — every service entry-point that returns []models.Project / *models.Project populates .Code before returning. - Models: Project.OurSide doc widened; new Project.OpponentCode (db:"opponent_code") and Project.Code (db:"-", projection-only). - CreateProjectInput / UpdateProjectInput accept OpponentCode; validateOpponentCode + nullableOpponentCode mirror our_side helpers. - validateOurSide widens to the seven sub-roles; legacy 'court' / 'both' rejected at the service layer with a clear error before the DB CHECK fires. - derivedCounterclaimOurSide CCR flip widened: applicant ↔ respondent, appellant → respondent; third_party / other / NULL pass through. - submission_vars: project.code added to the placeholder bag. ourSideDE / ourSideEN now use the gender-neutral "-Seite" / "-Partei" suffix shape (Klägerseite / Antragstellerseite / ...); better legal-prose default for a B2B patent practice, matches the form labels which already used this shape (cf. head's soft-note on Q4). Frontend: - ProjectFormFields: opponent_code on a new projekt-fields-litigation block (hidden by default, shown when type=litigation); our_side moved into projekt-fields-case and re-labelled "Client Role" / "Mandantenrolle" with three <optgroup>s + seven options. - project-form.ts: showFieldsForType toggles the new litigation block; readPayload / prefillForm wire opponent_code; our_side is now only emitted for type=case. - fristenrechner: ourSideToPerspective widened to the seven sub-roles (Active→claimant, Reactive→defendant, Other→null). ProjectOption type literal updated. - i18n.ts: new projects.field.client_role.* and projects.field.opponent_code.* keys (DE+EN). Legacy projects.field.our_side.* keys stay one release for cached bundles + Verlauf event-history rendering of the new sub-roles. Tests: - TestProjectCodeSegment, TestAssembleProjectCode, TestPatentLast3, TestSanitizeClientShort, TestProceedingTail, TestValidateOpponentCode, TestValidateOurSideSubRoles pin the new pure helpers. - TestOurSideTranslations widened to the seven sub-roles + new prose shape; 'court'/'both' arms now return "" (legacy rejected). - TestDerivedCounterclaimOurSide widened to the new flip map. Migration slot history (this branch was rebumped twice on 2026-05-20): mig 110 was claimed by m/paliad#51 (project_type_other, euler); mig 111 was claimed by m/paliad#48 (project_admin_and_select, gauss). Final slots 112 / 113. go build && go test ./internal/... && cd frontend && bun run build all clean. |
|||
| 3fdc969902 | wip(projects): bump migrations 110→111, 111→112 (euler claimed 110) | |||
| 5dea0a703b |
wip(projects): t-paliad-222 — backend + frontend changes (pre-merge checkpoint)
Backend: mig 110/111 (will be renumbered after merging main), validators + helpers widened, BuildProjectCode helper + projection populator wired into List/GetByID/ListAncestors/GetTree/CCR. All internal Go tests pass. Frontend: ProjectFormFields conditional render — opponent_code on litigation, our_side renamed to Client Role on case with grouped optgroups. i18n keys for both DE and EN. fristenrechner perspective mapping widened. project-form.ts payload reader/writer + showFieldsForType toggle for new litigation block. Migration slots about to be bumped (mig 110 was claimed by euler's project_type_other on main). |
|||
| 2ed0ef3177 |
feat(team-admin): t-paliad-223 Slice A — Project Admin role + inheritable role-edit gate
#48 — adds 'admin' as fifth project_teams.responsibility value, plumbs an inheritable role-edit gate via the materialised ltree path. - migration 110: ALTER responsibility CHECK, CREATE paliad.effective_project_admin(uuid,uuid) STABLE SECURITY DEFINER (mirrors can_see_project shape), REPLACE project_teams_update / _insert / _delete RLS policies. Idempotent + down-mig provided. Dry-run BEGIN..ROLLBACK clean on live supabase. - services/approval_levels.go: ResponsibilityAdmin const + IsValidResponsibility extension. responsibilityOpensGate UNCHANGED — admin is orthogonal to the 4-Augen approval gate. - services/team_service.go: ChangeResponsibility() with last-admin guard inside tx (counts admins on project + ancestor chain, excludes the row being changed). RemoveMember() also runs the guard when removing an admin row. New IsEffectiveProjectAdmin() driving the frontend affordance. legacyRoleFromResponsibility: admin → 'lead' (deprecated shadow column). - services/project_service.go: ErrLastProjectAdmin sentinel mapped to 409 in writeServiceError. - handlers/teams.go: new PATCH /api/projects/{id}/team/{user_id}. RLS-enforced; non-admins get 404 to avoid existence leakage. - handlers/projects.go: GET /api/projects/{id} now wraps the payload with effective_admin bool so the frontend drives the inline-select affordance without a second round-trip. - frontend/src/projects-detail.tsx + client/projects-detail.ts: admin appears as 5th option in 'Mitglied hinzufügen' dropdown. Team-list Rolle cell switches to an inline <select> for callers with effective_admin (read-only span otherwise). Optimistic PATCH with rollback on error (last-admin guard / 403 from RLS / etc.) surfaced as transient toast in #team-msg. - i18n: +6 keys (admin label + admin.hint + 3 error toasts × 2 langs). - tests: TestIsValidResponsibility now covers admin; new TestLegacyRoleFromResponsibility pins the mapping table. go build && go test -short ./internal/... && bun run build all clean. |
|||
| dc5f11ddef |
feat(projects): add 'other' as a real type; drop synthetic Empty filter
m/paliad#51 (t-paliad-221) — the type chip filter on /projects used to treat unclassified projects as a synthetic "Empty" bucket. Make 'other' a first-class projects.type value so every row carries a meaningful label and the filter UI stops needing a NULL/Empty shim. - mig 110: extend projects.type CHECK to include 'other'; backfill any NULL rows defensively (production query confirmed zero, but the NOT NULL constraint isn't load-bearing once the IN-list changes). - Go: add ProjectTypeOther constant; isValidProjectType + humanProjectType recognise it; handler doc lists 'other' in the ?type whitelist. - Frontend: new chip in the projects.tsx type filter, new option in the Create-Project form, DE "Sonstiges" / EN "Other" labels for the projects.type and projects.chip.type i18n families. Also drops a stray data-i18n-text attribute on the existing 'project' chip checkbox (it had no consumer in i18n.ts and the surrounding markup was nesting a <span> inside an <input>). |
|||
| a421bff856 |
feat(dashboard): t-paliad-219 Slice A1 — user_dashboard_layouts storage + service
Migration 109 + DashboardLayoutSpec + Service + WidgetCatalog. No HTTP
handlers and no frontend yet — those land in A2/A3/A4 as separate commits
for cleaner review.
Why slot 109 (not 107 from the design doc): leibniz claimed 107 for
caldav_sync_log.binding_id and 108 for caldav_mkcalendar_capability after
the design was filed. Boltzmann's gap-tolerant runner (
|
|||
| fbd087e0cd |
feat(caldav): Slice 2c MKCALENDAR + Google-degrade (t-paliad-212)
Final Slice 2 sub-slice: users on iCloud / Fastmail / Nextcloud /
Radicale / Baikal / SOGo can now create a brand-new calendar from the
Paliad UI with one click; users on Google CalDAV (and any future
no-MKCALENDAR provider) get a clean degrade UX that nudges them to
create the calendar in their provider's app and paste the URL back.
Per m's Q2 pick, the capability lives on user_caldav_config so the
probe runs once per server change, not per modal open.
Schema (mig 108)
- paliad.user_caldav_config.supports_mkcalendar boolean — NULL =
unprobed, TRUE = supported, FALSE = degrade.
- paliad.user_caldav_config.mkcalendar_probed_at timestamptz — used
by the next round of probes after SaveConfig invalidates.
- Idempotent (information_schema column-exists checks) + assertion.
CalDAV client
- ProbeMKCalendar: OPTIONS Allow header first; on absence of
MKCALENDAR, falls back to a synthetic MKCALENDAR against a
random .paliad-probe-XX/ path (with DELETE cleanup) to catch
legacy SOGo / misconfigured Radicale (design §4.2).
- MakeCalendar: issues MKCALENDAR with displayname + VEVENT-only
supported-components; returns ErrCalendarNameTaken on 405 so
the service layer can retry with a disambiguating suffix.
- Sentinel errors ErrCalendarNameTaken, ErrMKCalendarUnsupported.
Service
- CalDAVService.ensureMKCalendarProbed: lazy probe on first
/api/caldav-discover call after credential change; result persisted
via UPDATE on user_caldav_config. DiscoverCalendars response now
carries supports_mkcalendar so the UI can show / hide the create-new
radio.
- CalDAVService.MakeCalendar: re-probes if needed, issues MKCALENDAR
via the client (with 3-try -XX-suffix retry on name collision),
creates the matching binding, kicks off PushBindingNow. Returns
the partial result on push failure so the UI can show "created but
initial sync failed".
- InvalidateDiscoveryCache now also clears supports_mkcalendar so a
re-configured server gets re-probed on next open.
HTTP API
- POST /api/caldav-mkcalendar — {display_name, scope_kind, scope_id?,
include_personal?} → 201 {calendar_path, binding, initial_pushed}.
Errors: 501 supports_mkcalendar=false, 409 name conflict, 5xx
upstream. Partial-success (binding created, push failed) carries
initial_sync_error in the body so the UI can surface both bits.
Frontend
- Add-modal source picker becomes a 3-way radio: "Existierenden
wählen" / "Neuen Kalender erstellen" / "Eigene URL eingeben".
Create radio is visible only when supports_mkcalendar=true;
when false, the bilingual Google-degrade notice is shown
beneath the source picker.
- Submit dispatches to /api/caldav-mkcalendar (create) or
/api/caldav-bindings (existing / custom).
- 6 new i18n keys DE+EN under caldav.bindings.modal.source.*
+ caldav.bindings.error.create_*.
Verification
- mig 108 dry-run against live Supabase: both columns added, nullable,
no constraint surprise.
- go build ./... + go test ./internal/services/ ./internal/handlers/ +
bun run build all clean.
Slice 2 complete (2a + 2b + 2c). Slice 3 (hierarchy scopes:
client/litigation/patent/case) and Slice 4 (drop legacy scalar
caldav_uid/caldav_etag) remain.
|
|||
| 694c7a53ad |
feat(caldav): Slice 2a backend cut-over — bindings-driven sync (t-paliad-212)
Cuts the CalDAVService sync engine over from the Phase F scalar calendar_path to the binding-row model introduced in Slice 1 (mig 101). Invisible-but-shippable: existing Phase F users keep their backfilled all_visible binding, new users hitting the legacy PUT /api/caldav-config get an auto-created all_visible binding so the "configure → it just works" UX survives. Slice 2b adds the picker UI and write APIs on top. Schema (mig 107) - paliad.caldav_sync_log.binding_id (nullable, FK ON DELETE SET NULL so audit history survives binding deletes). - Per-binding index for the read path. - Idempotent (column-exists DO block) + assertion. Services - CalendarBindingService: ListForUser, ListEnabled, ListAllEnabled, Get, Create, Update, Delete, SetSyncStatus. Mirrors the table CHECK constraints client-side so the API returns useful 400s. - AppointmentTargetService: UpsertAfterPush, FindByUIDAndBinding, ListForBinding, DeleteByAppointmentAndBinding, StaleForBinding. Replaces SetCalDAVMeta as the authoritative source of per-target state; legacy scalar columns still written for back-compat. - AppointmentService.ForBinding: scope filter implementing all_visible, personal_only, project. Hierarchy scopes (client/litigation/patent/case) return ErrUnsupportedScope — Slice 3 wires them via the existing path-based descendant predicate. Sync engine rewrite - CalDAVService.Start iterates ListAllEnabled to discover users with at least one enabled binding. - runSyncOnce loops bindings, writes one caldav_sync_log row per (user, binding) tick, rolls the worst-case error up onto user_caldav_config.last_sync_error so /api/caldav-config still shows aggregate status. - pushBinding pushes the ForBinding() slice + cleans up stale-target rows (project unshared, scope PATCHed). - pullBinding swaps the N×GET pattern for REPORT calendar-multiget (RFC 4791 §7.9; chunked at 100 hrefs to stay inside provider rate limits) and reconciles via per-target etag comparison. - Hooks (OnAppointmentCreated/Updated/Deleted) fan out across the user's matching bindings using appointmentInBinding() — best effort per binding, same 30s timeout as Phase F. - SaveConfig auto-creates an all_visible binding on first-time configure so Phase F "configure → events appear" survives the cut-over. CalDAV client - New ReportMultiget verb implementing RFC 4791 §7.9 calendar-multiget. Chunked at multigetMaxHrefs=100 to fit Google Calendar's per-request cap. HTTP API - GET /api/caldav-bindings — read-only list of the authenticated user's bindings. Slice 2b adds POST/PATCH/DELETE. Verification - BEGIN..ROLLBACK against live Supabase (PG 15.8): mig 107 applies cleanly + the synthetic two-binding scenario lands the project appointment in both bindings while keeping the personal one in master only; cascade on appointment-delete drops targets; cascade on binding-delete drops targets AND sets sync_log.binding_id NULL. - go build ./..., go test ./internal/..., bun run build all clean. Backwards-compat - paliad.appointments.caldav_uid / caldav_etag still written in pushBinding so legacy readers see fresh values. Slice 4 drops them after telemetry confirms no path still reads them. |
|||
| 7a359989a9 |
feat(db): t-paliad-218 — gap-tolerant migration runner with applied-set tracker
Replaces the golang-migrate single-counter tracker with a hand-rolled runner over embed.FS that tracks applied state as a set in paliad.applied_migrations (version PK, name, applied_at, checksum). Closes the parallel-merge skip-hole the 2026-05-20 mig-103 incident exposed (m/paliad#44): a migration whose version is missing from applied_migrations runs on the next deploy regardless of which higher versions are already applied. Gaps are first-class. Slice 1 of the design at docs/design-migration-runner-applied-set-2026-05-20.md. All eight design decisions m-picked = inventor recommendation. Runner contract: - Ensure paliad schema → pg_advisory_lock(hash('paliad.applied_migrations')) → CREATE TABLE IF NOT EXISTS applied_migrations. - bootstrapFromLegacyTracker: if applied_migrations is empty and the legacy paliad.paliad_schema_migrations row is present and clean, INSERT rows 1..N for every on-disk version with checksum=NULL via ON CONFLICT DO NOTHING. Hard-fail if legacy tracker is dirty (operator must recover). - scanEmbeddedMigrations: hard-fail on two .up.sql files sharing a version prefix — the failure mode the post-mortem exposed. - checkNameAgreement: hard-fail on rename-after-apply mismatch (disk name for an already-applied version != DB name). - applyOne: SQL body + INSERT(version, name, now(), sha256(file_bytes)) in one transaction. All-or-nothing per migration. Checksums populated on apply for future drift detection; rows backfilled from the legacy tracker carry NULL (we can't fabricate a hash for what golang-migrate applied historically). Verify-on-deploy intentionally deferred to a focused follow-up — single if-block flip when m wants it. Up-only runner. .down.sql files stay in embed.FS as reference; manual roll-back path is psql + DELETE FROM paliad.applied_migrations WHERE version=N. Zero call sites for migrate.Down in the codebase today. Drops github.com/golang-migrate/migrate/v4 from go.mod (no other importers; verified via grep). Tests: - internal/db/migrate_test.go: TestMigrations_DryRun walks pending = on_disk \\ applied (read from paliad.applied_migrations, missing-table → empty set), runs each in BEGIN/ROLLBACK against the scratch DB. - cmd/server/main_smoke_test.go: TestBootSmoke asserts the applied set equals the on-disk set exactly (not just max-version-match) — catches the skip class the post-mortem documented. Dirty-flag check removed (rows are committed or absent, not 'dirty'). - All 45 service-test call sites of db.ApplyMigrations work unchanged (same signature, same fresh-DB behavior). Follow-up: mig 108_drop_legacy_trackers (DROP paliad.paliad_schema_migrations and public.paliad_schema_migrations) after one or two deploys of burn-in on this slice. |
|||
| 6401a8198d |
feat(offices): add Madrid as a firm office (mig 106)
m's ask 2026-05-20 09:42. Eighth HLC office alongside Munich, Düsseldorf, Hamburg, Amsterdam, London, Paris, Milan. - `internal/offices/offices.go` — append Madrid to All[] (display order: end of list, after Milan). Doc comment refreshed to point at the actual current CHECK constraints (users mig 002 + partner_units mig 018/024/027), not the obsolete akten reference from before projects-v2. - `internal/offices/offices_test.go` — add `madrid` to the valid-keys table. - mig 106 — extend the two CHECK constraints on users.office and partner_units.office. Idempotent (DROP IF EXISTS), audit_reason set_config at top, dry-run validated against the live youpc paliad schema (BEGIN; ALTER...; ROLLBACK). Frontend picks up Madrid automatically via GET /api/offices. Admin UI for managing firm office list is a separate longer-term issue — m's "for now, just add Madrid already" path. |
|||
| d8acbd613c |
feat(approvals): t-paliad-216 mig 103 — suggest-changes schema
Adds the schema scaffolding for the fourth approval action (alongside
Approve / Reject / Revoke):
1. Extends approval_requests.status CHECK to include 'changes_requested'.
2. Adds counter_payload jsonb — the approver's edited values on a
changes_requested row (the basis of the new row's payload).
3. Adds previous_request_id uuid FK — back-pointer from a SuggestChanges-
spawned row to its source. Partial index on the FK supports chain
traversal.
Non-blocking: extending a CHECK constraint is metadata-only on Postgres;
adding NULLable columns + a NULLable FK is metadata-only. Safe for live
deploy.
Dry-run validated against the live youpc paliad schema via BEGIN/ROLLBACK
(migration tracker at 102 pre-apply; schema unchanged post-rollback).
|
|||
| d127c768f7 |
feat(t-paliad-207): mig 105 — track-aware sequence reshuffle for upc.inf.cfi (infringement → revocation → amendment)
m's ask 2026-05-18 18:08: 'the infringement parts (like Replik) should show above the part for the revocation (Erwiderung Nichtigkeitswider- klage)'. Three tracks (infringement / revocation / amendment) coexist on upc.inf.cfi once with_ccr / with_amend are set. They share tied calendar dates because R.29/R.30/R.32 all key off the SoD or its descendants. Current sequence_orders (post-mig 100) interleave them arbitrarily; user sees Erwiderung-zur-CCR before Replik even though Replik is the infringement-side response to the same triggering event. **Re-sequencing** keeps the existing soc=0, prelim=5, sod=10 head and the interim=40 / oral=50 / decision=60 / cost_app=70 / appeal_spawn=80 tail untouched. The 10 reshuffled rules move into a track-aware arrangement: 10-19 infringement: sod=10, reply=12, rejoin=14 20-29 revocation: ccr=20, def_to_ccr=22, reply_def_ccr=24, rejoin_reply_ccr=26 30-39 amendment: app_to_amend=30, def_to_amend=32, reply_def_amd=34, rejoin_amd=36 Tied-date ordering after the reshuffle: D+3mo: sod(10), ccr(20) — SoD then its CCR D+5mo: reply(12), def_to_ccr(22), app_to_amend(30) — inf → rev → amd D+7mo: reply_def_ccr(24), def_to_amend(32) — rev → amd D+8mo: rejoin_reply_ccr(26), reply_def_amd(34) — rev → amd **Two-phase swap** — every reshuffled rule first parks at sequence 1000+number, then jumps to its final value. Prevents transient sequence-collisions if Postgres evaluates UPDATEs in parallel within the same statement. Each UPDATE is keyed by submission_code AND the SOURCE sequence_order, so re-apply is a no-op. audit_reason set_config at top per mig 099 hotfix pattern. Renumbered from mig 102 → mig 105 to avoid collision with archimedes system_audit_log mig 102 (merged between fermi's parked session and now); follows mig 104 (Einspruch name + CCR priority). |
|||
| dab06e068f |
fix(t-paliad-207): mig 104 — strip rule cite from Einspruch names + flip CCR priority informational→optional
Two corrections to mig 100's merged-state: 1. **CCR priority informational → optional**. m's correction 2026-05-18 18:01. The fermi amend ( |
|||
| 28c7215458 |
feat(export): t-paliad-214 Slice 1 backend — personal sync export endpoint + xlsx/json/csv writer
Adds GET /api/me/export streaming a deterministic .zip bundle of the caller's RLS-visible projection (per design §2.3): projects, deadlines, appointments, parties, notes, documents (metadata), audit events, approval requests, checklist instances + personal sidecars (me row, caldav config without ciphertext, views, pins, card layouts, paliadin turns) + reference data (proceeding_types, event_types, deadline_rules, courts, countries, holidays …) + restricted users_referenced sheet. Bundle shape: paliad-export.xlsx + paliad-export.json + per-sheet CSVs (UTF-8 BOM, RFC 4180) + README.txt + __meta.json. Outer zip is byte-deterministic — sorted file list, fixed Modified time on every entry, sorted JSON keys. Two runs at same row-state → identical bytes. ExportService.WritePersonal owns the SQL recipe + column discovery + PII deny-regex (?i)secret|token|password|api[_-]?key|private[_-]?key + per-sheet DropColumns belt-and-braces (e.g. user_caldav_config .password_encrypted explicitly dropped on top of the regex). Audit row written to paliad.system_audit_log before the run, patched with row_counts + file_size_bytes after. Migration 102 creates paliad.system_audit_log (generic event_type + actor_id/email + scope + scope_root + metadata jsonb). Idempotent CREATE TABLE IF NOT EXISTS + indexes; RLS enabled with self-read + admin-read policies. AuditService.ListEntries gains a 6th UNION branch so the new table surfaces on /admin/audit-log. excelize/v2 added to go.mod for xlsx generation. Pure-function tests pin formatCellValue value-coercion, PII regex, CSV quoting + BOM + umlaut survival, JSON shape, meta key order stability, filename slugify, and byte-determinism of the bundle assembly. Design: docs/design-paliad-data-export-2026-05-19.md §7 Slice 1. |
|||
| 8a43aed100 |
feat(caldav): mig 101 — multi-calendar binding schema + backfill (t-paliad-212 Slice 1)
Schema-only landing for Slice 1 of the CalDAV multi-calendar design
(docs/design-caldav-multi-calendar-2026-05-19.md). Sync engine NOT
touched — Slice 2 wires the per-binding fan-out. After this migration:
- paliad.user_calendar_bindings — N bindings per user with scope_kind
∈ {all_visible, personal_only, project, client, litigation, patent,
case}. Hierarchy scopes anchor scope_id at paliad.projects(id).
Partial unique indexes enforce one binding per (user, scope_kind,
scope_id) for hierarchical scopes and one per (user, scope_kind)
for the scope-less roots. RLS mirrors user_caldav_config.
- paliad.appointment_caldav_targets — per-(appointment, binding) join
carrying caldav_uid + caldav_etag. UID stays canonical per
appointment so the same event in N cals shares one UID.
- Backfill — one all_visible binding per existing user_caldav_config
row, one target row per appointment already pushed. Maps target to
the creator's binding, matching today's Phase F semantics where the
creator's goroutine owns the etag.
Legacy paliad.appointments.caldav_uid / caldav_etag columns are
untouched (kept as denormalised pointers through Slice 1+2; dropped
in Slice 4 after telemetry).
Dry-run verified against live Supabase (PG 15.8): synthetic config +
appointment backfill creates exactly 1 binding + 1 target; re-run is a
no-op; all CHECK + unique-index constraints enforce as designed; final
assertions pass with 0 missing rows.
Prod impact at landing: 0 rows in user_caldav_config and 0 appointments
with caldav_uid — backfill is a true no-op. Slice 1 ships invisible.
|
|||
| 586ba29b86 |
feat(test): migration dry-run gate + boot smoke (Slice 1)
Slice 1 of docs/design-paliad-test-strategy-2026-05-19.md — the test infrastructure that would have caught mig 098 (digit-regex) and mig 099 (missing audit_reason) before the deploy hit prod. Three new files + one route addition: - Makefile: `make verify-migrations` (alias `verify-mig`) runs the per-migration dry-run + boot smoke against TEST_DATABASE_URL. Fails fast with a clear error if TEST_DATABASE_URL is unset so CI can't silently pass a missing env var. `make test` and `make test-go` cover the rest of the short / full Go suites. - internal/db/migrate_test.go (TestMigrations_DryRun): walks every pending *.up.sql in numeric order, applies each inside its own BEGIN..ROLLBACK transaction, fails on the first SQL error with the file name + Postgres error. "Pending" = greater than the scratch DB's current tracker version, so fresh-DB CI runs verify everything while developer scratch DBs only re-verify the new pending migration. Always non-destructive — the rollback runs even on success. - cmd/server/main_smoke_test.go (TestBootSmoke): boots the apply path end-to-end, asserts (a) db.ApplyMigrations returns nil, (b) the tracker advanced to the highest *.up.sql version on disk with dirty=false, (c) GET /healthz on the registered mux returns 200. The dry-run catches per-migration syntax errors; this catches the apply+bind path the container actually runs. - internal/handlers/handlers.go: adds a GET /healthz public route — a no-auth, no-DB liveness probe. Used by the boot smoke; also safe for any future orchestrator or uptime check. Both live-DB tests gate on TEST_DATABASE_URL and skip cleanly without it, matching the rest of paliad's live-DB test pattern. Verification: go build ./... clean, go vet ./... clean, go test -short ./internal/... ./cmd/... clean (all packages pass, live-DB tests skip), bun run build clean (2436 i18n keys unchanged). Per CLAUDE.md inventor → coder gate, NOT self-merged. |
|||
| c10f8cff70 |
feat(t-paliad-207): mig 100 — make CCR filing visible in calc output when with_ccr is set
m's observation 2026-05-18 (interactive session): toggling "Mit Nichtig-
keitswiderklage" surfaces the response rules (def_to_ccr, reply, rejoin,
…) but the triggering event itself — the act of filing the CCR — is
invisible. Per R.25 VerfO the CCR is filed AS PART OF the Statement of
Defence with the same 3-month deadline, so the corpus author (mig 028)
skipped it. UX problem: users see consequences without the cause.
**New rule** `upc.inf.cfi.ccr`:
- parent: `upc.inf.cfi.soc` (root anchor, same as SoD)
- duration: 3 months (same as SoD — no separate deadline)
- party: defendant
- legal_source: `UPC.RoP.25.1`
- condition_expr: `{"flag":"with_ccr"}`
- priority: **`informational`** — renders as a notice card, no save
action, no duplicate write into paliad.deadlines (the SoD's row
already covers the calendar date).
**Sequence reshuffle** — inserting at sequence_order=11 pushes
def_to_ccr 11→12 and app_to_amend 12→13 so the timeline reads
SoD → CCR → def_to_ccr → app_to_amend (cause before effect).
**Idempotency** — INSERT uses NOT EXISTS keyed on
(proceeding_type_id, submission_code, lifecycle_state='published');
UPDATEs are guarded by the source sequence_order so re-apply is a
no-op. audit_reason set via set_config('paliad.audit_reason', ...,
true) at the top per the mig 099 hotfix pattern.
Migration counter re-checked against origin/main + ls
internal/db/migrations/ | tail before picking 100 — per the friction
note from msg 2016.
Build hygiene: go build/vet clean; bun run build clean (no i18n
changes). Down.sql restores both sequence values + DELETEs the new
row. Branch: mai/fermi/interactive-session.
|
|||
| 283c9e8f67 |
fix(mig 099): add missing audit_reason wrapper
Mig 099 (drop_with_po_flag) crash-looped paliad.de prod immediately after deploy: the mig 079 trigger on paliad.deadline_rules raises EXCEPTION 'audit reason required' on UPDATE when paliad.audit_reason is unset. Original file (fermi, t-paliad-207) only had the UPDATE, no set_config wrapper. Patch: prepend the standard 'SELECT set_config(paliad.audit_reason, ...)' at the top so the trigger sees the reason. Same shape as every other migration that mutates deadline_rules. Manual recovery already applied via head MCP — UPDATE'd the 2 rows with audit_reason set, marked tracker version=99 dirty=false, force-restarted the container which booted clean. This commit aligns the in-repo file with the recovered prod state. Idempotent: the WHERE clause matches only rows that still carry with_po, so re-apply is a no-op. |