4c0babb2f311ceefa0864a4f6b04d443ed27d721
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4c0babb2f3 |
feat(checklisten): instanceable checklists — DB-backed state, Akte linkage
Checklisten move from one-per-slug localStorage state to a template/instance
model. A user creates multiple named instances of each template (UPC SoC,
EPA Einspruch, …), each with its own checkbox state in paliad.checklist_instances
and an optional akte_id for office-wide visibility.
- Migration 014: paliad.checklist_instances + RLS mirroring the Termine
pattern (akte_id nullable → creator-only; akte_id set → can_see_akte gate).
- Static template data moves out of internal/handlers into internal/checklisten
so both handlers and the new ChecklistInstanceService can reference it
without an import cycle.
- ChecklistInstanceService: CRUD + state merge via `state || $n::jsonb`
so concurrent checkbox toggles don't clobber each other. Reset clears
state to {}. Akte-linked mutations append akten_events audit rows.
- Handlers: GET/POST /api/checklisten/{slug}/instances, GET/PATCH/DELETE
/api/checklisten/instances/{id}, POST .../reset, GET /api/akten/{id}/checklisten.
- /checklisten/{slug} redesigned to show template metadata + instance
table + "Neue Instanz" modal (with optional Akte dropdown). The
interactive checkboxes move to /checklisten/instances/{id} where the
state is DB-backed and Reset posts to the server. Fixes the original
Reset button regression — it now operates on real server state rather
than silently failing client-side.
- Akten detail grows a Checklisten tab listing linked instances with
progress bars; only loads on tab activation.
- localStorage-based progress removed from the overview grid (state no
longer lives there).
- DE + EN i18n keys added.
Verified: bun run build clean; go build ./...; go vet ./...; go test ./...
all green.
|
||
|
|
b56ef660df |
feat(termine): Phase F — Termine (appointments) + CalDAV sync
Ship the appointments feature with bidirectional CalDAV synchronisation.
Closes KanzlAI audit §1.3 by encrypting CalDAV passwords at rest with
AES-256-GCM; plaintext credentials never touch the DB or API responses.
Backend
- `internal/services/termin_service.go`: CRUD with per-row visibility.
Personal Termine (akte_id NULL) visible only to created_by; Akte-attached
Termine follow AkteService.GetByID. Every Akte-attached mutation appends
an akten_events row for the audit trail.
- `internal/services/caldav_service.go` (+ caldav_client.go, caldav_ical.go,
caldav_crypto.go): per-user goroutine, 60s tick, push VEVENT + pull with
UID/ETag reconciliation. Last-write-wins on conflict; conflicts on
Akte-attached Termine append to akten_events.
- CALDAV_ENCRYPTION_KEY env var (32-byte AES-256, base64). Server refuses
to start with malformed key; unset key leaves CalDAV disabled and all
/api/caldav-config* endpoints return 501.
- Migration 013: paliad.user_caldav_config (password_encrypted bytea) +
paliad.caldav_sync_log (last-5 per user). RLS: user owns their row only.
- HTTP handlers: GET/POST/PATCH/DELETE /api/termine, GET
/api/akten/{id}/termine, /api/caldav-config CRUD + /test + /log.
Frontend
- Termine list / detail / new / kalender pages (Bun TSX + per-page client
TS), calendar month grid with type-coloured dots and click-popup.
- Einstellungen/CalDAV settings page: URL/user/password (write-only),
test-connection button, status card, sync log table, delete button that
purges credentials.
- Akten detail "Termine" tab replaces the Phase D placeholder — inline
add-termin form + list.
- Sidebar: Termine entry activated; new "Einstellungen" group with CalDAV.
- DE/EN i18n complete for every new surface.
Security posture
- AES-GCM with 12-byte random nonce prepended to ciphertext
- Password field has `json:"-"` on the model; API never returns it
- Frontend always sends password via write-only <input type=password>
- DeleteConfig purges the encrypted blob from the primary row
- TestConnection without stored creds requires explicit password
t-paliad-010
|
||
|
|
d1909c766e |
feat: Phase C — Fristenrechner → DB-backed via FristenrechnerService
- Delete internal/calc/deadlines.go/deadline_rules.go/holidays.go (ported to services) - fristenrechner handler routes through FristenrechnerService when pool present - Returns 503 with German message when DATABASE_URL unset (page still renders) - Migration 012: add name_en columns + seed 9 UI-facing proceeding types - Commit captures cronus's work after session termination |
||
|
|
95817fe78c |
fix(db): use paliad_schema_migrations tracker to avoid public.schema_migrations collision
Production crash when DATABASE_URL was first set on the shared Supabase: pq: column "dirty" does not exist at column 17 (42703) in line 0: SELECT version, dirty FROM "public"."schema_migrations" Root cause: the Supabase instance already had a differently-shaped public.schema_migrations (version-only, no dirty column) from another app or earlier tool. golang-migrate's default tracking table is called "schema_migrations" and lives in current_schema() (public, since paliad didn't exist yet at migrator startup). The driver tried to read its own schema from the foreign table and blew up. Fix: 1. Set postgres.Config.MigrationsTable = "paliad_schema_migrations" — a uniquely-named tracker that cannot collide with another app's table. 2. Pre-create the paliad schema before invoking golang-migrate so subsequent migrations target it cleanly. Idempotent via IF NOT EXISTS. 3. Leave the tracker in `public` (default SchemaName). Rationale: the first migration's down-step is DROP SCHEMA IF EXISTS paliad CASCADE, which would take a paliad.schema_migrations tracker with it and break any subsequent migrate.Up(). Keeping it in public makes down-cycles safe. Verified locally: - Reproduced the collision by creating a public.schema_migrations with only a version column (matching the production shape) and running the fixed migrator against it. - Pre-existing public.schema_migrations untouched (version=42 preserved). - New public.paliad_schema_migrations created at version=11. - All 15 paliad.* tables created. - Idempotent: second migrator run reports ErrNoChange, no double-apply, seed data unchanged. - Live tests (TEST_DATABASE_URL) still pass against the collision DB. |
||
|
|
bcc4939af2 |
feat(services): Phase B — sqlx pool, services, Akten/Frist endpoints
Implements docs/design-kanzlai-integration.md §8 Phase B.
Pool & infrastructure:
- internal/db/pool.go — sqlx connection pool via DATABASE_URL
(lazy, sync.Once, returns nil if unset)
- cmd/server/main.go wires pool + services on startup; skips gracefully
if DATABASE_URL unset (existing endpoints still work)
Services (internal/services/):
- holidays.go — ported from KanzlAI. Audit §1.6 fix: replaces unguarded
map with sync.Map of *yearEntry (sync.Once per year), race-safe under
concurrent readers.
- deadline_calculator.go — ported. days/weeks/months + before/after
timing + holiday/weekend adjustment via HolidayService.
- deadline_rule_service.go — ported, DB-backed. List, GetRuleTree,
GetFullTimeline (recursive CTE for cross-type spawns), GetByIDs,
ListProceedingTypes.
- user_service.go — reads paliad.users; GetByID returns (nil, nil) for
users who haven't onboarded yet (safe default = no visibility).
- akte_service.go — new. Office-scoped visibility enforced at the app
layer (defense-in-depth alongside RLS). ListVisibleForUser uses the
visibility predicate directly in SQL so indexes can drive the query.
Create/Update/Delete enforce role gates:
* associates can only create in their own office
* only admins can move an Akte between offices
* only partners/admins can toggle firm_wide_visible
* only partners/admins can delete (soft, status='archived')
Writes an akten_events row on create, status change, firm-wide toggle,
collaborator change.
- parteien_service.go — ported. Visibility inherited from the parent
Akte via AkteService.GetByID gate.
Sentinel errors:
- services.ErrNotVisible → handlers return 404 (never leak existence)
- services.ErrForbidden → 403
- services.ErrInvalidInput → 400
Auth context:
- internal/auth/user.go — WithUserID middleware extracts the `sub` claim
from the Supabase JWT session cookie and injects uuid.UUID into the
request context. Runs after Client.Middleware (which already validated
the cookie expiry). Handlers use auth.UserIDFromContext().
Handlers (internal/handlers/):
- akten.go — full CRUD for /api/akten + /api/akten/{id}/parteien.
All require DB configured (503 otherwise) and authenticated user
(401 otherwise). Returns 404 for non-visible IDs.
- deadline_rules_db.go — GET /api/deadline-rules, GET
/api/proceeding-types-db, POST /api/deadlines/calculate.
The /api/deadlines/calculate endpoint lives alongside the existing
in-memory /api/tools/fristenrechner; Phase C swaps the UI over and
deletes the in-memory rule tree.
- handlers.Register now takes an optional *Services bundle; when
DATABASE_URL unset the DB-backed endpoints return 503 with a clear
error message.
Tests (internal/services/):
- holidays_test.go — Easter algorithm (5 years spot-checked), German
federal holidays, weekend + Neujahr adjustment, concurrent cache
reads under -race.
- deadline_calculator_test.go — days/weeks/months calc, before timing,
Karfreitag→Ostermontag skip (lands on Tue 2026-04-07), batch with
zero-duration rule.
- akte_service_test.go — live DB test behind `TEST_DATABASE_URL` (skip
otherwise). Verifies 4-Akte × 3-user visibility model AND role
enforcement (associate can't delete, can't cross-office-create,
invalid office rejected).
Manual verification:
- `go build ./...` + `go vet ./...` clean
- `go test ./internal/services/ -race` passes (DB tests skip without URL)
- With TEST_DATABASE_URL set, all visibility + role tests pass
- Live HTTP smoke test with forged JWT cookie:
* /api/deadline-rules returns 40 rules
* /api/proceeding-types-db returns 7 types
* /api/deadlines/calculate INF + 2026-04-15 returns calculated deadlines
* /api/akten returns [] (user has no paliad.users row yet — safe default)
* /login, / still work (no regressions)
|
||
|
|
1b2ef28334 |
feat(db): Phase A — paliad schema, RLS, migrations, golang-migrate
Implements docs/design-kanzlai-integration.md §8 Phase A. Schema (paliad.*): - users (extends auth.users) with office, practice_group, role - akten with visibility columns: owning_office, collaborators uuid[], firm_wide_visible (per design §2) - parteien, fristen, termine, dokumente, akten_events, notizen (polymorphic notes; notizen_exactly_one_parent CHECK) - proceeding_types, deadline_rules, holidays (reference data) - 4 feedback tables re-namespaced from public.* into paliad.* (handler swap to direct DB is a follow-up; old public tables stay intact for now and continue serving via PostgREST) Visibility (paliad.can_see_akte): - single SQL function, used by every RLS policy - predicate: firm_wide_visible OR owning_office matches user's office OR auth.uid() ∈ collaborators OR user is admin - mirrored at app layer in Phase B (defense in depth) RLS (real, not permissive): - akten: visibility predicate; insert restricted to own office or admin; delete restricted to partners + admins - parteien/fristen/dokumente/akten_events: inherit via can_see_akte(akte_id) - termine: personal (akte_id NULL) visible only to creator; Akte-linked follow visibility predicate - notizen: paliad.notiz_is_visible() resolves polymorphic parent - reference tables: SELECT for any authenticated user - users: SELECT all; UPDATE/INSERT only self - feedback tables: INSERT for any authenticated user (write-only) Seed data (ported from KanzlAI seed_upc_timeline.sql): - 7 proceeding_types (INF, REV, CCR, APM, APP, AMD, ZPO_CIVIL) - 40 deadline_rules (32 UPC + 4 ZPO + 4 cross-type appeal spawns) including conditional logic: Reply rule code (RoP.029b → 029a) and Rejoinder duration (1mo → 2mo) flip when CCR active - 55 holidays (DE federal 2026/2027 + UPC summer 2026 + UPC winter 26/27) Indexes per audit §3.3 + visibility-predicate hot paths: - akten: (status, owning_office), (owning_office), partial on firm_wide_visible, GIN on collaborators - fristen: (status, due_date), (akte_id) - termine: (start_at), (akte_id) - akten_events: (akte_id, created_at DESC) - notizen: 4 partial indexes per parent type - users: (office), (role) Migration tooling: - golang-migrate/migrate/v4 with embed.FS source - Migrations live in internal/db/migrations/ (Go embed can't reach outside the package; this is the conventional Go layout for embedded migrations) - Applied at server startup before HTTP listener binds - DATABASE_URL is optional today (existing knowledge tools work without DB); becomes required once Phase B services land - Mock Supabase auth schema for local testing in internal/db/migrations/_dev/mock_supabase_auth.sql (excluded from embed pattern by the underscore prefix) Other changes: - Dockerfile: bump golang to 1.24, copy go.sum (audit §2.9), rename binary patholo → paliad - docker-compose.yml: add DATABASE_URL passthrough - README.md: rewritten to reflect Paliad brand + Phase A migration system Verified locally: - 11 migrations applied cleanly against postgres:16-alpine - RLS enabled on all 15 paliad.* tables (verified via pg_class.relrowsecurity) - Visibility predicate verified with 4-case scenario: - Alice (Munich associate): sees Munich + firm-wide + collab-on (t f t t) - Bob (Düsseldorf associate): sees Düsseldorf + firm-wide + collab-on (f t t t) - Carol (Munich partner): sees Munich + firm-wide only (t f t f) - Anonymous: sees firm-wide only (f f t f) - migrate down + re-up cycle clean (initial 007 down had ordering bug, fixed: drop policies before referenced function) - Existing endpoints (/, /login) return 302 + 200 — no regressions |