Two-palette swap at :root and :root[data-theme="dark"]; FOUC-prevention
inline <script> in PWAHead reads paliad-theme + paliad-sidebar-pinned +
paliad-sidebar-width from localStorage before the stylesheet loads, so
the page paints in the persisted state from frame one. New theme.ts
client owns the runtime side: cycles auto → light → dark → auto, listens
to prefers-color-scheme while pref="auto", broadcasts change events to
the sidebar toggle so the sun/moon/auto icon stays in sync (incl. on
OS-level theme flips). Sidebar gains a sun/moon toggle below the lang
item with localized aria-label/tooltip describing the next click action.
Surface tokens introduced (--color-surface-{2,muted}, --color-input-bg,
--color-overlay-{faint,subtle,strong,modal}, --color-border-strong,
--shadow-{lg,xl}, --status-{red,amber,green,blue,neutral}-{bg,fg,...},
--tree-icon-{client,litigation,patent,case,project},
--sidebar-scrollbar-{thumb,...,width}); status pills, dashboard cards,
agenda urgency markers, frist-due-chip, akten-status-chip, termin-type
badges all read tokens or get a class-level dark override at the bottom
of global.css. Form inputs render on white in light mode (m: 2026-04-30)
and on a value below --color-surface in dark mode so the well still
reads as depressed below the card panel.
Sidebar scrollbar themed thin + cream-channel alpha with
scrollbar-gutter: stable so the collapsed icon column doesn't shift
when the nav overflows on tall (admin) layouts; .sidebar-icon width
shrinks by var(--sidebar-scrollbar-width) to keep icons centered in
the visible content area.
The pre-paint script also fixes the sidebar-pinned FOUC (maria's add):
sets <html class="sidebar-pinned"> from localStorage before paint, with
sidebar.ts mirroring the class on <html> on every pin toggle so the
new selector :root.sidebar-pinned .has-sidebar tracks the existing
.has-sidebar.sidebar-pinned (body) selector. width is also pre-applied
when within clamp.
Build: bun run build clean (1224 i18n keys, 36 pages).
Smoke: Playwright on /login in both modes — body bg/fg/cards/inputs
read from the right tokens, FOUC script lands in <head> before the
stylesheet, dark→light→auto cycle toggles via the sidebar button.
F-9 from t-paliad-074. Aligns the i18n key namespace with the codebase's
English-language convention; no UI text changes.
Renames in frontend/src/client/i18n.ts (the source-of-truth file):
akten.* -> projects.* (merged with projekte.*; projekte wins on collision,
the more-recently-edited value, per brief)
fristen.* -> deadlines.*
termine.* -> appointments.*
projekte.* -> projects.*
notizen.* -> notes.*
Scope of changes:
- 760 key lines renamed in i18n.ts (380 unique keys × 2 langs)
- 70 akten/projekte suffix collisions resolved by dropping akten.* lines
(140 lines dropped total — projekte values preserved)
- 19 inner-segment fixes (e.g. projects.detail.fristen.add ->
projects.detail.deadlines.add, and template-literal sites like
`tDyn(`fristen.${x}`)` whose suffix begins with ${...})
- 476 caller-side replacements across 27 *.ts/*.tsx files
(literal t() / tDyn() args, template-literal prefixes,
"prefix." string concatenations, data-i18n attributes)
- i18n-keys.ts (generated) regenerated by build.ts: 1218 keys total
t-paliad-078's typed registry + build-time data-i18n scanner caught this
rename was complete: `bun run build` reports "i18n scan: data-i18n
attributes clean", meaning every literal data-i18n attribute in TSX/TS
sources references a key that exists in i18n.ts post-rename.
Out of scope (per brief): backend Go service rename (t-paliad-080 F-4),
URL paths (/akten, /projekte routes still server-side), CSS class names
(akten-table, akten-form, etc.), and German sub-tokens like .akte (label
"Akte:") or .no_akten (the modal hint when no project is linked).
Lime text on the cream/white BG fails WCAG AA. Adds a foreground token that
is midnight by default and lime inside the .sidebar scope (which lives on
midnight). Rewires every text-color use of --color-accent to the new token,
including the double-fallback typo variants. Decoration uses (border, BG,
border-bottom) keep --color-accent (= lime).
mAi/paliad#2 (full dark mode) flips --color-accent-fg back to lime in one
place — no need to revisit every rule.
F-8 from the t-paliad-074 audit. Replaces silent `?? key` fallback with a
typed key surface so drift caught at compile/build time, not in prod.
- New `frontend/src/i18n-keys.ts` (generated): `I18nKey` literal union of
all 1288 keys in `i18n.ts`. Regenerated by `frontend/build.ts` on every
build; written only when content changes (no spurious diffs).
- `t(key: I18nKey)` is now strict — `t("fristn.detail.title")` fails
`tsc --noEmit`. New `tDyn(key: string)` is the explicit escape hatch
for runtime-composed keys (`tDyn(\`fristen.status.${x}\`)`); 27 dynamic
call sites migrated.
- Build-time scan in `build.ts` walks `src/**/*.{ts,tsx}` for literal
`data-i18n` / `data-i18n-placeholder` / `data-i18n-title` attributes
and aborts the build on any value not in the key set. Skips `${...}`
interpolations (can't resolve statically). Applied before bundling so
no artefact ships when an unknown literal is present.
Surfaced and fixed during migration:
- `data-i18n="fristen.save.modal.project"` (fristenrechner.ts:145) →
`fristen.save.modal.akte` — F-04-class bug, would render the raw key.
- `t("termine.field.project.none")` (appointments-new.ts:30) →
`termine.field.akte.none` — same class.
- `t("checklisten.instance.project.open")` (checklists-instance.ts:155)
→ `checklisten.instance.akte.open` — same class.
- 4 duplicate-key entries in `i18n.ts` removed (TS1117): `nav.termine`
and `akten.detail.tab.termine` each appeared twice in DE and twice in
EN with identical values.
Out of scope (per brief): the German-vs-English i18n-key namespace split
flagged as F-9, JSX intrinsic typing, and the `akten` → `projects`
half-rename in checklists-detail.ts. Those stay tsc-noisy until separate
tasks land.
F-2 from t-paliad-074 audit. The inlined visibility predicate had drifted
back into 6 hot-path SQL sites despite the central helper extracted in
t-paliad-058. Consolidating now so future visibility changes (e.g.
Chinese-wall in design v2 §8) only need one edit.
**Sites converted (6):**
- dashboard_service.go:158, 214, 244, 274
- agenda_service.go:138, 204
All six replace `$N = 'global_admin' OR EXISTS (path-walk)` with the
existing `visibilityPredicatePositional("p", 1)` helper. The helper
resolves global_admin via EXISTS on paliad.users — the role string no
longer flows through positional args, removing one foot-gun (typo'd
literal mismatched against bound role) entirely. Equivalence verified
on the live youpc DB:
tester@hlc.de (global_admin, 1 team membership):
old predicate count = 11 new predicate count = 11
standard user (no team):
old predicate count = 0 new predicate count = 0
**No new helper variant added.** The audit suggested
`visibilityPredicateLateral`, but the existing positional helper drops
into the dashboard/agenda WHERE clauses unchanged — adding a redundant
variant would be technical debt. dashboard/agenda do not use LATERAL
JOIN; they use plain WHERE EXISTS in (sub-)SELECT context, which is
already what visibilityPredicatePositional emits.
**Other 4 sites flagged by audit — left intentionally:**
- reminder_service.go:312, 325 are role-restricted (`pt.role = 'lead'`)
membership checks, NOT visibility predicates. Adding a global_admin
shortcut to the lead branch would over-include rows: every global
admin would receive every project's lead-targeted reminder, even with
the `own.escalation_contact_id` override that exists precisely to
avoid that. global_admin already has its own dedicated branch in the
query (`$3 = TRUE AND own.escalation_contact_id IS NULL` at line 328).
- deadline_service.go:422 (`assertCanAdminProject`) is role-restricted
(`pt.role IN ('admin', 'lead')`) and already short-circuits global_admin
at the Go level before the SQL runs (line 413). Both halves correct;
no change needed.
- team_service.go:162 (`IsEffectiveMember`) was dead code with no callers
in the entire repo. "Is this user a structural team member?" and
"can this user see this project?" are different questions; adding a
global_admin shortcut would have conflated them. Deleted instead.
**Test:** new TestVisibilityPredicate_DashboardAgendaForGlobalAdmin in
visibility_test.go seeds a project + deadline + appointment + activity
event with project_teams empty, then asserts a global_admin sees all
four on /dashboard and /agenda while a standard user sees none. Skips
when TEST_DATABASE_URL is unset (matching the existing live-DB tests).
**Pre-existing finding (separate concern):** the live-DB test gate is
currently blocked locally by a stale `public.paliad_schema_migrations`
(version=2, dirty=t) left over from before the schema-pinned tracker
landed. Authoritative `paliad.paliad_schema_migrations` is at version
27, dirty=f. Out of scope for this task; should be filed as cleanup.
Bundle of small audit findings, all doc-only or dead-code:
- F-5: refresh stale escalation-contact comment in models.User —
Settings UI dropdown shipped 2026-04-29 (t-paliad-066).
- F-10: add "OBSOLETED by migration 018" note to migrations 004/005/006
so readers stop hunting for the live shape in obsolete files.
- F-11: document the data-loss semantics of dropping
paliad.partner_unit_events on the 027 down — audit rows are
append-only telemetry, accepted loss on rollback.
- F-15: drop the patholo_session / patholo_refresh cookie fallback
added during the 2026-04-16 rebrand. Active users have long since
been re-authed through the upgrade path; inactive users hit the
normal /login flow.
- F-16: refresh stale /api/departments comment in team_pages.go to
/api/partner-units (renamed in t-paliad-070).
- F-17: move internal/db/migrations/_dev/mock_supabase_auth.sql to
internal/db/devtools/ so a future loosening of the //go:embed
pattern can't accidentally ship the dev-only fixture.
- F-18: update docs/project-status.md "Audit polish-2" entry — the
batch shipped via t-paliad-067 / 068 / 073, follow-ups are now
tracked under the 2026-04-30 re-audit + t-paliad-074.
go build / vet / test clean.
The suggestion + feedback handlers wrote to legacy public-schema tables
(`patholo_link_suggestions`, `patholo_link_feedback`) via Supabase PostgREST.
The patHoLo→Paliad rebrand moved those tables into the paliad schema as
`paliad.link_suggestions` / `paliad.link_feedback` — PostgREST is not
configured to expose paliad on the youpc Supabase, so all three callsites
500'd in prod.
Replace the PostgREST integration with a new LinkService backed by the same
sqlx pool every other paliad service uses. Schema-qualified table names
work directly via DATABASE_URL, the inconsistent supabaseInsert/Count
helpers go away, and the suggestion/feedback handlers now use requireDB
for clean 503s when the pool isn't wired.
handleSuggestionCount keeps its tolerant 0-on-error behaviour so the admin
badge never blocks page render. When DATABASE_URL is unset the count
endpoint returns 0 instead of 503 — knowledge-platform-only deployments
still serve the Link Hub page.
Flagged in t-paliad-074 (F-12).
Migration 027 renamed paliad.departments → paliad.partner_units and
paliad.department_members → paliad.partner_unit_members but two queries in
AdminDeleteUser were missed by the rename sweep, so admin off-boarding
500'd in prod. Update both DELETE/UPDATE statements and the surrounding
comments to match the current schema.
Flagged by ada in t-paliad-074 (F-1).
Read-only audit after the 9-merge push of t-paliad-066..073. Surfaces
18 findings across 7 lenses (service boundaries, naming, frontend↔
backend contract, migrations, tests, dead code, doc drift) plus three
architecture observations carried forward from the 2026-04-18 audit.
Top 3 punch list:
- F-1 (🔴 active): AdminDeleteUser SQL writes to dropped tables
paliad.department_members / paliad.departments. Live production bug,
blocks admin user-delete. user_service.go:768,773. Missed by
t-paliad-070 rename sweep (last touched 2026-04-27, predates rename).
- F-13 (🔴 active): 7 live-DB integration tests skip silently when
TEST_DATABASE_URL unset, no CI exists. Same pattern that masked the
t-paliad-069 reminder bug for ~24h and that hid F-1 above.
- F-2 (🔴 active): visibility predicate inlined in 10 hot-path SQL
sites despite central helper in visibility.go (dashboard/agenda/
reminder/team/deadline service). Inlined sites silently skip the
global_admin shortcut.
No code changes — head sequences dispatch.
Six findings from docs/audit-polish-2-2026-04-29.md DEFER list:
- F-23: hide STATUS column on /deadlines + /projects when every visible
row shares the same status. Toggled at render time via a CSS class on
the table; the column re-appears the moment filters re-introduce
variety.
- F-32: agenda urgency pill now renders only when it disagrees with
the day-bucket heading (e.g. an Überfällig deadline that lands in
HEUTE through a filter quirk). Common case drops the redundant tag.
- F-38: bottom-nav agenda badge already counted overdue+today (the
brief's option (b)); added a localized title + aria-label so the
count's semantics ("X überfällig + Y heute fällig") is no longer
ambiguous.
- F-40: glossary filter chips no longer mix EN+DE — DE shows
"Streitsachen / Erteilungsverfahren / Allgemein", EN keeps
"Litigation / Prosecution / General". Same i18n keys cover the
Suggest-modal category dropdown.
- F-48: /projects/{id}/sub-projects now 301-redirects to the canonical
/children URL via the existing redirects.go mechanism. Added a small
redirects_test.go to lock the alias in.
- F-49: dropped the meta-circular 2026-04-22 "Neuigkeiten / What's New"
changelog entry that referenced "this changelog" itself.
go build/vet/test clean, bun run build clean.
F-25 (mobile tables → card layout) is redesign-class and is scoped at
the bottom of the PR description as t-paliad-074, not implemented here.
Pre-fix `time.NewTicker(time.Hour)` fired every hour from container start,
so a Dokploy redeploy at 13:27:50 produced ticks at HH:27:50 forever —
drifting the user-visible arrival of a 09:00-Berlin digest anywhere in the
09:xx hour, and entirely losing the slot when redeploys happened to land
during the slot's hour (m saw this on 2026-04-29).
Replace the simple ticker with a recompute-per-iteration sleep to the next
HH:00:00 boundary using nextTopOfHour(now). The recompute self-corrects
any clock skew or RunOnce duration rather than accumulating drift.
Add runStartupCatchUp: on boot, fire any user/slot whose hour has already
passed today but has no log row yet. The slot_date dedup makes this safe
(re-firing a logged slot is a no-op). Without this a single mistimed
redeploy still loses a day for affected users.
Tests: TestNextTopOfHour (boundary math, including the 13:27:50 signature
and sub-second offsets), TestNextTopOfHour_AlwaysLandsOnBoundary (fuzz
across an hour of offsets), TestNextTopOfHour_StableAfterRunOnce (confirms
the next fire is HH+1:00 after a fire at HH:00, not HH:00+delay1),
TestSlotPastDueToday (catch-up filter table), and a live-DB
TestRunStartupCatchUp_RecoversMissedMorningSlot covering the redeploy-at-
11:50-Berlin scenario plus dedup on a second startup the same day.
Frontend half of the rename:
- New /admin/partner-units page (admin-partner-units.tsx + .ts) with
full CRUD + member management. Mirrors /admin/team's aesthetic and
uses the same modal pattern. Card on /admin flips from "Geplant"
to "Verfügbar" with ICON_BUILDING and a /admin/partner-units link.
- Sidebar gains a "Partner Units" admin nav item between Team and Audit.
- Onboarding form replaces the free-text Dezernat input with a select
populated from /api/partner-units; submits partner_unit_id which the
backend uses to insert a membership row in the user-create tx.
- Settings: dezernat tab removed entirely (TabName drops to 3). The
read-only "Meine Partner Units" view now lives as a card on the
profile tab. Free-text dezernat input removed from the profile form.
~250 lines of admin-CRUD removed; replaced by ~70 lines of read-only
partner-units summary.
- /admin/team: Dezernat column dropped from the table and the inline
edit row; "Onboard existing account" modal no longer asks for one.
Column count drops from 10 to 9.
- /team directory: groups by structured partner_unit_members only;
drops the free-text fallback grouping and the "Ohne Dezernat" loose
bucket. Single "Ohne Partner Unit" orphan group catches users in no
unit.
- i18n: ~30 dezernat.* + onboarding.dezernat + admin.team.col.dezernat
+ admin.card.departments + team.* keys removed; ~30 partner_unit.*
keys added in DE+EN. "Partner Unit" / "Partner Units" used as a
loanword in DE.
- /api/departments?include=members → /api/partner-units?include=members
in team.ts (the only frontend-side fetch URL referencing the old
endpoints).
go build / vet / test clean. cd frontend && bun run build clean.
DB-backed email-template editor for global_admins, replacing the
"Kommt bald" placeholder. Admins can edit invitation, deadline_digest,
and the shared base wrapper for both DE and EN, preview against sample
data, save with versions, and reset to the embedded default.
Backend:
- Migration 026 adds paliad.email_templates (active row per (key, lang))
and paliad.email_template_versions (append-only, retained 20 deep).
- EmailTemplateService — GetActive falls through to the embedded per-
language file when no DB row, Save validates parse + structural
invariants and writes a version, Reset deletes the active row, Restore
copies a version back. Mutations require DB; reads work without.
- MailService now consults the service for body and subject and falls
back to the embedded default if the active row is malformed at parse
time — a corrupt admin save can never wedge the send path.
- Subjects move from Go (buildDigestSubject + inviteSubject) to
text/template strings stored in the (key, lang) row. Default subjects
ship with a {{/* keep this phrasing */}} comment pointing at the
reminder-redesign doc so the SLO framing rationale survives edits.
- Bilingual templates split into per-language files (invitation.de.html
+ .en.html, deadline_digest.de.html + .en.html, base.de.html + .en.html).
No more {{if eq .Lang}} branching inside templates.
- Handlers under /api/admin/email-templates/* gated by the existing
RequireAdminFunc(users) admin middleware, same shape as /admin/team.
Frontend:
- /admin/email-templates list page — three cards (one per template),
each linking to DE + EN editors with their last-modified status.
- /admin/email-templates/{key}?lang=de three-pane editor — subject + body
textarea + variable docs + actions on the left, sandboxed iframe
preview + version log on the right. 500 ms debounced live preview;
save validates server-side (422 on parse error, surfaced inline).
- admin.tsx flips the Email-Templates card from PLANNED to verfügbar.
- 50 new i18n keys (DE + EN) for the editor surface.
Tests: GetActive fallback path, ValidateTemplate happy + sad paths,
SaveRequiresStore on no-DB service, RenderTemplate body + subject
goldens, full SYSTEMAUSFALL/SYSTEM FAILURE subject matrix.
Smoke (knowledge-platform-only run, no DB/auth):
- GET /admin/email-templates → 302 to /login
- GET /api/admin/email-templates → 401
- go build/vet/test clean, bun run build clean
Design: docs/design-email-templates-2026-04-29.md.
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.
m's 21:44 answers expanded the rename scope and resolved all 5 open Qs:
- Naming: partner_unit everywhere (not 'department')
- API + URL rename too: paliad.departments → paliad.partner_units,
/api/partner-units, /admin/partner-units
- Settings admin section: removed
- Audit emit: in this PR (paliad.partner_unit_events table)
- users.dezernat: dropped entirely (not renamed)
Migration 026 now does: best-effort second seed of department_members from
dezernat free-text → DROP COLUMN → rename departments + department_members
tables to partner_units + partner_unit_members → rename junction column to
partner_unit_id → rename constraints/indexes/policies → create
partner_unit_events audit table with RLS.
Single tx, exception-trapped renames for idempotency on freshly-provisioned
DBs.
Onboarding form: free-text input replaced with a partner-unit <select> that
inserts a membership row in the user-create tx. Settings profile loses the
free-text field.
PR strategy: still single PR, ~2200 lines net (heavier than v1 due to
structured-side rename + audit plumbing).
DB-backed templates with embedded fallback, per-language split, full
edit/preview/version/restore loop. Subject moves from Go-built strings to
template-rendered. Five open questions for m parked at §8 — most loaded:
should base.html be editable or read-only.
Replaces the "Geplant: Audit-Log" placeholder on /admin with a working
viewer that unions paliad.project_events + caldav_sync_log + reminder_log
into a single keyset-paginated timeline.
- AuditService.ListEntries (internal/services/audit_service.go) does one
UNION ALL across the three sources, projecting each into a unified
AuditEntry shape and ordering by (timestamp, id) DESC. Cursor is
(BeforeTS, BeforeID) — matches the project-event Verlauf pattern. ILIKE
search escapes %/_ so "100%" doesn't act as a wildcard.
- GET /api/audit-log (internal/handlers/audit.go) accepts
source/from/to/q/before_ts/before_id/limit, validates the cursor halves
are paired, and returns { entries, next_cursor }. Both API and the
GET /admin/audit-log SPA shell are wrapped in auth.RequireAdminFunc, so
non-admins get 403 (API) / 302 (browser) via the same gate /admin/team
uses.
- Frontend (admin-audit-log.tsx + client/admin-audit-log.ts) renders the
table with source dropdown, range presets (24h / 7d / 30d / custom /
all), free-text search (debounced 250ms), and "Weitere laden" cursor
pagination. project_events rows reuse translateEvent (t-paliad-067 PR-1)
for DE/EN narrative parity with the dashboard activity feed; caldav and
reminder rows have their own per-event-type i18n keys.
- /admin landing card moved from PLANNED to AVAILABLE; sidebar admin
group gains a third entry.
Inventor design doc for the Dezernate→Partner Units rename and the new
/admin/departments management surface that replaces the placeholder card.
Key proposals:
- Single PR, single migration (026: users.dezernat → users.department).
- New /admin/departments page mirrors /admin/team aesthetic; lifts the CRUD
out of /settings?tab=dezernat.
- User-facing label "Partner unit" / "Partner units" (same in DE+EN per m).
- Defer audit event emission to t-paliad-071 to keep this PR focused.
- Phase 2 follow-up: drop the free-text users.department duplicate once
onboarding can pick from the structured registry.
Five open questions for m in §12 before coder shift starts.
$2 was the offset, used only in the morning dateCond. Evening's query
referenced $1, $3, $4 — $2 was passed but unused, and Postgres can't
infer the type of an unreferenced parameter ('could not determine data
type of parameter $2', 42P18).
Inline offset directly into the morning dateCond as a literal '%d days'
(safe — it's clamped to ≥1 above). New positional layout:
$1 = today
$2 = userid
$3 = is_global_admin
Three rounds of SQL fights for one query. Adding integration coverage
to TestRunSlotForUser is a follow-up — it currently skips when
TEST_DATABASE_URL is unset, which is why none of these reached prod via
CI.
Previous fix replaced $arg with :arg per sqlx.Named convention but the
query body contains PostgreSQL `::TYPE` cast operators (`::uuid[]`,
`::date`, `::interval`). sqlx.Named eats the second `:` thinking it's
a named-arg prefix → 'syntax error at or near ":"'.
Switching to positional $1..$4 sidesteps sqlx.Named entirely. Args
passed directly to db.SelectContext.
Order: $1=today, $2=offset, $3=userid, $4=is_global_admin.
Same root cause as 1652436 — the $/literal token form was the right
intuition; the proper fix is positional, not :name.
fetchSlotDeadlines built the query with $today_arg / $userid_arg /
$offset_arg / $is_global_admin_arg placeholders. sqlx.Named only
recognises :name (colon prefix). Postgres got the literal $arg and
rejected with 'syntax error at or near "$"' on every tick — that's
why no reminder_log row has been written since the t-paliad-064 deploy
yesterday.
Changed all four placeholders to :name. The integration test that would
have caught this (TestRunSlotForUser) skips when TEST_DATABASE_URL is
unset, so CI never hit the live SQL.
Today's morning slot is permanently lost (hourly-tick design issue
tracked in t-paliad-069). Deploying now while Berlin hour=16 should
fire m's evening slot immediately on container startup via the
Start()→RunOnce() call.
Was hitting msupabase-kong (port 8000 on mlake) which serves the
flexsiebels/msbls Supabase. Paliad lives on youpc Supabase, exposed
via Traefik at ystudio.msbls.de. Verified: query 'SELECT count(*)
FROM paliad.users' returns 31 (matches live DB) instead of 0 (msbls
has the schema but no rows).
Auth header reuses YOUPC_SUPABASE_AUTH which is already in dotenv.
Effective on next Claude Code session start.
Per docs/audit-polish-2-2026-04-29.md PR-3. Greenlit by m 2026-04-29 15:22.
- F-16 type-pill saturated colours collapsed to one neutral midnight-tint
chip. The five .akten-type-chip.akten-type-{client,litigation,patent,
case,project} per-type backgrounds (lavender/pink-red/cyan/salmon/
neutral-green) made /projects feel alarming for a routine type label
and the colours carried no semantic ranking. Replaced with a single
rgb(var(--hlc-midnight-rgb) / 0.06) bg + var(--color-text) fg; the type
label inside the chip carries the differentiation. The per-type
modifier classes are kept on the markup so a future signal-use
(highlighting Mandant roots, etc.) can re-introduce a colour for one
specific type without re-adding the random palette. Same neutralisation
applied to .akten-office-chip on /admin/team STANDORT — the audit
flagged it as the same class of issue.
- F-20 .login-tab.active and .gebuehren-tab.active flipped to the
canonical pattern from .akten-tab.active — lime underline + midnight
text + 600 weight. Active tabs now read identically across /login,
/tools/gebuehrentabellen, project detail, deadline detail, appointment
detail, settings, and admin.
- F-37 Notiz textarea now ships a small footer hint reading
"Strg+Enter (oder ⌘+Enter) zum Speichern" / "Ctrl+Enter (or ⌘+Enter)
to save". The keyboard-shortcut listener at notes.ts:426 was already
wired; this is purely the visible affordance. New i18n key
notizen.shortcut.hint (DE+EN); new .notiz-form-hint CSS rule sized
0.75rem muted-text below the actions row.
Verified
- bun run build clean.
- go build/vet/test ./... all green.
- Live smoke pending Dokploy redeploy.
Per docs/audit-polish-2-2026-04-29.md PR-2. Local visual cleanups across
deadlines, appointments, projects, project detail, dashboard, settings,
onboarding.
- F-13 + F-42 the .frist-akte-title CSS rule was renamed to
.frist-project-title (matching the markup that the rename sweep already
produced) plus text-overflow:ellipsis and a max-width gutter, and the
client renderers now stamp title= on the project-title span so the full
ref+title is reachable on hover. Fixes the
"L-2026-001Siemens AG ./." collision and trims the deadline rows that
were ballooning to 2 lines.
- F-15 "Projekt archivieren" demoted from .btn-danger to .btn-secondary
(neutral outline). Confirm-modal action stays red.
- F-24 the /projects filter row groups label+select pairs into
.akten-filter-group divs and stacks each as a full-width labelled block
at <480px instead of wrapping each label/select onto its own line.
- F-27 single-element breadcrumbs hide on root projects — the lone crumb
used to echo the H1 below it.
- F-28 empty REFERENZ + CLIENTMATTER cells on /projects and ORT on
/appointments render an em-dash so the placeholder convention matches
/admin/team and /projects/{id}/deadlines.
- F-33 truncated project refs on the dashboard upcoming-deadlines and
upcoming-appointments lists carry a title= attribute with the full
"REF · Title" string, so hover reveals the truncated tail.
- F-36 /projects/new no longer defaults to Mandant — a "Bitte wählen…"
placeholder is the initial selection (required attr blocks submit).
New projekte.field.type.choose i18n key in DE+EN.
- F-39 the /projects search counter renders "X / Y" in tree view too
(was bare "X"), matching the flat-view format.
- F-43 /projects/{id}/parties empty state is now an .akten-empty-card
with a "Partei hinzufügen" CTA underneath the message, wired to the
same form-open handler as the toolbar button.
- F-47 onboarding + settings job_title placeholder swaps the EN-DE-EN
mix "z.B. Associate, Partner, PA" for "z.B. Associate, Partner,
Patentanwalt" / "e.g. Associate, Partner, Patent Attorney". Three
named titles, no abbrev, EN-jargon convention kept consistent.
- F-50 mobile bottom-nav clearance bumped from 1rem to 1.75rem on
body.has-sidebar main so the centre FAB (margin-top: -10px above the
56px bottom-nav) clears the last list item with a real gutter.
Verified
- go build/vet/test ./... all green.
- bun run build clean.
- Live smoke pending deploy.
Per docs/audit-polish-2-2026-04-29.md PR-1. Single concern: text rendered
to a German narrative that was still English or raw-keyed.
- F-04 deadlines-new.ts now references the existing fristen.field.akte.*
keys (the SSR template already used them) instead of the non-existent
fristen.field.project.* keys, so the picker no longer renders the raw
i18n key.
- F-07 + F-21 dashboard activity log + project Verlauf:
• i18n.ts gains the missing dashboard.action.short.project_type_changed
plus a parallel event.title.* key set (full noun-phrase form for
Verlauf, complementing the dashboard's verb form) and
event.description.* templates with {title}/{count}/{parent}
placeholders.
• New translateEvent(eventType, title, description) helper localizes a
stored project_events row for display; parses both new value-only
descriptions and legacy English+DE-mix shapes ("Deadline „Foo"
geändert", "Type case → litigation", "Note zu deadline hinzugefügt").
Wired into dashboard.ts and projects-detail.ts renderers.
• Go services now write descriptions as value-only payloads (the title,
the count, the parent slug, or "old → new") so future rows are
locale-clean. Affected services: deadline_service.go (5 sites),
appointment_service.go (3 sites), note_service.go (1 site),
project_service.go (2 sites: status_changed, project_type_changed).
• Translation covers historical project_events rows too — the
legacy-format parsers in translateEventDescription strip the English
"Type"/"Status" prefix and pull the quoted title out of "Deadline
„Foo" geändert" so DE/EN renders correctly without DB migration.
• Renamed dashboard.action.short.project_* DE labels from "...Akte" to
"...Projekt" to match the project-rename direction.
- F-10 deadlines list REGEL column now resolves rule_name/rule_name_en
via a JOIN-side alias on deadline_service.ListWithProjects (added
RuleName/RuleNameEN to DeadlineWithProject). New ruleDisplay() helper
prefers the localized rule name and falls back to em-dash; never
renders the raw rule_code slug ("inf.rejoin").
- F-12 fristen.col.akte and termine.col.akte DE values flip "Akte" →
"Projekt"; matching SSR placeholder text on deadlines.tsx and
appointments.tsx column headers (EN already said "Matter").
- F-29 the checklists empty-state hint on /projects/{id}/checklists is
split into prefix/link/suffix spans so the <a href="/checklists"> stays
intact after applyTranslations() runs (the previous single-string i18n
value collapsed the anchor on first paint).
- F-35 projekte.subtitle DE flips "Fälle" → "Verfahren" (matches the
actual type taxonomy: Mandant/Streitsache/Patent/Verfahren/Projekt).
Same fix on projekte.empty.hint. EN keeps "cases" since EN labels the
case type as "case".
- F-46 dashboard.greeting.prefix EN flips "Good day" → "Hello".
Verified
- go build ./... + go vet ./... + go test ./... all green.
- bun run build clean.
- Dashboard activity widget + project Verlauf renderer verified by
reading the translated paths; live smoke pending deploy.
Exposes paliad.users.escalation_contact_id (added in migration 025) via
the Benachrichtigungen tab so users can route DRINGEND/overdue
escalation to a specific colleague instead of the global_admins
fallback.
Service:
- UpdateProfileInput.EscalationContactID *string (empty = clear, matches
Dezernat tri-state pattern). Server-side validation rejects self-
pointer (also enforced by CHECK in migration 025) and unknown UUIDs.
Reminder read path:
- digestRow now carries owner.escalation_contact_id and the audience
predicate adds the override. visibleForCategory's "global admin"
branch suppresses when an override is set, so escalation does not
fan out to the whole admin team. Test table extended with override
cases (escalation contact sees overdue / DRINGEND, admin suppressed).
UI / client:
- New "Eskalations-Kontakt" section under Benachrichtigungen with a
select populated from /api/users (excluding self, sorted by name).
First option is the default-fallback marker; selecting it clears.
- savePrefs PATCHes escalation_contact_id alongside the existing
reminder fields.
i18n: einstellungen.prefs.escalation.{heading,hint,default_option}
in DE + EN.
docs/project-status.md: flips the open follow-up to "shipped".