Commit Graph

7 Commits

Author SHA1 Message Date
m
7c44bbec7e refactor: onboarding form — drop Praxisgruppe, free-text role, add Dezernat (t-paliad-020)
- Drop the Praxisgruppe field from the onboarding form. Every Paliad user
  is in patent practice, so the field carried no signal. The DB column is
  retained for future use (set to NULL on insert).
- Switch role from a 4-value enum (partner/associate/pa/admin) to free
  text with a <datalist> of suggestions (Partner, Associate, PA, Of
  Counsel, Referendar/in, Trainee, wiss. Mitarbeiter/in, Sekretariat).
  German firms have many roles beyond the original four.
- Add an optional Dezernat field — the team led by a specific partner.
  Free text, no FK (the partner may not be registered yet).

Backend:
- Migration 015: drop the role enum CHECK, replace with non-empty CHECK;
  ADD COLUMN dezernat text.
- UserService.Create: drop validRoles map, require non-empty role string,
  trim and persist Dezernat. Admin bootstrap gate unchanged.
- models.User gains Dezernat *string; userColumns SELECT updated so
  /api/me returns it.

Frontend:
- onboarding.tsx: replace role <select> with <input list=...>; add
  dezernat input; remove practice_group.
- onboarding.ts: send dezernat (if non-empty), require role.
- i18n: add onboarding.role.placeholder, onboarding.dezernat[.placeholder],
  onboarding.error.role; remove the role.* enum and practice_group keys.
2026-04-18 20:26:11 +02:00
m
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.
2026-04-17 13:54:32 +02:00
m
5a9f8e5874 feat(notizen): Phase I — Notizen (polymorphic notes)
Polymorphic notes attached to Akten, Fristen, Termine, or AktenEvents.
Schema (paliad.notizen + paliad.notiz_is_visible) shipped with Phase A
migrations; this phase adds the service, handlers, and shared UI.

Backend
- NotizService (internal/services/notiz_service.go): ListForAkte /
  ListForFrist / ListForTermin / ListForAktenEvent + Create / Update /
  Delete. Visibility resolves through the parent row — AkteService.GetByID
  for Akte/Frist/AktenEvent parents, TerminService.GetByID for Termin
  parents (personal Termine are creator-only).
- Edit restricted to the original author; delete allows author +
  partner/admin. Create on an Akte-scoped parent appends an akten_events
  "notiz_created" audit row in the same transaction; personal Termin
  notes skip the audit.
- Author join (paliad.users) surfaces display_name + email on every
  listed note so the client can render "von <Name>" without per-row
  /api/users fetches.
- Routes wired in handlers.go: GET/POST /api/akten|fristen|termine/{id}/
  notizen, PATCH/DELETE /api/notizen/{id}.

Frontend
- Shared client module frontend/src/client/notizen.ts exposes
  initNotes(container, parentType, parentId). Renders an add-note form,
  list of note cards with relative timestamps (gerade eben / vor N
  Minuten / gestern / …), edit + delete affordances gated by author/
  role, optimistic add/edit/delete with rollback on error, Ctrl+Enter
  submit, and URL auto-linkification inside sanitised note bodies.
- Integrated into akten-detail (Notizen tab — placeholder replaced),
  fristen-detail (new "Notizen" section below the detail list), and
  termine-detail (new "Notizen" section above the edit form).
- DE + EN i18n keys added; obsolete akten.detail.soon.notizen placeholder
  keys removed.
- Notiz-card styles added to global.css (accent-coloured focus, hover
  actions, relative-time colour) matching the existing Verlauf card look.
2026-04-17 12:12:29 +02:00
m
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
2026-04-17 11:59:49 +02:00
m
316dc9f9bf feat(fristen): Phase E — Persistent deadline management UI
Adds the persistent-deadline layer on top of the Phase A schema:

Backend (Go)
- internal/services/frist_service.go: CRUD + bulk import + summary
  counts, all gated through AkteService.GetByID for office-scoped
  visibility. Every mutation writes an akten_events row.
- internal/handlers/fristen.go: GET/POST/PATCH/DELETE for /api/fristen,
  /api/fristen/{id}, /api/fristen/{id}/complete, /api/fristen/summary,
  /api/akten/{id}/fristen, /api/akten/{id}/fristen/bulk.
- internal/handlers/fristen_pages.go: serves the four new HTML pages.
- Models: Frist + FristWithAkte (joined for the list page).
- Service wired into cmd/server/main.go.

Frontend (Bun TSX + per-page client TS)
- /fristen        — list with traffic-light summary cards (red/amber/
                    green), status + Akte filters, inline mark-complete.
- /fristen/neu    — create form (Akte select, due date, optional rule
                    + notes); /akten/{id}/fristen/neu pre-selects.
- /fristen/{id}   — detail with inline edit, complete, role-gated delete.
- /fristen/kalender — month grid with deadline dots + day popup.
- Akten detail "Fristen" tab now shows the real list (Phase D
  placeholder removed).
- Fristenrechner: "Als Frist(en) speichern" CTA opens a modal that
  picks an Akte + which calculated rows to import (POSTs to /bulk).
- Sidebar: activates the Fristen entry (was greyed-out in Phase D).
- DE/EN i18n for all new copy.
- Traffic-light + calendar styles in global.css.

Visibility, audit and role-gating reuse the Phase B/D primitives —
no new RLS or auth surface.
2026-04-16 17:28:44 +02:00
m
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
2026-04-16 17:11:02 +02:00
m
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)
2026-04-16 14:25:55 +02:00