Design for zero-overdue SLO, per-user bundled digests (one email per slot
per local-day), DRINGEND evening escalation, and global-admin escalation
on overdues. Includes the actual TZ root cause (alpine container has no
tzdata; LoadLocation silently falls back to UTC) and the embed-tzdata fix.
Awaiting m's go/no-go before implementation.
Survey-only pass across the authenticated paliad surface as test admin
on Playwright at 1280×900 + 375 mobile spot-checks + DE/EN toggle.
Top 10 (best value-per-effort):
1. Strip "Hogan Lovells"/"HL" from public surface (landing, downloads)
2. Pick lime as the single primary green; retire forest-green
3. "Projekt archivieren" red → neutral (reversible, not destructive)
4. /admin/team search input has overlapping placeholder text (visible bug)
5. fristen.field.project.choose raw i18n key on /deadlines/new
6. Activity log leaks project_type_changed + "Type case → litigation"
7. lang="de" on date and time inputs (mm/dd/yyyy + 09:00 AM in DE UI)
8. "Akte" → "Projekt" residue on /deadlines + /appointments
9. Office values lowercased no-umlaut on /projects/{id}/team
10. Project tabs use href="#" — middle-click broken
Plus 40 other findings ranked by severity (broken/friction/polish) and
effort (≤30min/1-2h/half-day+). Suggested 5-PR batching.
41 screenshots in tests/screenshots-polish-2026-04-27/ covering every
sidebar entry + project detail tabs + DE/EN + mobile.
No code changes. Implementation tasks dispatched separately by head.
Conflation: paliad.users.role was simultaneously job title (display only)
and global permission ('role=admin' checks across Go/SQL/JS). m wanted
to set his real job title ('Counsel Knowledge Lawyer') without losing
admin access — the t-paliad-050 admin-team UI even rejected role='admin'
on edit, so any UI-driven update silently demoted m.
Per m's three-axis principle ("firm roles are not project roles are not
tool roles"), this lands TWO orthogonal columns:
* paliad.users.job_title — free text, NULL allowed, display only.
NEVER gates anything in code or SQL.
* paliad.users.global_role — CHECK ('standard'|'global_admin'),
default 'standard'. The only thing that gates ops.
Migration 023:
* Drops NOT NULL + 'associate' default off the legacy role column
* Promotes role='admin' rows to global_role='global_admin'; clears
their role text; sets m's job_title='Counsel Knowledge Lawyer'
* Renames role -> job_title with CHECK (job_title IS NULL OR <> '')
* Replaces can_see_project body with global_role='global_admin'
* CASCADE-rebuilds every RLS policy under canonical English names —
with the historic u.role IN ('partner','admin') gates simplified
to u.global_role='global_admin' only (job_title NEVER gates)
Code surface:
* internal/models/models.go: User.Role -> User.JobTitle (*string) +
User.GlobalRole (string)
* internal/services/user_service.go: bootstrap (first row promoted to
global_admin via pg_advisory_xact_lock(7346298141), unchanged constant);
UpdateProfile drops role, accepts job_title only; AdminUpdateUser adds
global_role with last-admin demotion guard (ErrLastGlobalAdmin);
IsAdmin reads global_role
* Other services (dashboard/agenda/appointment/project/deadline/
department/party/note/checklist_instance): pass user.GlobalRole into
visibility predicates; partner-or-admin gates simplified to
global_admin only
* Handlers: drop now-impossible ErrAdminBootstrapOnly cases;
admin_users handles ErrLastGlobalAdmin -> 409
* department_service: SQL u.role -> u.job_title, DepartmentMember.Role
-> JobTitle (*string)
Frontend:
* /api/me + Me interfaces ship {job_title, global_role}
* Onboarding form: 'Berufsbezeichnung / Job title' (job_title)
* Settings + admin-team forms: same renames + i18n updates
* Admin-team: new 'Berechtigung / Permission' column with
'Standard'|'Global Admin' badge + dropdown editor; last-admin
demotion guard at the UI layer
* Sidebar admin-section reveal: me.global_role==='global_admin'
* deadlines/deadlines-detail/projects-detail/notes: partner-as-permission
gates dropped, only global_admin grants those operations
Tests:
* user_service_test: bootstrap promotes first user to global_admin,
subsequent default to standard; AdminUpdateUser refuses to demote
the last global_admin; IsAdmin reads global_role
Migration applied to ydb 2026-04-27. Live state verified:
* m: job_title='Counsel Knowledge Lawyer', global_role='global_admin'
* tester: job_title=NULL, global_role='global_admin'
* 29 stub colleagues: job_title='associate', global_role='standard'
Conflation today: paliad.users.role is simultaneously job title (display only),
global permission (`role='admin'` checks across Go/SQL/JS), and not-quite-but-
sort-of project_teams.role (already separated). m wants to record his real job
title ("Counsel Knowledge Lawyer") without losing admin access — the existing
admin-team UI even rejects role='admin' on edit, so any UI-driven update
silently demotes him.
Design proposes:
- Rename paliad.users.role -> paliad.users.job_title (free text, NULL allowed)
- Add paliad.users.global_role (CHECK IN ('standard','global_admin'),
default 'standard')
- Single migration 023 does the rename, populates global_role from the old
role, fixes m to job_title='Counsel Knowledge Lawyer', updates
can_see_project, rebuilds RLS policies
- Inventory of every role='admin' call site across services/handlers/
migrations/frontend bucketed by what migrates vs. what stays
- Keeps the existing 'partner' gate as job_title-driven (already broken in
prod — "Partner" capital-P vs lowercase 'partner' check; documented as
out-of-scope follow-up)
- Bootstrap rule (first user becomes admin) keeps the same advisory lock,
flips global_role instead of role
- API surface: /api/me returns both fields; admin-team UI gets a Permission
column with a global_role dropdown + last-admin demotion guard
Awaiting m greenlight before implementation phase.
Design only — no code changes. Five-slot bottom bar for phones (<768px),
center slot opens slide-up Quick-Add sheet (Frist / Termin / Projekt),
right slot reuses the existing mobile sidebar drawer. Tablets and
desktop unchanged. Awaiting m's review before implementation.
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).
- 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.
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).
Implements docs/design-kanzlai-integration.md §8 Phase A.
Schema (paliad.*):
- users (extends auth.users) with office, practice_group, role
- akten with visibility columns: owning_office, collaborators uuid[],
firm_wide_visible (per design §2)
- parteien, fristen, termine, dokumente, akten_events, notizen
(polymorphic notes; notizen_exactly_one_parent CHECK)
- proceeding_types, deadline_rules, holidays (reference data)
- 4 feedback tables re-namespaced from public.* into paliad.*
(handler swap to direct DB is a follow-up; old public tables stay
intact for now and continue serving via PostgREST)
Visibility (paliad.can_see_akte):
- single SQL function, used by every RLS policy
- predicate: firm_wide_visible OR owning_office matches user's office
OR auth.uid() ∈ collaborators OR user is admin
- mirrored at app layer in Phase B (defense in depth)
RLS (real, not permissive):
- akten: visibility predicate; insert restricted to own office or admin;
delete restricted to partners + admins
- parteien/fristen/dokumente/akten_events: inherit via can_see_akte(akte_id)
- termine: personal (akte_id NULL) visible only to creator; Akte-linked
follow visibility predicate
- notizen: paliad.notiz_is_visible() resolves polymorphic parent
- reference tables: SELECT for any authenticated user
- users: SELECT all; UPDATE/INSERT only self
- feedback tables: INSERT for any authenticated user (write-only)
Seed data (ported from KanzlAI seed_upc_timeline.sql):
- 7 proceeding_types (INF, REV, CCR, APM, APP, AMD, ZPO_CIVIL)
- 40 deadline_rules (32 UPC + 4 ZPO + 4 cross-type appeal spawns)
including conditional logic: Reply rule code (RoP.029b → 029a) and
Rejoinder duration (1mo → 2mo) flip when CCR active
- 55 holidays (DE federal 2026/2027 + UPC summer 2026 + UPC winter 26/27)
Indexes per audit §3.3 + visibility-predicate hot paths:
- akten: (status, owning_office), (owning_office), partial on
firm_wide_visible, GIN on collaborators
- fristen: (status, due_date), (akte_id)
- termine: (start_at), (akte_id)
- akten_events: (akte_id, created_at DESC)
- notizen: 4 partial indexes per parent type
- users: (office), (role)
Migration tooling:
- golang-migrate/migrate/v4 with embed.FS source
- Migrations live in internal/db/migrations/ (Go embed can't reach
outside the package; this is the conventional Go layout for embedded
migrations)
- Applied at server startup before HTTP listener binds
- DATABASE_URL is optional today (existing knowledge tools work without
DB); becomes required once Phase B services land
- Mock Supabase auth schema for local testing in
internal/db/migrations/_dev/mock_supabase_auth.sql (excluded from
embed pattern by the underscore prefix)
Other changes:
- Dockerfile: bump golang to 1.24, copy go.sum (audit §2.9), rename
binary patholo → paliad
- docker-compose.yml: add DATABASE_URL passthrough
- README.md: rewritten to reflect Paliad brand + Phase A migration system
Verified locally:
- 11 migrations applied cleanly against postgres:16-alpine
- RLS enabled on all 15 paliad.* tables (verified via pg_class.relrowsecurity)
- Visibility predicate verified with 4-case scenario:
- Alice (Munich associate): sees Munich + firm-wide + collab-on (t f t t)
- Bob (Düsseldorf associate): sees Düsseldorf + firm-wide + collab-on (f t t t)
- Carol (Munich partner): sees Munich + firm-wide only (t f t f)
- Anonymous: sees firm-wide only (f f t f)
- migrate down + re-up cycle clean (initial 007 down had ordering bug,
fixed: drop policies before referenced function)
- Existing endpoints (/, /login) return 302 + 200 — no regressions
Five changes from athena's review (paliad/athena → paliad/cronus):
1. §2 rewritten — office-scoped visibility from day one (NOT firm-wide).
- paliad.users adds: office (required), practice_group (optional), role
- paliad.akten adds: owning_office, collaborators uuid[], firm_wide_visible
- SQL function paliad.can_see_akte(akte_id) used by every RLS policy
- Visibility predicate: own office OR collaborator OR firm_wide OR admin
- Real (not permissive) RLS policies enforced from Phase A
- Defense in depth: app-layer ListVisibleForUser mirrors the predicate
- Onboarding flow added (Phase D) so users self-identify office on signup
2. Mandate → Akten throughout (German end-to-end):
- Tables: paliad.akten / parteien / fristen / termine / dokumente /
akten_events / notizen
- Go structs: Akte, Partei, Frist, Termin, Dokument, AkteEvent, Notiz
- URLs: /akten, /akten/[id], /akten/[id]/{verlauf|fristen|...}
- UI: "Akten", "Aktenverwaltung", "Zur Akte speichern" CTA on Fristenrechner
- Naming convention table added in §3
3. §9 risk added: Outlook/Exchange CalDAV is limited; Phase F ships with
CalDAV only (verified against dav.msbls.de + iCloud); long-term plan is
Phase K = EWS / Microsoft Graph backend behind same sync abstraction.
4. Compliance/IT-approval unknown removed from §9 (m handles out of band).
5. Single-tenant risk replaced by visibility-model risk (now the
security-critical layer); Phase A and B both gain Opus design reviews
on the visibility predicate; Phase B integration test requires 3 users
in 2 offices; pre-Phase J pen-test pass added.
Effort: 52h → 56h (Phase A +2h for visibility model, Phase D +2h for
onboarding + collaborator UI). Total with design ~59h, ~2-3 weeks.
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).
Comprehensive design for two interactive tools:
- Prozesskostenrechner: DE (LG/OLG/BGH/BPatG), UPC, and EPA cost estimates
- Fristenrechner: Patent deadline calculator with holiday adjustment
Covers UI layout, data models, API contracts, calculation logic,
fee tables (GKG/RVG/PatKostG/UPC/EPA), deadline rules for all
proceeding types, and phased implementation plan.
Key differentiator: EPA proceedings coverage (not in KanzlAI).