Three rename leftovers from t-paliad-025 fixed in one shot:
1. TSX/TS element ID mismatches — every page that worked via getElementById was
broken because the client TS was renamed (e.g. project-title) but the TSX
still used the German id (akte-title), so $() / getElementById would throw
"missing element". Renamed `akte-*` → `project-*`, `termin-akte-*` →
`termin-project-*`, `frist-akte-*` → `frist-project-*`, `new-instance-akte`
→ `new-instance-project`, `frist-filter-akte` → `frist-filter-project`,
`termin-filter-akte` → `termin-filter-project` across all affected TSX.
2. Migration 020 idempotency — every ALTER TABLE/FUNCTION/COLUMN now lives in
a DO $$…EXCEPTION WHEN undefined_table/column/function THEN NULL block.
Production already has English names (manually patched), and the rewritten
migration 018 creates English names directly on a fresh DB; the old
non-defensive 020 would have failed in both scenarios. Down migration
wrapped the same way for symmetry.
3. PostgREST endpoint names — `checklists_feedback` and `courts_feedback`
referenced tables that don't exist; migration 020 renames the source
tables to `checklist_feedback` / `court_feedback` (singular, matching
`link_feedback`). Handlers now point at those. `glossary_suggestions`
reverts to `glossar_suggestions` — that table lives in the shared public
schema (pre-paliad era) and is not under our migration control.
Verified: go build / go vet / go test / bun run build all clean. Migration 020
dry-runs clean against current production state inside a transaction.
knuth's rename changed TS/TSX filenames but left <script src> tags
pointing at old German JS names (akten-neu.js, fristen.js, termine.js,
glossar.js, einstellungen.js, gerichte.js, checklisten.js). These 404'd
in production.
knuth's rename (t-paliad-025) changed all Go code and URLs to English
but forgot the DB migration. Production tables still German (fristen,
termine, projekte etc.) while code references English names (deadlines,
appointments, projects). This caused reminder_service to fail with
'relation paliad.deadlines does not exist'.
t-paliad-030. Adds `/agenda` — a single page that merges every visible
deadline and appointment into a day-grouped timeline, the third overview
surface alongside Dashboard and the per-resource lists.
- AgendaService: merges paliad.deadlines + paliad.appointments, gated by
the same team-membership predicate used everywhere else; personal
appointments stay creator-only. Items are sorted by date and tagged
with urgency (overdue / today / tomorrow / this_week / later) so the
client can apply the traffic-light colours without re-deriving buckets.
- GET /api/agenda?from&to&types and GET /agenda with the same server-side
hydration pattern as /dashboard (JSON payload spliced into the shell so
the timeline paints on first frame).
- Frontend: agenda.tsx + client/agenda.ts render a day-grouped timeline
with type/range chips; filters round-trip through the URL.
- Sidebar entry under "Übersicht"; DE+EN i18n across all new keys.
Adds a sidebar-wide search bar (t-paliad-026) that hits a single GET
/api/search?q=... endpoint returning grouped results. Static content
(glossary, courts, link hub, checklist templates) is scanned in memory
against the curated Go slices; DB content (projects, deadlines,
appointments, checklist instances, users) is visibility-gated through
the same predicates the normal list endpoints use.
Frontend: new sidebar.ts-owned controller debounces 200ms, renders a
grouped dropdown, supports "/" to focus, Escape/arrows/Enter for
navigation, mobile-full-width overlay, and highlights matches.
Adds a hardcoded changelog (internal/changelog) served via
GET /api/changelog and /api/changelog/unseen-count?since=<iso>, a
/changelog page that renders entries newest-first, and a sidebar
"Neuigkeiten" link with a lime badge showing the count of unseen
entries since the caller's last visit (localStorage stamp).
- internal/changelog: Entry struct, 11 pre-populated entries covering
everything shipped so far (Dashboard, Projects/Deadlines/Appointments,
CalDAV, Checklists v2, Glossary, Courts, Invitations, Settings,
Paliad rename, and the changelog itself).
- Handler: public via auth-gated protected mux. Lexicographic string
compare treats YYYY-MM-DD entries and ISO 8601 cutoffs symmetrically.
- Sidebar: new sidebar-changelog link before the Einladen button; the
badge is populated by a fetch on every page load, suppressed on
/changelog itself to avoid flash, and cleared on visit by stamping
localStorage in changelog.ts's DOMContentLoaded handler.
- i18n: DE + EN keys for nav, page chrome, and tag labels.
- Unit tests for sort order, copy semantics, and same-day cutoff.
Task: t-paliad-027
Unauthenticated bookmark hits on old German URLs (/akten, /fristen, …)
should 301 to the new English path directly. Previously the redirects
lived under the auth middleware, so bookmarked URLs triggered a 302 →
/login → (after login) → 301 round-trip. Registering them on the outer
mux gives the expected one-hop 301.
Paliad prod data lives on the youpc Supabase (100.99.98.201:11833,
search_path=paliad,public), not on the flexsiebels Supabase that
${SUPABASE_AUTH} resolves to. Next session will pick this up on load
and can run schema work against the correct DB.
1. **Remove Billing-Referenz from the client create form.** Per m: the field stays
in the DB (projekte.billing_reference column) but no longer in the UI. Dropped
the input + label from akten-neu.tsx, the payload write from akten-neu.ts, and
the projekte.field.billing_reference i18n keys (DE + EN).
2. **Add a Notizen (Notes) free-text field to project create + detail at every level.**
Uses the existing projekte.description column (added in migration 018 — nullable
text). Not to be confused with the polymorphic Notizen feature (threaded notes
per projekt/frist/termin), which stays as-is.
- akten-neu.tsx: textarea (rows=4) inserted above the Status select, rendered
for every type (not type-conditional). akten-neu.ts: payload.description set
on submit when non-empty.
- akten-detail.tsx: new description block between header + tabs with
akte-description-display (read) + akte-description-edit (textarea, edit mode).
Edit/save flow on initTitleEdit extended to also PATCH description. Batch PATCH:
only sends keys that actually changed.
renderHeader() populates both, tags the wrapper data-empty=1 when nothing set
(CSS can hide when empty if desired).
- i18n: projekte.field.description / projekte.field.description.placeholder /
projekte.detail.description.heading in DE (Notizen) + EN (Notes).
go build/vet/test + bun run build all clean.
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.
- 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.
**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.
- 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.
- 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.
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.
- 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.
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.
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).
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.
- 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.
- 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.
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.