F-6 from t-paliad-074 architecture audit. The Gitea repo was renamed
m/patholo → mAi/paliad → m/paliad, but go.mod still declared
`mgit.msbls.de/m/patholo` and every internal import echoed the
pre-rebrand name.
Sweep:
- go.mod: module path → mgit.msbls.de/m/paliad
- All *.go files: imports rewritten via sed
- README.md, docs/design-kanzlai-integration.md: mAi/paliad → m/paliad
- Frontend issue-reference comments (mAi/paliad#N → m/paliad#N) in
i18n.ts, theme.ts, sidebar.ts, app.ts, Sidebar.tsx, PWAHead.tsx,
global.css
Verified: go build/vet/test ./... clean, bun run build clean,
no remaining mgit.msbls.de/m/patholo or mAi/paliad references
outside docs that intentionally describe the rename history.
Backend rename (frontend lands in next commit):
- Migration 026: rename paliad.departments → paliad.partner_units,
paliad.department_members → paliad.partner_unit_members, junction FK
department_id → partner_unit_id, plus all constraints/indexes/policies.
Pre-drop seed re-runs migration 019's logic to capture any users.dezernat
drift, then DROP COLUMN. Adds paliad.partner_unit_events audit table
with RLS (any-authenticated read, global_admin write).
- models.User.Dezernat dropped. Department / DepartmentMember →
PartnerUnit / PartnerUnitMember.
- DepartmentService → PartnerUnitService (file renamed via git mv to
preserve blame). Every mutation now opens a tx and emits a
partner_unit_events row in the same tx (created/updated/deleted/
member_added/member_removed). Update emits before/after snapshots;
Delete emits BEFORE the cascade so the FK still resolves, then
ON DELETE SET NULL keeps the historical row.
- /api/departments/* → /api/partner-units/*. Handlers renamed.
- New /admin/partner-units page handler stub.
- AuditService UNIONs the new partner_unit_events source as a 4th
branch; handler accepts AuditSourcePartnerUnitEvents.
- user_service: drop dezernat from CreateUserInput / UpdateProfileInput
/ AdminCreateInput / AdminUpdateInput. CreateUserInput gains
PartnerUnitID *uuid.UUID — onboarding can pick an initial unit and
the membership row + audit event are inserted in the same tx.
- Settings tab aliases drop dezernat/department.
- Legacy /dezernate and /departments now redirect to
/admin/partner-units (admins only see it; non-admins land on the
forbidden bounce).
go build / vet / test compile clean.
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'
- 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.
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.