Adds the second Fristenrechner mode (mirrored from youpc.org's deadline
calc): pick a UPC trigger event + date, see all deadlines that flow
from it. Coexists with the existing course-of-proceedings timeline mode
via a tab toggle on /tools/fristenrechner.
Backend:
- internal/services/event_deadline_service.go — EventDeadlineService.
ListTriggerEvents (alphabetical), Calculate (resolves all deadlines
flowing from a trigger). Routes through HolidayService for weekend +
holiday rollover. Honours the new working_days unit. Resolves
composite rules (alt_* + combine_op) by computing both legs and
picking max/min. Used by R.198/R.213 ("31d OR 20wd, whichever is
longer") imported in PR-1.
- internal/services/event_deadline_service_test.go — covers
addWorkingDays (forward, backward, zero, holiday-skip), composite
rule semantics, before-timing.
- internal/handlers/fristenrechner.go — two new endpoints:
GET /api/tools/trigger-events, POST /api/tools/event-deadlines.
- handlers.Services / dbServices: new EventDeadline / eventDeadline
field; wired in cmd/server/main.go from the same HolidayService.
Frontend:
- frontend/src/fristenrechner.tsx — tab strip + second wizard panel
(3 steps: trigger picker → date → flat result list).
- frontend/src/client/fristenrechner.ts — initEventMode wiring,
typeahead filter over the 102 trigger events, Calculate flow,
bilingual rendering, composite-rule labels, lang-change refresh.
- frontend/src/client/i18n.ts — 27 new keys (DE+EN) under
deadlines.mode.* and deadlines.event.* (incl. units, timing).
- frontend/src/styles/global.css — fristen-mode-tabs, mode-panel,
event-list, event-result-row visual style.
Working-day arithmetic detail: the new addWorkingDays helper steps
one day at a time and skips runs of non-working days (Sat/Sun + DE
federal + UPC vacations seeded via paliad.holidays). Day-zero is the
caller's job — addWorkingDays(0) returns the input unchanged so
callers can decide whether to roll forward via AdjustForNonWorkingDays.
Composite-rule resolution: when a row carries alt_duration_value +
alt_duration_unit + combine_op, Calculate computes both legs,
picks max/min, and surfaces a compositeNote like
"max(31 days, 20 working_days) → working_days leg" so the UI can
explain which leg won.
PR-3 will land Tier 1 bug fixes from the audit (CCR adaptive,
UPC_APP grounds anchoring, EP_GRANT priority, rule-code normalisation).
Read-only research deliverable. Compares paliad's 9-proceeding-type
Fristenrechner ruleset (52 public rules in deadline_rules) against
youpc's 70-deadline event-driven calc (data.deadlines + data.events).
Top findings (§1 executive summary):
- youpc covers 64 distinct UPC RoP rule codes; paliad covers ~5
- The two tools answer different questions (timeline-by-procedure vs
search-by-trigger-event) — biggest gap is structural, not data
- Paliad's holiday system is materially better; youpc's defaults are empty
Critical bugs surfaced (§4):
- Public UPC_INF Fristenrechner ignores CCR-conditional rejoinder
duration (always uses 029.c/1mo, should be 029.d/2mo when CCR filed).
KanzlAI internal INF type already wires this; public type doesn't.
- UPC_APP grounds chained off notice instead of decision date,
giving wrong dates when notice is filed early
- EP_GRANT publish chained off filing instead of priority date
- Rule_code format inconsistent across migrations (RoP 23 vs RoP.023)
Recommendations ranked across 5 tiers (§6) for m to review.
Open product decisions in §7. No code changes.
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.