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.
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.
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.
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
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.
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.
- 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
41 courts: UPC Court of Appeal, Central Division sections (Paris/Munich/Milan),
13 Local Divisions, Nordic-Baltic Regional Division, 10 German courts
(LG, OLG, BGH, BPatG, DPMA), EPO (Munich HQ, Haar boards, Rijswijk), and
9 national courts (NL, UK, FR, IT). Addresses verified against official
sources; uncertain details left empty rather than guessed.
New page at /gerichte with search, dual filter pills (type + country),
expandable cards, print-friendly CSS, Supabase feedback (gerichte_feedback).
Migration at docs/migrations/002_gerichte_feedback.sql.
Six bilingual patent-workflow checklists (UPC Statement of Claim, Defence,
Confidentiality Application, Representative Registration; BPatG Nullity;
EPO Opposition) with grouped items, rule references, and tips. Index page
lists cards with regime filter and per-checklist progress; detail page
persists check state in localStorage (patholo:checklist:<slug>), shows a
live progress bar, supports reset and print, and submits feedback via
Supabase checklisten_feedback.
New /links page with 22 curated links across 5 categories:
- Gerichte & Ämter (UPC CMS, EPO, DPMA, BPatG, EUIPO)
- Recherche (Espacenet, DPMAregister, DEPATISnet, Google Patents, WIPO)
- UPC (Rules of Procedure, Fees, Practice Directions)
- Gesetze (PatG, EPÜ, UPCA, GKG, RVG, ZPO via dejure.org)
- HL Intern (placeholder links)
Features:
- Client-side category filter tabs
- "Link vorschlagen" button with modal form (POST /api/links/suggest)
- Per-card feedback icon with modal (POST /api/links/feedback)
- Pending suggestion count badge
- Full DE/EN i18n support
- Static link data served via GET /api/links (Go map)
- Supabase PostgREST integration for suggestions/feedback storage
- Sidebar nav entry with chain-link icon
Supabase migration in docs/migrations/001_link_suggestions.sql
(needs to be applied on ydb.youpc.org before collaborative features work).
- New /glossar page with 73 bilingual patent law terms across 5 categories
(Litigation, Prosecution, UPC, EPA, General)
- Client-side instant search filtering both DE and EN columns
- Category filter pills for quick narrowing
- Suggest-a-term button opens modal form for new term submissions
- Per-term feedback icon for correction suggestions
- Suggestions stored in Supabase (glossar_suggestions table with RLS)
- Go API: GET /api/glossar (terms JSON), POST /api/glossar/suggest
- Full DE/EN i18n support, responsive layout, print-friendly
- Added to sidebar nav, landing page tools section, and build pipeline
New /downloads route behind auth with Sidebar, i18n DE/EN,
and download card for HL Patents Style.dotm. Structured so
adding more files is a one-liner in the files array.
Client-side i18n system with localStorage persistence:
- Shared i18n module (frontend/src/client/i18n.ts) with 120+ translation keys
- Language toggle buttons in header on all pages (including login)
- data-i18n attributes on all static translatable elements
- t() function for dynamically rendered content (calculator results, timeline)
- onLangChange callbacks re-render dynamic content on language switch
- Date formatting adapts locale (de-DE / en-GB) per language
- Replaces old dual-display pattern (card-en spans) with single-language switching
Go deadline engine (internal/calc/):
- 9 proceeding types: UPC (INF/REV/PI/APP), DE (INF/NULL), EPA (OPP/APP/GRANT)
- ~50 deadline rules with durations, parties, rule references
- German federal holiday computation (Easter via Anonymous Gregorian)
- Weekend/holiday adjustment with transparency (original vs adjusted dates)
- 8 unit tests covering holidays, adjustment, and full deadline chains
Frontend (Bun/TSX):
- 3-step wizard: select proceeding → enter date → view timeline
- Visual timeline with party badges, rule references, adjustment warnings
- Print-friendly layout
API: POST /api/tools/fristenrechner (protected, JSON)
GET /api/tools/proceeding-types (protected, JSON)
Route: GET /tools/fristenrechner (protected page)
Home page: Added "Werkzeuge" section with cards linking to both tools
Replace Go HTML template rendering with a Bun + TSX build-time static
site generator. Go backend becomes API-only for auth.
Frontend:
- Custom JSX-to-HTML-string factory (zero dependencies)
- TSX components for Header, Footer, index page, login page
- Client-side login.ts handles tab switching and fetch()-based auth
- Bun bundler compiles client JS, build.ts renders pages to dist/
Backend:
- Auth handlers return JSON (POST /api/login, POST /api/register)
- Login page served as static HTML from dist/
- Static assets served from /assets/ (public)
- Auth middleware unchanged (cookie check, redirect to /login)
- Removed template parsing and renderPage
Dockerfile:
- 3-stage build: Bun frontend -> Go backend -> alpine runtime
- Frontend dist copied to /app/dist in final image
Removed: templates/, static/css/ (replaced by frontend/)