F-7 of the t-paliad-074 architecture audit. Sweeps the last German-named
CSS leftovers — purely a class-name change, no behaviour or styling
delta. 466 references across global.css and ~30 TSX/TS files.
Naming rules applied:
- Generic table/tabs/form/empty/controls/detail/events/status/type/
suggestion/chip/col/ref/search-wrap/select/soon/loading/muted/
unavailable/row/header-row/title-input -> .entity-*
- Truly generic widgets dropped the prefix: .multi-* (multi-select
panel), .filter-*, .collab-* (collaborator picker; bare class is
now .collab-picker), .firmwide-*, .office-*, .back-link
- Project-specific names kept specific: .party-form/-controls/-table
(parties on a project), .checklists-hint, .netdocs-link
- Page-scoped IDs in projects.tsx -> projects-search/-count/-body;
projects-new.tsx -> project-new-form/-msg
- German content "akten-bezogen" tightened to "aktenbezogen" (one-word
form is also valid German) so the strict grep stays clean
Per audit recommendations §6.2 (rec 5-9). Ports 14 RoP rules from
youpc's deadline calc into paliad.deadline_rules so they're surfaceable
in the timeline (course-of-proceedings) Fristenrechner mode in addition
to the existing trigger-event mode.
Adds 4 new proceeding types and extends 2 existing trees:
- UPC_DAMAGES (new) — Schadensbemessung (R.137.2, R.139 reply, R.139 rejoin)
- UPC_DISCOVERY (new) — Bucheinsicht (R.142.2, R.142.3 reply, R.142.3 rejoin)
- UPC_COST_APPEAL (new) — Berufung Kostenentscheidung (R.221.1)
- UPC_APP_ORDERS (new) — 15-day order-flavor (R.220.2, R.220.3, R.237 b, R.238.2)
- UPC_INF extended — R.151 chained off inf.decision
- UPC_APP extended — decision-flavor cross-appeal pair (R.237 a, R.238.1)
Total: 14 RoP rules across 5 families. New types appear in the
proceeding-type picker via deadlines.upc_damages / upc_discovery /
upc_cost_appeal / upc_app_orders i18n keys (DE + EN).
Design notes in the migration explain why some rules live in their own
proceeding type (when the legal anchor differs from UPC_INF/UPC_APP's
trigger date) vs being chained off existing rules.
Q6 of t-paliad-088 left firm-wide event_type creation open to any user; this
ships the moderation surface admins use to dedupe and clean up the resulting
drift.
Service layer (internal/services/event_type_service.go):
- ListAllForAdmin(filter) — firm-wide rows with usage_count and
author_display_name, optionally including archived (single query, scalar
subquery + LEFT JOIN paliad.users). Sorted live-first, then category +
label_de.
- ListPrivatePendingPromotion — every private non-archived row across all
users, sorted by usage_count DESC.
- ArchiveBulk(ids) — UPDATE archived_at=now() WHERE is_firm_wide AND NULL.
- Promote(id) — flip is_firm_wide=true; surfaces ErrEventTypeSlugTaken on
collision so the admin can merge instead.
- Restore(id) — flip archived_at back to NULL; same slug-collision surface.
- MergeIDs(winner, losers) — tx-scoped INSERT … SELECT … ON CONFLICT
redirect of deadline_event_types from losers → winner, then DELETE on the
loser junction rows, then archive the losers. Refuses if the winner is
archived or private. Junction PK does the dedup.
- requireAdmin gate runs at every method (defence-in-depth on top of the
handler-level RequireAdminFunc).
Handlers (internal/handlers/admin_event_types.go):
- GET /api/admin/event-types[?include_archived=1]
- GET /api/admin/event-types/private
- POST /api/admin/event-types/archive { ids:[…] }
- POST /api/admin/event-types/merge { winner_id, loser_ids:[…] }
- POST /api/admin/event-types/{id}/promote
- POST /api/admin/event-types/{id}/restore
- GET /admin/event-types page shell.
All wrapped behind auth.RequireAdminFunc at registration time.
Frontend:
- New /admin/event-types SPA (admin-event-types.tsx + client/admin-event-types.ts):
search, "Archivierte anzeigen" toggle, per-row archive/restore, bulk
archive, merge modal (winner picker defaults to highest-usage row),
separate table for private types pending promotion.
- Sidebar entry under Verwaltung; admin landing card.
- ~50 i18n keys DE+EN under admin.event_types.* + nav.admin.event_types.
- CSS for archived badge, merge option list, bulk-actions bar.
Out of scope (deferred): public "merge request" workflow for non-admins.
F-3 from t-paliad-074 architecture audit. NoteService used to call
ProjectService.GetByID and AppointmentService.GetByID just for the
visibility bit (~6 cross-service full-row reads in note_service.go).
Each was a full SELECT on the parent row when only a boolean was needed.
Add CanSee(ctx, userID, id) (bool, error) on the two parent services:
single EXISTS round-trip, no projection. Personal Appointments stay
visible only to their creator; project-anchored Appointments inherit
the project's visibility predicate (global_admin shortcut + team-walk).
NoteService gains two private helpers — requireProjectVisible and
requireAppointmentVisible — that wrap CanSee + ErrNotVisible. All
visibility-only sites in note_service.go (ListForProject /
ListForDeadline / ListForAppointment / ListForProjectEvent /
CreateForProject / CreateForDeadline / requireVisible) now go through
the helpers.
CreateForAppointment keeps appointment.GetByID — it legitimately needs
the appointment's project_id for the audit-event row.
DeadlineService.CanSee was not added: note_service never reaches into
the deadline service for visibility (it does its own SELECT project_id
FROM paliad.deadlines and gates via the project predicate).
Test: cansee_test.go covers the gate level for both new methods —
admin sees everything (global_admin shortcut), team member sees their
team's, non-member sees nothing, missing IDs are invisible to all,
personal appointments are private to creator.
Shared client module client/event-types.ts exposes three surfaces:
1. attachEventTypePicker — multi-tag chip cluster with typeahead suggest
and an inline "+ Neuen Typ hinzufügen…" affordance. Mounted on
/deadlines/new and the /deadlines/{id} edit modal.
2. attachEventTypeMultiSelectFilter — listbox-panel filter (search + Alle
+ Ohne Typ + grouped checkbox list, click-outside / Escape dismiss).
Mounted on /deadlines and /agenda. Trigger styled like the existing
<select>s; serialises to ?event_type=<uuid>,<uuid>,none.
3. openAddEventTypeModal — modal with label_de/label_en/category/
jurisdiction/firm_wide. Live duplicate-warning fed by
/api/event-types/suggest (Q6 mitigation). Firm-wide checkbox is
only rendered for global_admin (per the design's permission model).
Added Typ column on /deadlines (hidden when no visible row carries an
event_type — matches the t-paliad-073 hide-on-uniform pattern).
Added Typ display + edit on /deadlines/{id}; PATCH now sends
event_type_ids when the picker is mounted.
i18n: 36 new keys (DE+EN) under event_types.* + deadlines.field/col/
filter.event_type + agenda.filter.event_type + common.cancel.
CSS in global.css: .event-type-picker / .event-type-chip /
.akten-multi-trigger / .akten-multi-panel / .akten-event-type-pill /
.event-type-add-modal. Mobile (<640px) collapses the panel into a
bottom sheet.
bun run build clean (1302 i18n keys regenerated, data-i18n scan clean).
go build / go vet / go test ./... clean (PR-1 still green after rebase).
Migration 030 adds paliad.event_types and paliad.deadline_event_types
junction. ~43 firm-wide seeds biased toward submissions (25 UPC
submissions + 8 UPC decisions/orders/hearings + 5 EPO + 4 DPMA/DE + 1
cross-jurisdiction). UPC-seeded rows carry a loose trigger_event_id
column (no FK constraint per Q2: event_types leads, trigger_events
follows). RLS policies are defense-in-depth — primary enforcement is
in the Go service layer. Per Q6, any authenticated user can create
firm-wide types; admins moderate via the soft-delete archive lever.
EventTypeService: List (firm-wide ∪ own-private), GetByID, Create
(slug auto-derived, supports diacritics → ASCII), Update (author OR
admin-on-firm-wide), SuggestSimilar (powers the duplicate-warning in
the add modal), AttachToDeadlineTx + ValidateForUser + ListForDeadlines
for the junction.
DeadlineService gains an EventTypeService dependency and now:
- accepts event_type_ids on Create / Update / CreateBulk
- attaches them in the same transaction as the deadline insert
- hydrates EventTypeIDs on every Get / List / ListForProject
- supports the multi-select Typ filter via ListFilter.EventTypeIDs +
IncludeUntyped (UNION semantics within types, AND-intersected with
Status/Project)
AgendaService gets the same Typ filter on its deadline side;
appointments are unaffected.
API:
- GET /api/event-types?category=&jurisdiction=
- GET /api/event-types/suggest?q=
- POST /api/event-types
- PATCH /api/event-types/{id} (set archive=true to hide)
- GET /api/deadlines?event_type=<uuid>,<uuid>,none
- GET /api/agenda?event_type=<uuid>,<uuid>,none
- POST/PATCH /api/deadlines accept event_type_ids: [uuid]
go build / go vet / go test ./... clean.
Frontend (picker + custom-add modal + multi-select filter) follows in
PR-2. Admin moderation panel deferred to t-paliad-089 follow-up.
m greenlit all 7 open questions on 2026-04-30 12:23. Notable changes
from the initial draft:
- Submissions are explicitly the primary Event-Type use case, not a
secondary discriminator. m: "those are the event types I mean,
mainly". Deferring a separate paliad.submissions table stands.
- /deadlines + /agenda Typ filter is MULTI-SELECT (UNION across
selected types, AND-intersected with Status/Projekt). New
EventTypeMultiSelect component spec'd in §4: trigger button styled
like the existing <select>s, popover with search + grouped checkbox
list. Status/Projekt stay single-select.
- Firm-wide Event-Type creation OPEN to any authenticated user. RLS
insert policy simplified to created_by=self. Admins moderate via
archive. Mitigation: duplicate-warning in the add modal. Follow-up
t-paliad-089 flagged for admin moderation panel.
- Broader-scope seeds confirmed (UPC + EPO + DPMA + DE + contract).
- §12 rewritten as a resolution table.
The first PR caught all `var(--color-bg-muted, #fallback)` sites. This catches
the 5 remaining `var(--color-bg-muted)` sites *without* fallback in the admin
email templates page (.admin-et-card-key, .admin-et-card-lang-btn:hover,
.admin-et-variable-type, .admin-et-preview-subject, .admin-et-version-row:hover).
Without fallback, an undefined custom property resolves to `unset` →
`transparent`, so these elements rendered with no visible background in
either mode (rather than light-grey in light mode like the fallback-form
variant). Same root cause though: the `--color-bg-muted` token name was
never defined anywhere.
All 10 sites (5 with hex fallback + 5 without) now use `--color-surface-muted`.
Build clean.
Standalone paliad.event_types table with nullable FK on paliad.deadlines,
seeded from a curated subset of paliad.trigger_events (UPC submissions +
decisions) plus hand-written EPO/DPMA/DE-national/contract entries.
Picker on /deadlines/new + edit modal with grouped options + inline
custom-add modal (private types for any user, firm-wide gated to
global_admin). Filter <select> on /deadlines (matching existing
Status/Projekt pattern, not pills) and pill-row on /agenda. Submissions
are NOT a separate entity — category='submission' on event_types carries
the discrimination until a real Schriftsatz-Verwaltung is built.
Awaiting m's go/no-go on §12 before any implementation.
`/team` count pills (member counts per office / per partner unit) rendered with
`var(--color-bg-muted, #f4f4f7)` — but `--color-bg-muted` was never defined, so
the literal `#f4f4f7` always won and the pills stayed light-grey in dark mode.
Sweep across `frontend/src/styles/global.css`:
- 5x `--color-bg-muted, #f3f4f6|#f4f4f7` → `--color-surface-muted` (the actual
themed chip-bg token; light: #f3f4f6, dark: 5% cream over midnight).
Sites: `.team-group-count`, `.team-dept-tag`, `.admin-soon-badge`,
`.admin-audit-event`, `.admin-audit-source`.
- Trigger-event Fristenrechner block (added in PR-2 / t-paliad-086) used a
parallel set of fictional tokens (`--surface-color`, `--surface-soft`,
`--accent-soft`, `--text-color`, `--text-muted`, `--border-color`,
`--accent`, `--accent-text`, `--border-light`) that were never defined,
so the entire panel rendered in fallback hex literals — white card,
light-grey duration chip, pale-lime rule-code, dark `#111` body text.
In dark mode the bg/border/divider stayed light while text stayed dark,
on a midnight body — unreadable.
Re-pointed all 27 sites onto the project's `--color-*` token system.
- `rgba(0, 0, 0, 0.0X)` literal overlays (hover/active states for
`.search-result`, `.palette-action`, `.quick-add-row/-cancel`,
`.pwa-install-dismiss`, `.termin-type-chip/-badge`, `.termin-personal-tag`,
`.caldav-status-card`) → `--color-overlay-faint|subtle` (the existing
tokens that flip to white-channel alpha in dark mode).
- Removed redundant hex fallbacks on already-themed tokens
(`var(--color-surface, #ffffff)` → `var(--color-surface)` etc) — the
`:root[data-theme="dark"]` block already defines all of them.
Acceptance:
- `cd frontend && bun run build` → clean.
- Sweep-greps from the task brief now return 0 hits (excl. one comment).
- No new tokens introduced — reuses the t-paliad-083 / t-paliad-082 palette.
Refs t-paliad-087.
Implements the four audit recommendations from §6.1 of
docs/audit-fristenrechner-completeness-2026-04-30.md plus a holiday-
adjustment cap fix surfaced by PR-2's smoke test.
(1) UPC_INF CCR-conditional rejoinder
Public Fristenrechner now flips inf.reply (RoP.029.b → RoP.029.a) and
inf.rejoin (1mo / RoP.029.c → 2mo / RoP.029.d) when the user ticks
"Mit Widerklage auf Nichtigkeit." Implemented via a new
`condition_flag` column on paliad.deadline_rules: when the rule names
a flag and the API request's flags array contains it, the calculator
substitutes alt_duration_value/unit and alt_rule_code. Independent of
the existing `condition_rule_id` mechanism (which references a real
rule in the same proceeding tree — only useful for matter-attached
trees that already seed the CCR rule).
(2) UPC_APP / internal APP grounds anchoring
`app.grounds` is now anchored on the trigger date (the appealed
decision) with a 4-month duration, not chained 2mo after `app.notice`.
Per RoP 220.1 the legal rule is "4 months from notification of the
decision," independent of when the notice itself was filed. The chain
only happened to give the right answer when both legs landed on a
working day; under holiday rollover (e.g. notice deadline pushed to
Monday) the grounds deadline drifted off the 4mo legal target.
(3) EP_GRANT publish anchor on priority date
New `anchor_alt` column on paliad.deadline_rules. ep_grant.publish
carries `anchor_alt='priority_date'`. The Fristenrechner UI surfaces
an optional "Prioritätstag" input (visible only when EP_GRANT is
selected) that, when populated, anchors the publish-A1 calculation on
the priority date instead of the filing. Falls back to filing date
when the priority field is empty (the case for purely-EP applications
with no foreign priority claim).
(4) Rule-code format normalisation
Migration 029 normalises 'RoP 23' → 'RoP.023', 'RoP 29b' / 'RoP.029b'
→ 'RoP.029.b', 'RoP 220.1' → 'RoP.220.1', etc. across deadline_rules.
Matches the canonical youpc format already used by the PR-1 imported
event-deadline rule codes.
(+) AdjustForNonWorkingDays cap bumped 30 → 60
Surfaced by the PR-2 smoke test: SoD on 2026-04-30 (3mo from trigger)
landed on Sat 2026-08-29 instead of Mon 2026-08-31. The 30-iteration
safety bound on AdjustForNonWorkingDays cannot walk past the 33-day
UPC summer vacation plus flanking weekends. Bumped to 60. Pure-Go
one-liner, locked by a follow-up production smoke (real
paliad.holidays seed has the UPC vacation).
Schema (migration 029): two new nullable text columns on
paliad.deadline_rules — `condition_flag` and `anchor_alt`. Both ignored
by every existing rule; only the rows updated above carry values.
Models: DeadlineRule gains ConditionFlag + AnchorAlt (nilable strings).
Service: FristenrechnerService.Calculate now takes a CalcOptions struct
(PriorityDateStr, Flags). API handler accepts optional priorityDate and
flags fields on POST /api/tools/fristenrechner.
Frontend: TSX surfaces the priority-date row + CCR checkbox conditionally
on selectedType (only EP_GRANT / UPC_INF respectively). Client TS reads
them and threads through the API call. New i18n keys for both DE+EN.
Migration 029 dry-run validated on prod Supabase (BEGIN/ROLLBACK):
schema + UPDATEs apply cleanly, rule states match expected post-fix
shape. Tests + go build/vet + bun build all clean.
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).
Adds a Role filter row alongside the existing Office row on /team. Pills
are rendered from the distinct paliad.users.job_title values present in
the loaded users; "Alle" + Partner / Counsel / Senior Associate /
Associate / … / PA / Paralegal in seniority order, anything unrecognised
sorted alphabetically after.
The interface field name was previously `role: string`, left over from
before t-paliad-051 split paliad.users.role into job_title +
global_role. The API has been returning `job_title` since then, so the
role line on every card was silently empty. Updated User /
DepartmentMember interfaces to `job_title?: string | null`, and
renderUserCard now displays it via roleLabel(). Search now matches
job_title too.
Role values are normalised case-insensitively (DB still has both
"Associate" and "associate" today — separate cleanup), and a roleLabel()
helper looks up team.role.<slug> with the raw job_title as fallback so
new titles render even before the i18n entry exists.
Files
- frontend/src/team.tsx — second team-filter-row
- frontend/src/client/team.ts — User.job_title, ROLE_ORDER,
presentRoles, buildRoleFilters, userMatchesRole, roleLabel; render()
intersects office × role × search
- frontend/src/client/i18n.ts — team.filter.role + 10 team.role.* keys
(DE/EN)
loadAkten() fetched /api/projects but assigned the result to a stray
'akten' identifier (implicit global) while renderAkteOptions() iterated
over the declared 'projects' variable. Result: the project select on the
new-checklist-instance modal was always empty.
Two-line typo from the akten→projects rename sweep.
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).