Commit Graph

105 Commits

Author SHA1 Message Date
m
59e1cb1445 Merge: i18n fallback fix + missing projekte.* translations 2026-04-20 17:06:38 +02:00
m
449075deaf fix(i18n): preserve default HTML text when key missing + add all projekte.* keys
Root cause: applyTranslations() in client/i18n.ts unconditionally overwrote
textContent/placeholder/title with t(key), and t() falls back to the raw key
name when no translation exists. Result: every projekte.* data-i18n attr in
the v2 pages rendered the literal key string ('projekte.heading',
'projekte.subtitle', ...) because I shipped the pages with new i18n keys
without adding the translations.

Two fixes, both in client/i18n.ts:

1. **Fallback behaviour**: applyTranslations() now uses a new internal
   tOrEmpty(key) that returns '' when the key is missing in DE and EN,
   and the call site only overwrites the DOM when the lookup yielded a
   real value. Missing keys no longer clobber the author-provided default
   text. This is belt-and-braces for any future page that ships a key
   before its translation does.

2. **Missing translations added**: ~90 projekte.* keys for DE and EN,
   covering the list page (projekte.heading/subtitle/new/search/filter.*/
   view.*/col.*/empty.*/unavailable), the create form (projekte.neu.*,
   projekte.field.*, projekte.cancel/submit/error.*), and the detail page
   (projekte.detail.title/back/loading/notfound/edit/save, tab.* for all
   eight tabs, verlauf.*, team.form.*/col.*, kinder.*, parteien.*
   form/role/col/empty, fristen.*, termine.*, checklisten.*, delete.*).

go build/vet/test + bun run build all clean.
2026-04-20 17:06:21 +02:00
m
adb0ce2c9d fix(modals): add padding to .modal-card — content no longer flush to edges 2026-04-20 17:05:53 +02:00
m
746bced30b Merge: projekte-detail v2 + tree view + per-Dezernat member manager (t-paliad-024 follow-ups)
- akten-detail.tsx rewritten as v2 shell: breadcrumb, type chip, ClientMatter with
  ancestor inheritance, netDocuments link, Team tab (direct+inherited), children tab.
- Tree view mode in Projekte list (depth-indented by path).
- Per-Dezernat member management panel in settings (add/remove with typeahead).
- i18n DE+EN coverage for all new keys.

Branch mai/cronus/projekte-detail-v2 @ 7e0c063.
2026-04-20 15:35:39 +02:00
m
7e0c06342b feat: projekte-detail rewrite + tree view + per-Dezernat member manager (follow-ups)
**akten-detail.tsx rewrite (now projekte-detail-shaped):**
- Removed office-chip, firmwide-chip (v2 no longer uses them).
- Added type-chip, ClientMatter display (inherits via ancestors when absent),
  netDocuments external link.
- Breadcrumb nav above header, populated from /api/projekte/{id}/ancestors.
- New 'Untergeordnet' tab with children list from /kinder endpoint;
  'Untervorhaben anlegen' link pre-fills parent via ?parent=<id>.
- New 'Team' tab: lists direct + inherited members (inheritance badge
  shows ancestor title), remove button gated on self-or-partner/admin,
  add form with user typeahead and role picker.
- akten-detail.ts: Akte interface rewritten (reference/type/parent_id/
  path/client_number/matter_number/netdocuments_url/court/case_number).
  parseAkteID now accepts both /projekte/{id} and /akten/{id}. New loaders
  loadAncestors/loadChildren/loadTeam/loadUserList. TabId extended with
  'team' and 'kinder'.
- akten-neu.ts: applyParentFromQueryString pre-fills parent picker when
  navigated from a projekt's 'Untervorhaben anlegen' link, auto-switches
  type from 'client' to 'case'.

**Tree view in Projekte list:**
- Third view mode 'tree' alongside flat/roots. Sorts filtered rows by
  path (ancestors precede descendants); depth-indented title cell with
  ↳ branch glyph based on depthOf(path).

**Per-Dezernat member manager:**
- einstellungen Dezernat tab 'Verwalten' button now toggles an inline
  manage panel per Dezernat (expanded row below the admin table row).
- Panel shows current members with per-row remove (confirm dialog).
- Add-member form with user typeahead against /api/users, posts to
  /api/dezernate/{id}/members.
- Wires once per Dezernat (data-wired guard); reloads My Dezernat on
  any membership change.

i18n: DE + EN keys for dezernat.manage_heading/loading/no_members/
add_member*/add/remove/confirm_remove/error.user_required and for every
projekte.type.* / projekte.team.role.* / projekte.team.direct /
projekte.team.inherited.hint / projekte.view.tree / projekte.detail.team.*
/ projekte.detail.clientmatter.inherited.

go build/vet/test + bun run build all clean.
2026-04-20 15:35:01 +02:00
m
a2388e9a6b Merge: data model v2 — hierarchical Projekte, Teams with inheritance, Dezernate 2026-04-20 15:18:35 +02:00
m
41cc295500 feat: Dezernate settings tab + best-effort seeding migration (Phase 4)
- einstellungen.tsx: fourth tab 'Dezernat'. My Dezernat card (name, office,
  lead, member list). Admin-only 'Dezernate verwalten' section with table
  (name/office/lead/members/delete) + 'Neues Dezernat anlegen' form behind
  a details summary. Admin controls hidden unless /api/me.role='admin'.
- client/einstellungen.ts: loadDezernatTab() fetches /api/dezernate, then
  per-dezernat /api/dezernate/{id}/members to resolve membership for the
  'My Dezernat' view. Admin table with delete-with-confirm. New-Dezernat
  form posts to /api/dezernate; inserts into in-memory list on success.
  TabName + TABS + loadedTabs dispatcher extended.
- i18n: dezernat.* keys (DE+EN) — heading/subtitle/admin section/table
  columns/form labels/error strings.
- Migration 019: best-effort seed of paliad.dezernate + dezernat_mitglieder
  from paliad.users.dezernat free-text. Each distinct non-empty name
  becomes one Dezernat (office = MIN(members.office)); every user whose
  free-text matches joins. free-text column preserved so a second pass
  can clean it up later. down-migration only deletes rows we inserted
  (matches name = btrim(user.dezernat)), leaves admin-created Dezernate
  alone.

go build/vet/test + bun run build all clean.
Branch mai/cronus/implement-data-model-v2 now covers all four phases.
2026-04-20 15:12:24 +02:00
m
640d5c1a23 feat: frontend v2 — Projekte list/create, dashboard + downstream field renames
- akten.tsx + client/akten.ts rewritten for v2: renders /projekte list with
  type filter (client/litigation/patent/case/project), status filter, flat
  vs roots view toggle, search across title/reference/client_number/
  matter_number. Columns now Title / Type / Reference / ClientMatter /
  Status / Updated (no more office column per v2 team-based visibility).
- akten-neu.tsx + client/akten-neu.ts rewritten: type selector drives
  conditional fields (client industry/country/billing; patent number +
  filing/grant dates; case court + case_number). Parent projekt picker
  (typeahead over /api/projekte, stores parent_id). ClientMatter
  client_number + matter_number (7-digit patterns) + netdocuments_url
  fields on every type.
- dashboard.ts field renames: akte_id → projekt_id, akte_title →
  projekt_title, akte_ref → projekt_ref. Activity/deadline/appointment
  links now point to /projekte/{id}.
- Mass field rename across fristen-*, termine-*, checklisten-*,
  fristenrechner.ts, notizen.ts, akten-detail.ts: akte_id → projekt_id,
  akte_aktenzeichen → projekt_reference, akte_title → projekt_title,
  akte_office → projekt_office. URL paths /akten/${...} → /projekte/${...}.

Pages still referencing deprecated shape (owning_office, collaborators,
firm_wide_visible) render blank for those columns — acceptable during
transition, full akten-detail rewrite (add breadcrumb + Team tab with
inheritance) still pending.

go build/vet/test + bun run build all clean.
2026-04-20 15:09:22 +02:00
m
4ac9dacaa0 feat: /projekte routes + sidebar label + legacy POST shim (Phase 3 partial)
Server-side:
- GET /projekte[...] routes alias the existing Akten list/detail/tab pages
  so users can reach the v2 URL without a 404 during the cutover. The TSX
  pages themselves still render the legacy HTML shell pointing at
  /api/akten legacy aliases.
- POST /api/projekte (and legacy POST /api/akten alias) now accepts BOTH
  old shape ({aktenzeichen, owning_office, court_ref}) and new shape
  ({reference, type, parent_id, client_number, matter_number,
  netdocuments_url, case_number}). aktenzeichen → reference,
  court_ref → case_number. owning_office is ignored (no longer part of
  visibility model).

Frontend:
- Sidebar nav link 'Akten' → 'Projekte' → /projekte.
- i18n: nav.projekte added (DE: 'Projekte', EN: 'Projects').

Still PENDING (Phase 3 remainder + Phase 4):
- Frontend TSX pages (akten.tsx, akten-detail.tsx, akten-neu.tsx etc.) still
  use legacy field names (aktenzeichen, owning_office, collaborators,
  firm_wide_visible). GET /api/akten returns NEW shape (reference, type,
  parent_id, path, client_number, matter_number, netdocuments_url). UI will
  display blank fields where the old column is missing. Full rewrite needed
  per task spec (tree view, type filter, breadcrumb, team tab with
  inheritance badges, client create form, projekt create with type +
  parent typeahead).
- Dezernate settings tab (Phase 4) not yet built — API endpoints exist at
  /api/dezernate[...] but no UI.
- Dashboard JSON shape changed (akte_id → projekt_id, akte_title →
  projekt_title, akte_ref → projekt_ref); frontend dashboard.tsx needs an
  update to read the new field names.

Build: go build/vet/test and bun run build all clean.
2026-04-20 14:55:06 +02:00
m
cb2841fba9 feat: handlers — Projekt/Team/Dezernat wiring (Phase 2)
- handlers/projekte.go (was akten.go): Projekt CRUD + tree ops (children,
  tree, ancestors), events cursor-paginated, parteien endpoints.
- handlers/teams.go: GET/POST/DELETE on /api/projekte/{id}/team. ListEffectiveMembers
  returns direct + inherited (annotated with inherited_from_id/title).
- handlers/dezernate.go: admin-gated CRUD for paliad.dezernate + member
  add/remove. Readable by any authenticated user.
- handlers/fristen.go, termine.go, notizen.go, checklist_instances.go updated
  to use projekt_id. Kept /api/akten/{id}/fristen|termine|notizen|checklisten
  as legacy aliases pointing at the same projekt-aware handlers.
- handlers/users.go: dropped handleListAkteEvents (superseded by
  handleListProjektEvents under /api/projekte/{id}/events).
- cmd/server/main.go: ProjektService + TeamService + DezernatService wired
  into handlers.Services. Downstream services (Parteien, Frist, Termin,
  Notiz, Checklist) take projektSvc.
- Removed obsolete internal/services/akte_service_test.go. go build/vet/test
  all clean.

Legacy /api/akten routes still resolve (handlers/JSON shape unchanged on
the GET/POST path) so frontend stays functional during the client cutover.
New /api/projekte routes live alongside.

Phase 3 (frontend tree UI, /projekte page, team tab) + Phase 4 (Dezernat
settings tab) still pending.
2026-04-20 14:52:44 +02:00
m
9aa8037193 refactor: services — Projekt, Team, Dezernat services (WIP Phase 2)
Models: Akte → Projekt (tree type + parent_id + path + client/matter numbers
+ netDocuments URL + type-specific client/patent/case columns). AkteEvent →
ProjektEvent. FristWithAkte → FristWithProjekt. TerminWithAkte → TerminWithProjekt.
Notiz.AkteID → ProjektID. ChecklistInstance.AkteID → ProjektID. Partei.AkteID →
ProjektID. User adds AdditionalOffices pq.StringArray.

Services:
- NEW projekt_service.go replaces akte_service.go. Adds tree ops: List/GetByID/
  ListChildren/ListAncestors/GetTree. Create auto-adds creator to projekt_teams
  role=lead in same tx. ResolveClientNumber walks path for inheritance.
  Visibility helpers (visibilityPredicate / Positional / Placeholder) centralise
  team-based access check: admin OR any ancestor/direct projekt_teams row.
- NEW team_service.go — AddMember/RemoveMember/ListDirectMembers/
  ListEffectiveMembers (unions direct + inherited via path, dedup by user;
  direct wins)/IsEffectiveMember. Inherited=true set at read time only.
- NEW dezernat_service.go — admin-gated CRUD + member add/remove + user
  membership lookup for settings page.
- frist_service.go → projekt_id everywhere, uses visibilityPredicate. ListFilter.
  AkteID → ProjektID.
- termin_service.go → projekt_id everywhere. CalDAV log reads projekt_events.
- notiz_service.go → projekt_id polymorphic branch; eventProjektID() looks at
  projekt_events; akten_event_id column kept (FK now resolves to projekt_events).
- parteien_service.go → projekt_id.
- checklist_instance_service.go → projekt_id with ClearProjekt flag.
- dashboard_service.go → rewrites all four queries against projekte +
  projekt_events + projekt_teams. Matter/Upcoming/Activity surfaces use
  ProjektID/ProjektTitle/ProjektRef.
- reminder_service.go → joins paliad.projekte, aliases a.reference AS
  akte_aktenzeichen for template compat.

Handlers/tests still reference old API — Phase 2 completion requires handler
rewrite (next commit). Build currently broken in internal/handlers.
2026-04-20 14:46:59 +02:00
m
5fcaa7471b feat(schema): data model v2 — migration 018 (projekte tree + teams + dezernate) [t-paliad-024 phase 1]
paliad.projekte — single self-referential tree (types: client/litigation/patent/case/project).
Materialised path (text, '.'-joined UUIDs, inclusive of self) + trigger maintenance.
ClientMatter numbers (client_number + matter_number, 7-digit CHECK each) and netdocuments_url.

paliad.projekt_teams — team membership with inherited flag (writes = false; services annotate
inherited rows on read by walking up path). Unique (projekt_id, user_id).

paliad.dezernate + paliad.dezernat_mitglieder — structural partner units (orthogonal to project
teams; informational office).

paliad.users — adds additional_offices text[] for partners across multiple offices.

Visibility simplified to team-based only: can_see_projekt() = admin OR direct/ancestor team
membership (path @> ancestor). owning_office GONE from every projekt — location is no longer
an access gate. Per head (2026-04-20): cases associate with lead partners, not offices.

Data migration: akten → projekte (same UUIDs, type='case', parent NULL orphans). Creator →
projekt_teams(role='lead'); collaborators → projekt_teams(role='associate'). Orphan akten with
no creator + no collaborators become admin-only until reassigned.

Child FK rename: akte_id → projekt_id on parteien, fristen, termine, dokumente, akten_events,
notizen, checklist_instances. No data move (same UUIDs). akten_events renamed to projekt_events.
notizen keeps its polymorphic 4-FK shape.

paliad.akten dropped. can_see_akte() and notiz_is_visible(akte) replaced.

Down-migration restores v1 schema best-effort: only type='case' projekte come back as akten;
non-case tree rows are lost (documented). owning_office backfilled from creator's primary office.

Followups (Phase 2): replace AkteService with ProjektService + TeamService + DezernatService,
wire creator-auto-lead into Create path, update all child services to use projekt_id.

No code changes in this commit — server will fail to build/start until Phase 2 lands.
2026-04-20 14:34:07 +02:00
m
da509755cf Merge: data model v2 design (Mandanten, nestable Projekte, Teams) 2026-04-20 14:18:36 +02:00
m
fabe32aa56 design: data model v2 — Mandanten + nestable Projekte + Teams (t-paliad-023)
Comprehensive design doc for the replacement of flat paliad.akten with:
  - paliad.mandanten (Clients as first-class table)
  - paliad.projekte (single self-referential typed tree, ltree materialised
    path, 5 project types: mandat/litigation/patent/verfahren/projekt)
  - paliad.teams + paliad.team_mitglieder (Dezernate + project teams in one
    table with kind-shape CHECK)
  - paliad.projekt_mitglieder (hot-path junction replacing akten.collaborators)

Polymorphic FK strategy: single project_id FK on fristen/termine/dokumente/
parteien/akten_events/checklist_instances. Notizen keeps its 4-way polymorphic
shape (akte_id renamed to project_id).

Visibility model: tree-connected — seeing any node grants access to the whole
tree (ancestors + descendants). Office-scope stays at project level; Mandant-
level firm_wide_visible / collaborators override.

Migration plan: 6 phases, non-destructive. UUIDs preserved between akten and
projekte rows so child tables only need column renames, no data moves.

Opinionated: German naming throughout (mandanten, projekte, teams,
team_mitglieder, projekt_mitglieder); /akten URLs alias to /projekte
indefinitely; akten_events table name kept for continuity.

Deliverable: docs/design-data-model-v2.md (920 lines, 14 sections).
2026-04-20 14:17:32 +02:00
m
b370d59eee Merge: settings page — profile, email prefs, CalDAV tabs 2026-04-20 13:18:40 +02:00
m
5fb55164b3 feat: settings page — profile, email preferences, CalDAV as tabs (t-paliad-022)
Unified /einstellungen page replaces the standalone CalDAV screen. Three
tabs today (Profil / Benachrichtigungen / CalDAV); adding more is additive
(one <a> in the tab nav, one <section> panel, one loader). Tab switching
is client-side from ?tab=<name> — default tab is Profil.

Profil tab lets users fix onboarding data without admin intervention:
display name, office, role, Dezernat, language. Email is read-only (the
source of truth is auth.users and an account-level change is out of
scope for the settings page).

Benachrichtigungen tab exposes deadline reminder preferences as a master
toggle plus three per-kind sub-toggles (overdue / tomorrow / weekly).
Preferences land in paliad.users.email_preferences (JSONB); missing keys
are treated as opt-in so existing users keep the behaviour they had
before the page shipped.

CalDAV tab is the old /einstellungen/caldav screen ported inline.
/einstellungen/caldav now 301-redirects to /einstellungen?tab=caldav so
bookmarks keep working.

Backend:
- PATCH /api/me (handlers/users.go) mutates the caller's paliad.users
  row. Attempts to include "email" in the body return 400 — the field is
  always server-authoritative.
- UserService.UpdateProfile builds a dynamic UPDATE from the pointer
  fields supplied; omitted keys are left untouched. Re-uses the
  admin-bootstrap guard for role changes.
- GetByID SELECT now includes lang + email_preferences so /api/me
  returns the data the settings page needs without a second round-trip.
- ReminderService consults email_preferences before sending — the helper
  reminderEnabled covers the master switch and per-kind overrides; corrupt
  JSON falls back to on so a bad row can't silence reminders.
- Migration 017 adds email_preferences jsonb NOT NULL DEFAULT '{}' and
  upgrades lang from nullable (from 016) to NOT NULL DEFAULT 'de' with a
  one-shot backfill. Down restores the nullable lang and drops
  email_preferences.

Model change: User.Lang moved from *string to string — it's NOT NULL in
the DB now, so the indirection was carrying no information. Inviter.Lang
and reminder row structs followed suit; the templates and callers used
""/"en" comparisons that translate 1:1.

Sidebar: the "Einstellungen" group now links to /einstellungen (instead
of just /einstellungen/caldav); the CalDAV sub-item is folded into the
tab nav on the page itself.

Tests: reminderEnabled has table-driven coverage (master switch,
per-kind, corrupt JSON, non-bool values). DB-backed user tests still
skip without TEST_DATABASE_URL as before.

Verified: go build ./..., go vet ./..., go test ./..., bun run build —
all clean.
2026-04-20 13:17:24 +02:00
m
e76056cfd1 Merge: email service — SMTP, deadline reminders, invitations (t-paliad-021) 2026-04-20 13:05:31 +02:00
m
11217f7bfa feat: email service — SMTP + deadline reminders + invitations (t-paliad-021)
- internal/services/mail_service.go: SMTP/TLS sender (implicit TLS on 465),
  html/template rendering, branded base layout + content templates, silent
  no-op when SMTP_* unset.
- internal/services/reminder_service.go: hourly scanner for Fristen that are
  overdue / due tomorrow / due within the week (Monday digest). Dedup via
  paliad.reminder_log (24h window).
- internal/services/invite_service.go: POST /api/invite flow with domain
  whitelist, in-memory 10/day/user rate limit, audit row in
  paliad.invitations.
- internal/handlers/invite.go: POST + GET /api/invite handlers.
- Sidebar "Kolleg:in einladen" button + modal on every page.
- migration 016: paliad.reminder_log, paliad.invitations, users.lang column.
- docker-compose: SMTP_* + PALIAD_BASE_URL env vars.
- docs/feature-roadmap.md: documented Supabase auth-SMTP routing as open
  question; current pilot keeps identity mails on Supabase default sender.

Rationale: get Paliad off Supabase's best-effort outbound for the
inbox-facing stuff (reminders, invitations) and move deadline nudges from
passive dashboard to active email. Custom Supabase auth SMTP is blocked on
the shared ydb.youpc.org instance — deferred until Paliad has its own
project or GoTrue webhook relay.
2026-04-20 12:34:38 +02:00
m
45c7cf34ef Merge: onboarding refinement (drop Praxisgruppe, free-text role, add Dezernat) 2026-04-18 21:15:45 +02:00
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
ebb206307d Merge: user onboarding flow (first-login profile setup) 2026-04-18 19:15:54 +02:00
m
b8f95f5d7a feat: user onboarding flow — first-login profile capture (t-paliad-019)
New users were stuck on the dashboard with a dead-end "Bitte schließen Sie das
Onboarding ab" message because nothing created the paliad.users row that all
matter-management features depend on. This adds the missing Phase D flow.

Backend
- UserService.Create: validates display_name / office / role, inserts the
  paliad.users row with (id, email) from the verified JWT claims (never from
  the request body — prevents onboarding as someone else).
- Admin bootstrap: only the very first paliad.users row may self-assign
  role='admin'; subsequent requests get ErrAdminBootstrapOnly (403). Guarded
  by pg_advisory_xact_lock so two concurrent first-logins can't race past
  the count=0 check under READ COMMITTED.
- POST /api/onboarding + GET /onboarding; the page is authenticated but NOT
  behind the onboarding gate (it's the one page users without a paliad.users
  row may reach).
- gateOnboarded middleware wraps the matter-management pages (Dashboard,
  Akten, Fristen, Termine, Einstellungen/CalDAV) and 302s to /onboarding
  when the caller has no paliad.users row. Knowledge-platform pages
  (Kostenrechner, Glossar, Links, Downloads, Gerichte, Gebührentabellen,
  Checklisten, Fristenrechner) stay ungated.
- auth.VerifiedClaims now carries the email claim; auth.ClaimsFromContext
  exposes it to handlers. GET /api/me includes the email in the 404 body so
  the onboarding form can pre-fill the display name from the local-part.

Frontend
- frontend/src/onboarding.tsx + src/client/onboarding.ts: centred card on the
  existing .login-card styling. Fields: display_name (required, pre-filled
  from email local-part), office (dropdown from /api/offices), role
  (dropdown, default associate), practice_group (optional).
- Dashboard client: toggleOnboardingHint now redirects to /onboarding
  instead of showing the dead-end hint — belt-and-braces behind the server
  gate in case the DB lookup fell through.
- DE + EN i18n keys for every label, placeholder, and error.
- Added onboarding to build.ts.

Tests: internal/services/user_service_test.go covers the valid path,
per-field validation, duplicate (ErrUserAlreadyOnboarded), and the
admin-bootstrap gate. Follows the existing TEST_DATABASE_URL skip pattern.
2026-04-18 19:13:57 +02:00
m
41b28bdfdb Merge: audit medium items (pagination, patholo rename, offices) 2026-04-18 19:03:24 +02:00
m
0cdc644b50 fix: audit medium items — Verlauf pagination, patholo→paliad rename, offices (t-paliad-018)
Three items from docs/improvement-audit.md §2:

I-5 Verlauf pagination
- AkteService.ListEvents now accepts a (before *uuid.UUID, limit int) cursor
- SQL uses a composite (created_at, id) cursor subquery — stable across
  rows written in the same microsecond
- Handler parses ?before=<uuid>&limit=<n>, service clamps to 200
- Frontend fetches first page (50) on init and exposes a "Mehr laden" /
  "Load more" button that keeps paging until the tail returns < page size
- i18n keys akten.detail.verlauf.loadMore / .loadingMore in DE + EN

I-8 patholo → paliad client-side rename with migrations
- i18n.ts: STORAGE_KEY is now paliad-lang; one-shot migration reads the
  old patholo-lang value, writes the new key, deletes the old
- sidebar.ts: same pattern for paliad-sidebar-pinned
- Cookie rename with dual-read grace period: SessionCookieName is
  paliad_session, LegacySessionCookieName keeps patholo_session as
  read-only fallback. Requests using the legacy cookie get upgraded to
  paliad_session in the response; legacy cookie is expired in the same
  response. ClearAuthCookies clears both names to prevent stale-cookie
  resurrection. Remove the legacy fallback after 2026-05-18 (30d cookie
  max age).
- handlers/links.go:extractEmailFromCookie reads either cookie name via
  auth.SessionCookieName / auth.LegacySessionCookieName

P-6 Single source of truth for offices
- New internal/offices package: Office struct + All + IsValid + Keys
- akte_service.go switched from inline isValidOffice to offices.IsValid
- GET /api/offices returns the list with DE + EN labels
- Akte create form (akten-neu.tsx) has an empty <select>; the client TS
  fetches /api/offices and populates options, re-rendering on lang change

Tests:
- internal/offices/offices_test.go covers IsValid + Keys + label coverage
- internal/auth: three new Middleware tests — legacy cookie still
  authenticates + upgrades the browser, new cookie wins when both are
  present (no clobber), missing cookie returns 401 on API paths

Build: go build ./... + go vet ./... + go test ./... + bun run build all clean.

Known out-of-scope: handlers/links.go still POSTs to public.patholo_link_*
via PostgREST; migration 011 created fresh paliad.link_* tables but the
handler refactor (move to direct DB, copy data, drop public tables) is a
separate phase documented in that migration's header.
2026-04-18 18:56:35 +02:00
m
f80d1a5753 Merge: audit quick wins (9 items — Dokumente tab, i18n, URLs, FRAND glossar, footer, calendars) 2026-04-18 09:15:43 +02:00
m
67cd66e054 fix: audit quick wins — important + polish batch (t-paliad-017)
Items from docs/improvement-audit.md §2 + §3:

I-1  Hide Dokumente tab entirely from Akten detail (Phase H deferred);
     drop placeholder TSX panel, VALID_TABS entry, and orphaned
     akten.detail.soon.* i18n keys.
I-2  Add data-i18n keys for all 7 office labels on the landing page.
     EN mode now correctly renders "Milan" (was "Mailand").
I-3  Unify UPC URLs in Gerichtsverzeichnis to the canonical hyphenated
     form (unified-patent-court.org) matching links.go — 43 occurrences.
I-6  Add SEP/FRAND glossary category with 13 entries (FRAND, SEP,
     Standard-essentielles Patent, Patentpool, Anti-Suit, Anti-Anti-Suit,
     Injunction Gap, Orange-Book-Standard, Huawei/ZTE, RAND, ETSI IPR,
     Patent-Hold-up, Patent-Hold-out) + filter pill + suggest-modal option.
I-7  Refresh README: list migration 014 (checklist_instances), mark
     Phase I (Notizen) and Phase J (docs) shipped.
P-1  Remove HL Intern stub links (URL "#") and the now-empty "hl" category.
P-2  Dashboard heading: "Meine Mandate" → "Meine Akten" (matches CLAUDE.md
     naming convention). Onboarding hint updated likewise.
P-4  Drop "Hogan Lovells Patent Practice" from the footer — Paliad is the
     firm-agnostic brand.
P-5  Empty-state text on Fristen- and Termine-Kalender when the viewed
     month has no items.

Verified: bun run build clean, go build / vet / test ./... clean.
2026-04-18 09:14:43 +02:00
m
a7df6eb977 fix: resolve route conflict /api/checklisten/{slug}/instances vs /api/checklisten/instances/{id}
Move instance-specific endpoints to /api/checklist-instances/{id} to avoid
Go 1.22+ ServeMux ambiguity panic. Server was crash-looping.
2026-04-18 09:05:57 +02:00
m
6406aba2a5 Merge: critical security fixes (JWT verification, Termine leak, role gates, email whitelist) 2026-04-18 03:16:03 +02:00
m
3e20806aee fix(security): verify JWT signatures + plug 4 other critical gaps (t-paliad-016)
C-1. Session JWT signature verification (authZ bypass fix)
- Add SUPABASE_JWT_SECRET env var; fail-fast at startup if unset.
- auth.Client.VerifyToken uses github.com/golang-jwt/jwt/v5 to verify
  HS256 signatures, reject alg=none, enforce exp/nbf/iat.
- Middleware stores verified claims in request context; WithUserID
  reads only verified claims (no more raw-cookie sub decoding).
- API requests get 401 on missing/invalid token (was 302 redirect).
- Refresh flow only runs on expiry; other signature failures reject
  outright and clear cookies.

C-2. Dashboard Termine cross-user privacy leak
- dashboard_service.loadUpcomingAppointments now mirrors
  TerminService.canSee: personal Termine (akte_id IS NULL) are
  creator-only; admins do NOT see other users' personal Termine.

C-3. Role gate on Parteien + Termine mutations
- ParteienService.Delete now partner/admin only (matches FristService).
- TerminService.Update / Delete on Akte-linked Termine now require
  partner/admin (or the original creator). Personal Termine stay
  creator-only.

C-4. Email gate → ALLOWED_EMAIL_DOMAINS whitelist
- isHoganLovellsEmail → isAllowedEmailDomain reading the env var
  (default: hoganlovells.com,hlc.com,hlc.de). Case-insensitive,
  whitespace-tolerant.
- login.tsx placeholder: name@hoganlovells.comname@hlc.com
- Error strings + login.hint (de/en) rewritten for HLC branding.

C-5. Docker compose env wiring
- docker-compose.yml gains SUPABASE_JWT_SECRET, CALDAV_ENCRYPTION_KEY,
  and ALLOWED_EMAIL_DOMAINS passthrough; commented-out
  ANTHROPIC_API_KEY line for Phase H readiness.

Tests
- auth_test.go: valid/wrong-secret/expired/alg-none/missing-sub/garbage
  token cases for VerifyToken.
- handlers/auth_test.go: default + env-override cases for the email
  whitelist.
- go build ./..., go vet ./..., go test ./... all clean.
2026-04-18 02:23:50 +02:00
m
3e14171808 Merge: product audit + improvement roadmap 2026-04-18 02:15:47 +02:00
cronus
bcdd3d7a59 docs(audit): product audit + improvement roadmap (t-paliad-015)
Post-Phase-A–J full-product audit: UX, code, content, architecture,
ops. 5 Critical findings (JWT signature bypass, dashboard Termine
leak, parteien/termin delete policy gap, @hoganlovells-only email
gate, CALDAV_ENCRYPTION_KEY missing from compose), 8 Important, 10
Polish, 11 Feature ideas, 14 tech-debt items. Each item has a file
reference and a concrete fix.

Top-two exploit-paths (detailed in §1):
  1. internal/auth/auth.go:178 — middleware decodes JWT exp but never
     verifies the signature; sub-claim is trusted downstream by every
     service. Any authenticated cookie → impersonate any user.
  2. internal/services/dashboard_service.go:245 — personal Termine
     leaked cross-user on the /dashboard "Kommende Termine" list
     (missing created_by filter on the akte_id IS NULL branch).
2026-04-18 01:22:23 +02:00
m
117ccefe07 Merge: instanceable checklists — DB-backed, Akte-linked 2026-04-18 01:13:18 +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
e96b9dfb77 fix(login): add autocomplete attributes to email/password fields 2026-04-17 13:47:33 +02:00
m
ee341742b6 fix(sidebar): disable horizontal scrolling on nav 2026-04-17 13:46:35 +02:00
m
42e5a8471c fix(sidebar): make nav scrollable when content overflows 2026-04-17 13:30:31 +02:00
m
59ba1d5778 Merge Phase J: Roadmap rewrite + post-integration status 2026-04-17 13:29:55 +02:00
m
416234b25a Merge Phase I: Notizen (polymorphic notes) 2026-04-17 13:29:55 +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
d0d4f624a1 docs: Phase J — roadmap rewrite + post-integration status
Rewrite docs/feature-roadmap.md per design-kanzlai-integration.md §5:
- All-in-one positioning: knowledge platform + Aktenverwaltung
- New Phase 0 (Aktenverwaltung Foundation) with shipped A–G items
- "What Paliad Is" replaces "What patholo Is NOT"
- Drop §2.3 UPC Rechtsprechungsübersicht (youpc.org link in Link Hub)
- Phase H (AI Frist-Extraktion) marked deferred
- Mark done items in prioritized backlog with completion dates
- Architecture Notes data-strategy: paliad schema + office-scoped RLS

Refresh .claude/CLAUDE.md:
- Aktenverwaltung + knowledge tools in Purpose
- Env var table incl. DATABASE_URL (Akten*/Fristen/Termine), CALDAV_ENCRYPTION_KEY
- Phase status (A–G shipped, H deferred, I pending)
- Akten naming convention (not "Mandate"/"cases")

Refresh README.md:
- Full feature list (Akten, Fristen, Termine, Dashboard + knowledge tools)
- Migration inventory (001–013), migration tracker note
- Env var table with usage semantics
- Project layout + current project status

Append "Post-Integration Status" section to design-kanzlai-integration.md:
- Per-phase shipment table with merge commits
- Phase H deferral note; Phase I pending note
- Phase J split: docs done here; infra retirement (Dokploy, schema drop,
  repo archive) pending head + m coordination
- Email-gate hardcode flagged for follow-up
2026-04-17 12:10:35 +02:00
m
04bf36666f Merge Phase F: Termine + CalDAV sync 2026-04-17 12:01:47 +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
bebf79ba63 Merge Phase G: Dashboard landing
# Conflicts:
#	frontend/build.ts
#	frontend/src/styles/global.css
2026-04-16 17:30:26 +02:00
m
82ae2f22e5 Merge Phase E: Persistent deadline management UI 2026-04-16 17:29:52 +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
b79ef258ef feat(dashboard): Phase G — logged-in landing page
New /dashboard route serves the authenticated home screen with a
server-rendered payload (no skeleton→fetch waterfall, per design
audit §2.3). / now redirects authenticated visitors to /dashboard
and keeps the marketing landing for anonymous visitors.

- DashboardService aggregates deadline + matter summaries, the next
  7d of Fristen/Termine, and the last 10 akten_events, all scoped
  by the standard office-visibility predicate.
- Dashboard handler splices the JSON payload into dist/dashboard.html
  as window.__PALIAD_DASHBOARD__ so the client paints on first frame;
  client re-fetches /api/dashboard every 60s to stay current.
- Sidebar gains an "Übersicht" group with the Dashboard entry at the
  top; DE/EN i18n keys + traffic-light card styles added.
- Empty-state copy, onboarding hint, and 503 handling keep the page
  intact when DATABASE_URL is unset.
2026-04-16 17:27:42 +02:00
m
89eafc04dd Merge Phase D: Akten CRUD UI 2026-04-16 17:11:07 +02:00
m
c82c85fdcc Merge Phase C: Fristenrechner → DB-backed 2026-04-16 17:11:07 +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
4296da5583 feat(akten): Phase D — Akten (Mandate) CRUD UI
- TSX pages: list, create form, detail with Verlauf/Parteien tabs +
  Fristen/Termine/Dokumente/Notizen placeholders for future phases
- Client TS bundles for each page (search, filter, tab switching, inline
  title edit, party add/remove, delete-confirm modal, collaborator picker)
- Sidebar refactored into groups (Arbeit/Werkzeuge/Wissen/Ressourcen);
  Akten as first Arbeit entry; Fristen/Termine shown disabled with tooltip
- Backend: /api/me, /api/users, /api/akten/{id}/events + AkteService.ListEvents
- Server routes for /akten, /akten/neu, /akten/{id} and tab sub-routes
- i18n: full DE/EN strings for Akten UI + sidebar groups; title attr support
- Lime CTAs (#c6f41c), office badges, status chips, audit-trail feed
- Office-scoped visibility (firm_wide_visible partner-only, delete
  partner/admin-only) gated in UI; backend enforces regardless
- Graceful DATABASE_URL-unset message on list page; no 5xx
2026-04-16 15:27:52 +02:00