Wraps up the post-rename cleanup arc. Records what shipped, what's still
open (none blocking), and the rationale for skipping Bug 10 (browser-
emitted console error, not suppressible from JS).
Bug 7 — /whatsnew was bare 404. Sidebar uses /changelog (canonical) but
users typing /whatsnew from memory hit the not-found chrome. Added
/whatsnew → /changelog as a 301 to internal/handlers/redirects.go,
following the existing legacy-redirect pattern. Wired on the OUTER mux so
unauthenticated bookmarks redirect one-hop instead of round-tripping
through /login. /search left as-is per the brief — sidebar's global-search
overlay is the live UX, /search would only be hit via typo and falls back
to the chromed 404 from t-paliad-037.
Bug 8 — /settings/caldav worked (200 → 301 → /settings?tab=caldav) but
/settings/notifications, /settings/dezernat, /settings/profile all 404'd.
Tabs themselves were fine in-page; only the deep-link form was broken.
Replaced the single CalDAV-only handler with a generic /settings/{tab}
redirector backed by a slug→canonical map that accepts both the German
tab IDs the client TS understands (profil, benachrichtigungen, dezernat)
and intuitive English aliases (profile, notifications, department).
Unknown slugs fall back to /settings (default tab) instead of 404 so
typos don't break.
Bug 10 — login form 401 console replay: skipped per brief permission.
Reproduced in Playwright; the console message is the browser's automatic
"Failed to load resource: the server responded with a status of 401"
emitted by the network stack itself. login.ts has no console.error call.
The only workarounds (server returns 200 with {ok:false}, or 422 instead
of 401) either compromise the security pattern or don't actually suppress
the browser log. Documented in the smoke delta report.
Verified: go build/vet/test clean, bun run build clean.
m reported /projects/{id} loaded the chrome and tabs but every panel was
empty even with deadlines/appointments/team rows that should render.
Console error: "Cannot read properties of null (reading 'length')" at
projects-detail.js — the Project Detail page expects every list endpoint
to return [] but at least two were returning literal JSON null.
Reproduced via the in-page fetch console:
/api/projects/{id}/parties → 200, body: "null"
/api/projects/{id}/children → 200, body: "null"
/api/projects/{id}/deadlines → 200, body: "[…]" (had data, fine)
/api/projects/{id}/team → 200, body: "[…]" (had data, fine)
Root cause: every list service in internal/services declared its result
as `var rows []models.X` and returned that to the handler, which
encoding/json marshals as `null` when the SELECT returns zero rows
(nil slice, not empty slice). Most endpoints happen to have data so
the bug stayed dormant until t-paliad-038 hit /projects/{id} where
parties + children are commonly empty.
Fix at the source — every list service that JSON-marshals to a client
now initialises `rows := []models.X{}` so the encoder produces `[]`:
party_service ListForProjekt
project_service List, ListAncestors, BuildTree, GetTree
(ListChildren goes through List)
deadline_service List + ListForProjekt
appointment_service List + ListForProjekt
note_service ListForProjekt
checklist_instance_service ListForProjekt
team_service List
department_service List + ListMembers + ListWithMembers
caldav_service was deliberately left alone — its lists are admin-only
debug surfaces, not user-facing tab fillers, and changing them would
mix scopes.
Belt-and-braces on the client too — projects-detail.ts now coerces every
`await resp.json()` for an array endpoint with `?? []` so a future
service regression can't crash the page.
Verified: go build/vet/test clean, bun run build clean.
URGENT bug: /deadlines/{id} rendered "Frist nicht gefunden oder keine
Berechtigung" while the underlying /api/deadlines/{id} returned 200, and
/deadlines list showed "Invalid Date" in the date column.
Root causes — same class as t-paliad-038, this time on deadlines and
appointments client TS:
1. parseFristID/parseTerminID still checked URL prefix "fristen"/"termine".
After t-paliad-025 renamed pages to /deadlines and /appointments,
parts[0] no longer matched → null id → notfound branch fired before any
API fetch. Renamed to parseDeadlineID/parseAppointmentID with the
correct "deadlines"/"appointments" prefix.
2. fmtDate in deadlines.ts blindly appended "T00:00:00" to the API's
due_date string. After the v2 schema, the API returns full ISO
datetime ("2026-04-22T00:00:00Z"), and "...ZT00:00:00" is invalid →
"Invalid Date". Guarded both fmtDate and urgencyClass with
iso.length === 10 / iso.slice(0, 10).
3. Half-renamed variables (`let allDeadlines` declared, `allFristen`
used; `let deadline`, `frist` referenced). Worked at runtime only
because the undeclared identifier became a non-strict global. Cleaned
up to use the declared English names everywhere.
Lockstep DOM ID + variable rename in client TS + matching TSX:
- frist-* → deadline-* (deadlines-detail, deadlines, deadlines-new,
deadlines-calendar)
- termin-* / termine-* → appointment-* / appointments-* (appointments-detail,
appointments, appointments-new, appointments-calendar)
- fristen-body/empty/unavailable → deadlines-* (list page)
- termine-body/empty/unavailable → appointments-* (list page)
- frist-cal-grid / termin-cal-grid → deadline-cal-grid /
appointment-cal-grid (calendars)
- loadFristen/loadTermine/loadAkten/loadFrist/loadTermin/loadAkte →
loadDeadlines/loadAppointments/loadProjects/loadDeadline/loadAppointment/loadProject
- deadlines.ts: dropped unused projekt_office field from Deadline interface
- appointments.ts: dropped unused projekt_office field from Appointment
interface
Dashboard cleanup — Go service was still emitting `projekt_ref`:
- internal/services/dashboard_service.go: UpcomingDeadline /
UpcomingAppointment / ActivityEntry json+db tags `projekt_ref` →
`project_reference`; SQL aliases `AS projekt_ref` → `AS project_reference`.
- frontend/src/client/dashboard.ts: interfaces switched to
project_reference; activity link href /projects/{id}/fristen →
/deadlines, /termine → /appointments (the German per-project subpaths
were dead — t-paliad-038 already renamed projects-detail tabs).
i18n key strings (fristen.*, termine.*) intentionally kept in German per
the t-paliad-025 convention (frontend default language is German). CSS
class names (frist-row, frist-due-chip, frist-cal-cell, termin-dot,
termin-type-*, akten-table-wrap) untouched — separate stylistic cleanup,
no IDs are referenced in CSS so the rename is safe.
Verified: go build/vet/test clean, bun run build clean, dist HTML
contains only the new English IDs (remaining German strings are i18n
keys and product-name CSS classes).
Root cause of the URGENT bug: parseAkteID() in
frontend/src/client/projects-detail.ts only accepted /projekte/{id} and
/akten/{id} URL prefixes. After t-paliad-025 renamed pages to /projects/{id},
parts[0] === "projects" failed both checks → null id → notfound branch
fired before any /api/projects/{id} fetch. The 200 from curl was real;
the page just never asked.
Fix: parseProjectID() now reads /projects/{id}. Old bookmark tab slugs
(verlauf, parteien, fristen, …) are mapped to their English successors so
deep links don't silently fall back to the default tab.
Bundled cleanup — every per-project subpath the client TS still hit was a
404 because the rename only touched top-level routes. Lockstep rename of
URLs, function names, DOM IDs, and the TabId union in projects-detail.ts
+ projects-detail.tsx:
- /api/projects/{id}/parteien|fristen|termine|notizen|checklisten →
/parties|deadlines|appointments|notes|checklists
- loadParteien/loadFristen/loadTermine/loadAkte/parseAkteID →
loadParties/loadDeadlines/loadAppointments/loadProject/parseProjectID
(the old loadParteien/loadFristen/loadTermine bodies even assigned to
undeclared `parteien`/`fristen`/`termine` — would have thrown
ReferenceError as soon as the catch branch ran)
- DOM IDs: akten-detail-* → project-detail-*, parteien-* → parties-*,
partei-* → party-*, project-fristen-* → project-deadlines-*,
project-termin(e)-* → project-appointment(s)-*,
project-checklisten-* → project-checklists-*, akten-events-* →
project-events-*, kinder-* → children-*, projekt-breadcrumb →
project-breadcrumb, frist-add-link → deadline-add-link,
termin-add-btn → appointment-add-btn
- Tab slugs in URL + data-tab + tab-* IDs: verlauf/kinder/parteien/
fristen/termine/notizen/checklisten →
history/children/parties/deadlines/appointments/notes/checklists
- frist-add-link href: /projects/{id}/fristen/neu →
/projects/{id}/deadlines/new
Sweep across the rest of frontend/src/client/:
- notes.ts: NotizParentType → NotesParentType, "frist"/"termin" →
"deadline"/"appointment", baseURL paths /…/notizen → /…/notes; updated
callers in deadlines-detail.ts and appointments-detail.ts.
- deadlines-new.ts: undeclared `akten` reference (loadAkten was assigning
to a never-declared name) replaced with `projects`; URL /…/fristen →
/…/deadlines; path-parsing of /akten/{id}/fristen/neu rewritten as
/projects/{id}/deadlines/new; preselectedAkteID → preselectedProjectID;
Project.aktenzeichen field (no longer emitted by API) → reference.
- fristenrechner.ts: bulk endpoint /…/fristen/bulk → /…/deadlines/bulk;
request body { fristen } → { deadlines } (server expects "deadlines"
key); ProjectOption interface now uses reference instead of
aktenzeichen.
- deadlines.ts, appointments.ts, deadlines-detail.ts, appointments-detail.ts,
checklists-detail.ts, appointments-new.ts: Project interface field
aktenzeichen → reference (the API returns "reference"; the old field
rendered as undefined in select options and detail headers).
i18n key strings (akten.detail.*, projekte.*, fristen.*, termine.*,
checklisten.*, notizen.*) intentionally kept in German per the
t-paliad-025 convention. CSS class names (frist-row, akten-table-wrap,
termin-dot, etc.) untouched — separate stylistic cleanup.
Verified: go build/vet/test clean, bun run build clean, dist HTML +
bundled JS contain only the new English IDs (remaining German strings
are i18n keys).
Four bugs from tests/smoke-auth-2026-04-25.md.
Bug 4 — Dashboard activity log leaked raw i18n keys. Root cause was a mix
of three issues:
- Go services wrote German event_types (frist_created, termin_*,
projekt_*, notiz_created, checkliste_*) — no matching i18n key.
- i18n.ts only had keys for legacy `akte_*` types, none for what was
actually being written.
- The dashboard renderer always rendered `e.title` (a static label like
"Project angelegt") as a trailing detail, duplicating the action verb.
Old `akte_created` rows had English titles ("Akte created") that
bled into German output.
Switched all event_type writes to English (deadline_*, appointment_*,
project_*, note_created, checklist_*, deadlines_imported). Moved dynamic
text out of `title` into `description` for status_changed and
deadlines_imported so the static label/description split is consistent.
Added i18n keys for both new English types AND legacy German types so
historical project_events rows render cleanly. Dashboard now prefers
description over title; falls back to title only for events with no
i18n match (defensive for any unknown legacy kinds).
Bug 5 — /deadlines and /appointments matter-filter dropdowns showed raw
keys `fristen.filter.project.all` / `termine.filter.project.all`. The
client TS referenced English-prefix keys that didn't exist; the existing
keys use `fristen.filter.akte.*` / `termine.filter.akte.*`. Updated the
client refs to match the existing keys (kept i18n key namespace stable
to avoid touching every other reference).
Bug 6 — /api/departments?include=members returned 500. Reproduced via
curl: ListWithMembers (and ListMembers) used `LEFT JOIN paliad.users` on
a member.user_id that FKs auth.users — pre-onboarding members produced
NULL u.email/display_name/office/role, which sqlx can't scan into the
non-pointer string fields. Switched both to INNER JOIN; unonboarded
members are skipped (correct UX — without a profile there's nothing to
render anyway).
Bug 9 — Bare `404 page not found` on unknown auth-gated paths
(/whatsnew, /search, /settings/notifications, etc). Added a chromed 404
page (frontend/src/notfound.tsx) with sidebar + friendly card + "back
to dashboard" CTA, plus a catch-all handler on the protected mux that
serves it with HTTP 404 (and JSON 404 for /api/* misses). Anonymous
visitors keep being redirected to /login by the auth middleware before
the catch-all runs, so no separate marketing-shell variant needed.
Verification:
- go build ./... + go vet ./... + go test ./... clean
- bun run build clean (notfound.html + notfound.js produced)
- Visual checks pending after deploy
Bug 1 (smoke-auth-2026-04-25.md) had a third symptom beyond the RLS
function bodies and the visibilityPredicate `::uuid[]` issue:
/api/deadlines/summary and /api/appointments/summary returned 500 with
`sqlx: missing destination name this_week in *services.SummaryCounts`.
Cause: SummaryCounts (deadline) and AppointmentSummaryCounts had only
`json:` tags. sqlx falls back to the lower-cased field name when no `db:`
tag is present, so `ThisWeek` mapped to `thisweek` — but the SQL aliases
the column as `AS this_week`. Adding `db:"this_week"` (and matching tags
for the other fields) lets sqlx find the destination.
Verified by hitting both endpoints; previously 500 → now expected 200.
Bug 1 in tests/smoke-auth-2026-04-25.md (/api/projects, /api/deadlines,
/api/appointments returning HTTP 500) had a second root cause beyond the
RLS function bodies fixed in 021: sqlx v1.4.0's named-parameter parser
strips one colon from `::uuid[]` while compiling `:user_id` / `:role`
placeholders, producing invalid SQL `:uuid[]` that Postgres rejects with
`syntax error at or near ":"`. The bug was masked by the earlier
`relation "paliad.projekte" does not exist` error.
Replacing `::uuid[]` with the equivalent SQL-standard `CAST(... AS uuid[])`
sidesteps the parser issue without changing semantics. Verified with a
small repro: `sqlx.Named` no longer corrupts the cast.
Only `visibilityPredicate` (the named-bind variant) is affected — the
positional and `?`-placeholder variants don't go through `sqlx.Named`.
Migration 020 renamed paliad.can_see_projekt → can_see_project (and
notiz_is_visible → note_is_visible) via ALTER FUNCTION but never rewrote
the bodies. On production the bodies still queried paliad.projekte /
projekt_teams / fristen / termine / projekt_events — all of which were
dropped or renamed in 018+020. Every RLS-enforced read against the new
English tables exploded with `relation "paliad.projekte" does not exist`,
breaking /api/projects, /api/deadlines, /api/appointments etc.
Same problem for the trigger functions paliad.projekte_sync_path() and
paliad.projekte_rewrite_subtree() — kept their German names and German
bodies; the triggers on paliad.projects still pointed at them.
Migration 021:
* DROP FUNCTION ... CASCADE drops can_see_project / note_is_visible
along with their 21 dependent RLS policies (whose names were still
German on prod: projekte_*, projekt_teams_*, fristen_all, termine_*,
parteien_all, dokumente_all, projekt_events_all, notizen_all,
checklist_instances_*).
* Recreates the two functions with English bodies + English parameter
names and rebuilds every dependent policy under its canonical
English name (matching migration 018).
* Drops the German trigger functions/triggers on paliad.projects and
recreates them as projects_sync_path / projects_rewrite_subtree.
Idempotent on a fresh DB (where everything is already English): the
CASCADE drops the same policies and the recreate produces an identical
end state.
Verified by running the up.sql in BEGIN/ROLLBACK against the actual
youpc prod Postgres — 21 policies dropped, 21 recreated, function
bodies now reference paliad.projects / project_teams / etc.
Refs: tests/smoke-auth-2026-04-25.md (Bug 3, root cause for Bugs 1+2).
Logged-in smoke test against paliad.de as the seeded test admin
(tester@hlc.de). Login flow + every gated route exercised; 17
screenshots and 10 prioritised bugs filed.
Top finding: paliad.can_see_project() still references the renamed-away
projekte/projekt_teams tables, which 500s every RLS-touching endpoint
(/api/projects, /api/deadlines, /api/appointments and the project-detail
page render). Two of today's three new shipments — project tree and
reminder service — cannot be exercised end-to-end as a result. Team
directory works modulo a stray /api/departments?include=members 4xx/5xx.
The marketing landing (`/`) renders the same Sidebar as protected pages, so
`initChangelogBadge()` was firing `GET /api/changelog/unseen-count` on every
anon visit and getting 401. Cosmetic noise + wasted round-trip.
Add an `authenticated` prop to Sidebar (defaults to true, no behavior change
on protected pages) and pass `false` from `renderIndex()`. The badge `<a>`
is omitted server-side; the existing `if (!badge) return` guard in
sidebar.ts naturally skips the fetch when the element is absent — no
client change needed.
Also append a clarifying note under the env-var table in .claude/CLAUDE.md:
"work without DB" doesn't mean "ungated for anon". The knowledge-platform
routes (Kostenrechner, Glossar, etc.) are still behind the auth gate; only
`/`, `/login`, `/logout`, and `/assets/*` are public. Misread by the smoke
tester briefer; spelled out now to prevent recurrence.
Anonymous-surface smoke test of paliad.de — all 11 legacy DE→EN redirects,
9 gated routes, /login form, and / marketing landing healthy. Two minor
findings noted in the report (knowledge platform is auth-gated contrary
to brief expectation; anon / fires a 401 on /api/changelog/unseen-count).
Authenticated flows untested — needs follow-up worker with creds.
Also registers @playwright/mcp in .mcp.json so future smoke runs can use
the /mai-tester skill's mcp__playwright__* tools directly instead of
falling back to a bunx script.
The German→English rename (t-paliad-025) renamed the projects table and
ReminderService struct fields, but the SQL aliases in sendPerFrist /
sendWeekly still spelled `frist_title`, `akte_aktenzeichen`, and
`akte_title`. sqlx.SelectContext could not map them to the
`deadline_title` / `project_reference` / `project_title` `db:` tags, so
every hourly reminder scan returned a "missing destination name" error
and emails silently stopped going out.
This commit:
* renames struct fields AkteAktenzeichen/AkteTitle on fristReminderRow
and weeklyRow to ProjectReference/ProjectTitle and updates the `db:`
tags to project_reference / project_title.
* rewrites the SELECT aliases (deadline_title, project_reference,
project_title) to match.
* propagates the new keys through deliverFristReminder /
deliverWeekly into the email template data and renames the matching
variables in deadline_reminder.html and deadline_weekly.html.
* updates mail_service_test.go fixtures to the new keys.
* adds TestSendPerFrist_ScansCleanly — a live-DB regression test
(skips without TEST_DATABASE_URL) that seeds a project + deadline
and asserts sendPerFrist / sendWeekly scan without error, so a
future tag/alias drift fails CI instead of going silent.
Adds /team page that lists every onboarded Paliad user, grouped by office
(default) or by department, with a free-text search and per-office filter
pills. Each card shows display name, role, primary office (with any
additional offices), department tag, and a mailto: link.
Backend:
- /api/users now also returns additional_offices (column was already on the
model + DB; just missing from the SELECT list).
- /api/departments?include=members returns each department enriched with
its lead user snapshot and the full member list — single fetch for the
"by department" grouping.
- New page handler /team behind the onboarding gate.
Frontend:
- frontend/src/team.tsx + frontend/src/client/team.ts (new) for the page
shell and client-side rendering / filtering.
- New "Team" entry in the Übersicht sidebar group with a users icon.
- DE/EN i18n keys (nav.team, team.*).
- Team-specific CSS for cards, group headers, avatars, and badges.
- Backend: GET /api/projects/tree returns the full visible project tree as
nested JSON with embedded children, open/overdue deadline counts per
node — visibility-scoped via the existing predicate, single round-trip.
- Frontend: new project-tree.ts module renders a collapsible, indented tree
with type icons (client/litigation/patent/case/project), status badges,
deadline-count chips and chevron toggles. Top two levels expand by
default; deeper nodes start collapsed. Expansion state persists in
sessionStorage so toggling list/tree keeps user choices.
- Wired to /projects via the existing Ansicht select (Liste/Baum/Wurzeln);
dedicated tree container coexists with the flat-list table.
- New i18n keys (de/en) + tree styles in global.css (lime accent on hover).
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.