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.com → name@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.
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.
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.
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
The Hogan Lovells merger makes the "HoLo" portmanteau obsolete. Paliad
(patent paladin) is firm-agnostic and survives future firm name changes.
- Page titles, logo/sidebar, footer, kostenrechner PDF branding
- All DE/EN i18n strings in frontend/src/client/i18n.ts
- README product line
Unchanged: repo/module/Go import paths, cookie names, Supabase table
names, localStorage keys, package.json name — all remain "patholo" as
internal identifiers. HL footer reference stays pending the post-merger
domain decision.
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.
PDF export: Branded print layout with patHoLo header, calculation metadata
(streitwert, date, VAT, selected instances), and disclaimer footer. Uses
@media print CSS for clean A4 output.
URL sharing: Encodes calculator state (streitwert, VAT rate, enabled instances
with all settings) in URL query params. Parses on load to restore state.
"Copy Link" button writes URL to clipboard and updates browser address bar.
Scenario comparison: "Compare" button snapshots current calculation as
Scenario A, shows side-by-side results panel. User modifies inputs for
Scenario B — results auto-update. Diff card shows cost delta with per-category
breakdown (court fees, attorney fees, etc.) and percentage change. Green/red
color coding for savings vs. cost increase.
All labels i18n'd in DE and EN.
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.
Sidebar layout with collapsible icon rail (64px) that expands to 240px
on hover (150ms delay) or pin (persisted in localStorage). Mobile
(<1024px) uses hamburger FAB with overlay. Active page highlighted.
Login page retains the original Header component.
New: Sidebar.tsx, sidebar.ts
Changed: index/kostenrechner/fristenrechner pages, global.css, i18n
Add GET /files/{filename} route (behind auth) that proxies files from
Gitea raw URLs with in-memory caching. Uses SHA-based cache invalidation:
checks Gitea commit API every 5 min, only re-downloads when file changes.
- internal/handlers/files.go: proxy handler with SHA-based cache
- POST /api/files/refresh: cache-bust endpoint
- GITEA_TOKEN env var for private repo access
- Download card on landing page with i18n DE/EN
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/)