Commit Graph

282 Commits

Author SHA1 Message Date
m
fee6afdb14 feat(t-paliad-083): dark mode — auto + manual toggle, system-pref default (mAi/paliad#2)
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.
2026-04-30 05:25:39 +02:00
m
34e5ffe94b Merge: t-paliad-080 service-layer naming sweep — Notiz/Termin/Frist/Projekt/Partei → Note/Appointment/Deadline/Project/Party 2026-04-30 04:39:42 +02:00
m
ce3227c1c0 refactor(t-paliad-080): service-layer naming sweep — Notiz/Termin/Frist/Projekt/Partei → Note/Appointment/Deadline/Project/Party
Mechanical rename across 8 service files plus their handler call sites and
two related helpers. The English types existed already; what changed are the
input-struct names, helper functions, list/create method suffixes, and
parameter names so they no longer mix English types with German parameter
names.

Renames cover:
- CreateNotizInput/UpdateNotizInput → CreateNoteInput/UpdateNoteInput,
  notizColumns/notizSelect → noteColumns/noteSelect, ListForProjekt/Frist/
  Termin → ListForProject/Deadline/Appointment, CreateForProjekt/Frist/
  Termin → CreateForProject/Deadline/Appointment, fristProjectID →
  deadlineProjectID
- CreateTerminInput/UpdateTerminInput → CreateAppointmentInput/
  UpdateAppointmentInput, terminColumns → appointmentColumns, ListForProjekt
  → ListForProject; parameter renames terminID → appointmentID, projektID
  → projectID
- CreateFristInput/UpdateFristInput → CreateDeadlineInput/
  UpdateDeadlineInput, fristColumns → deadlineColumns, ListForProjekt →
  ListForProject, isValidFristStatus → isValidDeadlineStatus; parameter
  renames fristID → deadlineID, projektID → projectID
- CreateProjektInput/UpdateProjektInput → CreateProjectInput/
  UpdateProjectInput, projektColumns → projectColumns,
  validateProjektStatus → validateProjectStatus, ProjektRole comment →
  ProjectRole
- CreateParteiInput → CreatePartyInput, parteiColumns → partyColumns,
  ListForProjekt → ListForProject; parameter renames parteiID → partyID
- OnTerminCreated/Updated/Deleted → OnAppointmentCreated/Updated/Deleted on
  the AppointmentCalDAVPusher interface and its CalDAVService impl
- formatTermin → formatAppointment in caldav_ical
- ListForProjekt → ListForProject, listWithProjekt → listWithProject,
  checklistInstanceWithProjektSelect → checklistInstanceWithProjectSelect,
  ClearProjekt → ClearProject (JSON tag clear_projekt unchanged — wire
  format)
- insertProjectEvent helper parameter projektID → projectID, error message
  "insert projekt_event" → "insert project_event"
- TeamService AddMember/RemoveMember/ListDirectMembers/ListEffectiveMembers
  parameter projektID → projectID; matching handler renames
- Frontend doc-comments referencing CreateProjektInput/UpdateProjektInput
  updated to CreateProjectInput/UpdateProjectInput

JSON wire tags (clear_projekt, etc.) and German user-facing strings
(glossary entries, search.go labels, email templates, changelog,
Terminsgebuehr, Fristenrechner product name) are intentionally untouched.

API contract unchanged. go build/vet/test ./... clean. Frontend bun build
clean.
2026-04-30 04:39:23 +02:00
m
4b4c61903d Merge: t-paliad-079 bulk-rename German-prefix i18n keys to English 2026-04-30 04:38:34 +02:00
m
5c11fe5e6d feat(t-paliad-079): bulk-rename German-prefix i18n keys to English
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).
2026-04-30 04:38:06 +02:00
m
74d4d913c2 Merge: t-paliad-082 light-mode contrast — accent text token (--color-accent-fg) 2026-04-30 03:59:20 +02:00
m
b25da860c8 fix(t-paliad-082): introduce --color-accent-fg so accent text isn't lime on cream
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.
2026-04-30 03:59:12 +02:00
m
d6a91ee43c Merge: t-paliad-078 type i18n key registry + build-time data-i18n scan 2026-04-30 03:56:47 +02:00
m
800668a483 feat(t-paliad-078): type i18n key registry + build-time data-i18n scan
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.
2026-04-30 03:56:32 +02:00
m
2b476e4f25 Merge: t-paliad-076 visibility predicate consolidation (6 sites + delete dead IsEffectiveMember) 2026-04-30 03:49:04 +02:00
m
31db66e3b7 refactor(t-paliad-076): consolidate visibility predicate — 6 dashboard/agenda sites use helper
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.
2026-04-30 03:48:49 +02:00
m
b178c47a44 Merge: t-paliad-081 doc + dead-code batch (F-5/F-10/F-11/F-15/F-16/F-17/F-18) 2026-04-30 03:42:42 +02:00
m
3da11bd798 chore(t-paliad-081): doc + dead-code batch (F-5/F-10/F-11/F-15/F-16/F-17/F-18)
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.
2026-04-30 03:42:25 +02:00
m
17aa840977 Merge: t-paliad-077 fix /api/links/suggest 500 (sqlx for paliad.link_*) 2026-04-30 03:18:05 +02:00
m
e468930342 fix(t-paliad-077): /api/links/suggest 500 — switch to sqlx for paliad.link_*
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).
2026-04-30 03:18:03 +02:00
m
8cd67433df Merge: t-paliad-075 admin_users.go comment cleanup 2026-04-30 03:12:46 +02:00
m
25ca1fa763 fix(t-paliad-075): drop stale department_members reference in handler comment 2026-04-30 03:12:45 +02:00
m
db20bf5442 Merge: t-paliad-075 fix AdminDeleteUser SQL (renamed partner_unit tables) 2026-04-30 03:08:14 +02:00
m
8bcfb6b960 fix(t-paliad-075): AdminDeleteUser SQL — use renamed partner_unit tables
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).
2026-04-30 03:08:08 +02:00
m
270f7d7ddc Merge: t-paliad-074 architecture improvement audit (ada) 2026-04-30 03:02:13 +02:00
m
61766161b7 docs(t-paliad-074): architecture improvement audit 2026-04-30
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.
2026-04-30 02:53:50 +02:00
mAi
2c67299740 Merge: t-paliad-073 audit polish-2 DEFER cleanup (F-23/32/38/40/48/49)
Six DEFER findings shipped per docs/audit-polish-2-2026-04-29.md.

Refs t-paliad-073
2026-04-30 00:31:05 +00:00
m
aef40bb425 feat(t-paliad-073): audit polish-2 DEFER list cleanup
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.
2026-04-30 02:29:09 +02:00
m
d6ff36dce4 Merge: t-paliad-069 reminder ticker boundary alignment + startup catch-up 2026-04-30 02:28:32 +02:00
m
ee83748089 fix(t-paliad-069): align reminder ticker to natural HH:00 boundaries + startup catch-up
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.
2026-04-30 02:28:19 +02:00
m
194c61b498 Merge: t-paliad-070 partner units rename + /admin/partner-units 2026-04-29 22:18:26 +02:00
m
832104af9e Merge remote-tracking branch 'origin/main' into mai/cronus/partner-units-rename
# Conflicts:
#	frontend/build.ts
#	frontend/src/admin.tsx
#	frontend/src/client/i18n.ts
#	internal/handlers/handlers.go
2026-04-29 22:17:32 +02:00
m
d50ba363a8 feat(t-paliad-070): partner-units frontend rename + new admin page
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.
2026-04-29 22:14:11 +02:00
m
8dc1beb4e1 Merge: t-paliad-072 admin email-templates editor 2026-04-29 22:10:00 +02:00
m
0e3411c40b feat(admin): /admin/email-templates editor (t-paliad-072)
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.
2026-04-29 22:09:39 +02:00
m
76785da3f6 feat(t-paliad-070): rename Department → PartnerUnit on the Go side
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.
2026-04-29 22:03:08 +02:00
m
f963b4b2bc Merge remote-tracking branch 'origin/main' into mai/cronus/partner-units-rename 2026-04-29 21:53:25 +02:00
m
633ce5a9fe design(t-paliad-070): incorporate m's answers — full partner_unit rename
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).
2026-04-29 21:50:27 +02:00
m
c4122bc265 docs(admin): t-paliad-072 — m greenlighted all 5 open Qs
DB-backed (Q1), subjects customisable with SYSTEMAUSFALL comment in seed
(Q2), base.html editable (Q3-A), 20-version retention (Q4), note field
kept (Q5). Coder shift unblocked from the inventor side.
2026-04-29 21:46:35 +02:00
m
9e216a4c44 docs(admin): design — admin email-templates editor (t-paliad-072)
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.
2026-04-29 21:46:35 +02:00
m
933a16b6eb Merge: t-paliad-071 admin audit-log viewer 2026-04-29 19:12:23 +02:00
m
2422603abf feat(admin): /admin/audit-log global timeline (t-paliad-071)
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.
2026-04-29 19:12:11 +02:00
m
1a89b0c490 design(t-paliad-070): partner units rename + admin departments page
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.
2026-04-29 19:03:14 +02:00
m
a719eb26a6 fix(reminder): inline offset, drop unused $2 in evening query
$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.
2026-04-29 16:34:17 +02:00
m
25a44dcaee fix(reminder): positional placeholders, drop sqlx.Named (collides with ::cast)
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.
2026-04-29 16:32:03 +02:00
m
1652436f1b fix(reminder): use sqlx.Named placeholders, not literal $arg
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.
2026-04-29 16:29:47 +02:00
m
93a90b0ffa fix(.mcp.json): point supabase MCP at youpc Kong (paliad's actual DB)
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.
2026-04-29 15:47:17 +02:00
m
4a25f2ee0f Merge: t-paliad-068 polish audit 2 PR-3 — tab harmonisation + chip neutralisation + Notiz hint 2026-04-29 15:27:15 +02:00
m
3dc56552fa fix(t-paliad-068): PR-3 tab harmonisation + chip neutralisation + Notiz hint (F-16, F-20, F-37)
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.
2026-04-29 15:27:09 +02:00
m
d00eb5f598 Merge: t-paliad-067 polish audit 2 — triage doc + PR-1 (i18n leak + activity log) + PR-2 (visual residue) 2026-04-29 15:06:13 +02:00
m
8fe05fe696 Merge main into mai/cronus/audit-polish-2-triage (resolve i18n.ts collision with brunel's t-paliad-066 escalation keys) 2026-04-29 15:05:50 +02:00
m
7d45626d57 fix(t-paliad-067): PR-2 visual residue + per-page polish (F-13, F-15, F-24, F-27, F-28, F-33, F-36, F-39, F-42, F-43, F-47, F-50)
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.
2026-04-29 14:32:53 +02:00
m
f583c650a2 fix(t-paliad-067): PR-1 i18n leak sweep + activity narrative (F-04, F-07, F-10, F-12, F-21, F-29, F-35, F-46)
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.
2026-04-29 14:26:04 +02:00
m
2ffdcb9c25 Merge: t-paliad-066 — escalation contact dropdown 2026-04-29 13:59:46 +02:00
m
bff2ec5107 feat(t-paliad-066): escalation contact dropdown in Settings → Notifications
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".
2026-04-29 13:59:30 +02:00