a6d0acbcb431576d6bdb850070a856d60cd85781
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 3d3a4fa36d |
feat(team-admin): t-paliad-223 Slice B — Add User via Supabase Admin API
#49 — adds a third "Konto direkt anlegen" path on /admin/team alongside "Onboard existing" and "Invite colleague". Creates both auth.users (via Supabase Admin API) and paliad.users in one click; new user is visible in dropdowns immediately and receives a paliad-branded magic-link email. - internal/services/supabase_admin.go: new SupabaseAdminClient — thin net/http shim. 3 methods (CreateAuthUser, GenerateRecoveryLink, DeleteAuthUser). 10s timeout. ErrSupabaseAdminUnavailable when key unset, ErrSupabaseEmailExists when 422-with-"already" returned. apikey + Bearer headers on every call. Sentinel errors for handler mapping. - internal/services/supabase_admin_test.go: 5 tests pin wire-shape (disabled mode, happy-path POST + headers + body, email-exists mapping, both action-link response shapes, DELETE-by-id route). - internal/services/user_service.go: UserService grows optional supabase + mail + baseURL dependencies via SetAddUserDeps. AdminCreateFullInput (email/display_name/office/job_title/profession/lang/send_welcome_mail + inviter fields). AdminCreateUserFull validates input → calls supabase.CreateAuthUser → inserts paliad.users (best-effort DeleteAuthUser rollback on insert fail) → writes paliad.system_audit_log row (event_type='user.added_by_admin') → sends welcome mail with magic-link (best-effort). - internal/templates/email/add_user_welcome.{de,en}.html: new template with magic-link CTA + base-URL fallback + firm-name placeholder. Editable through the existing /admin/email-templates editor (admin-overridable via DB). - internal/services/email_template_*.go: register 'add_user_welcome' as a fourth canonical key, defaultSubjects entry, sample data, variable contract (6 vars). - internal/services/mail_service_test.go: TestRenderTemplateAddUserWelcome pins both langs render with magic-link + firm + matching subject. - internal/handlers/admin_users.go: handleAdminCreateFullUser POST /api/admin/users/full. Fills inviter fields from auth.uid() server-side (never trusts the request body). Error map: 503 (unavailable), 409 (email exists / already onboarded), 400 (invalid input), 403 (domain not on whitelist), 500 (other). - internal/handlers/handlers.go: route registered behind adminGate. - cmd/server/main.go: LoadSupabaseAdminClient + users.SetAddUserDeps + boot-log line so the deployer knows whether the path is active. - frontend/src/admin-team.tsx: "Konto direkt anlegen" button + admin-add-full-modal with email/name/office/profession/job_title/lang fields + send-welcome checkbox (default on). - frontend/src/client/admin-team.ts: initAddFullModal — POST to /api/admin/users/full, inline error handling for 503 / 409 / generic, optimistic insert into users[] on success, name auto-fills from email local-part on blur. - i18n: +20 keys (admin.team.add.full + admin.team.add_full.*) × DE + EN. Design picks honoured: Supabase Admin API path (Q1), welcome email default on (Q2), two-step with best-effort rollback (Q3), job_title default 'Associate' (Q4), profession default 'associate' (Q5). Trade-off #3 from §6 (privileged credential broadens trust surface) accepted by m via head. go build && go test -short ./internal/... + bun run build all green. |
|||
|
|
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.
|
||
|
|
495e519475 |
feat(t-paliad-065): firm-agnostic branding via single FIRM_NAME constant
Paliad ships firm-agnostic per CLAUDE.md ("survives firm renames") but
landing copy, email templates, page titles, and form placeholders still
hard-coded "Hogan Lovells" / "HL Patents". Replaces every user-facing
firm reference with a single source of truth: internal/branding.Name on
the server and frontend/src/branding.ts in the bundle, both reading
FIRM_NAME at startup/build time and defaulting to "HLC".
Server: branding package + boot log; auth, invite, admin_users error
strings; courts/offices/models comments; mail templates thread
{{.Firm}} via injected payload default. Files handler keeps the
upstream "HL Patents Style.dotm" path (must match mWorkRepo's blob
name) but renders the user-visible DownloadName from branding.Name.
Frontend: branding.ts read via Bun.build define so process.env.FIRM_NAME
is statically substituted into client bundles (no runtime process
reference); index/login/downloads/kostenrechner/Sidebar/ProjectFormFields
and every i18n.ts string templated against ${FIRM}.
ALLOWED_EMAIL_DOMAINS whitelist intentionally untouched — email
domains and display name rotate independently.
Verified: go build/vet/test clean; bun run build clean; FIRM_NAME=Acme
override produces "Acme" in HTML and JS bundles end-to-end.
|
||
|
|
765bfe0648 |
feat(t-paliad-064): bundled-digest reminder service + settings UI (PR-3/4)
Replaces the per-deadline reminder model (overdue / tomorrow / due_today_evening / weekly templates and four per-kind send paths) with one bundled digest per (user, slot, local-date) — owner + project leads + global_admins as audience tiers, three category sections per email. Service rewrite (internal/services/reminder_service.go): - RunOnce iterates users, evaluates morning/evening slot per user's tz, calls runSlotForUser for each match. - runSlotForUser checks the slot+date dedup (migration 025), fetches the three pending-deadline categories visible to u (overdue / due_today / due_warning at u.reminder_warning_offset_days), composes a digest, and inserts the dedup row only on successful send. - Audience filter applied per row in Go: due_warning to owner/lead, due_today to owner/lead (+global_admin in evening), overdue to owner/global_admin (NOT lead — system failure escalates past the team). - Subject ladder: ÜBERFÄLLIG / SYSTEMAUSFALL when overdues are in the bundle; DRINGEND on evening when due_today still pending; "Frist- Erinnerung: N offen" otherwise. EN equivalents. - Retired sendPerFrist, sendWeekly, deliverFristReminder, deliverWeekly, buildSubject, slotForKind, matchesLocalDueDate. Templates: - Added deadline_digest.html with three category sections (red/amber/ neutral), DRINGEND wording on evening, IsOtherOwner attribution row. - Removed deadline_reminder.html, deadline_due_today.html, deadline_weekly.html. User schema (Go side): - models.User gains ReminderWarningOffsetDays (int, default 7) and EscalationContactID (*uuid.UUID, nullable). - userColumns SELECT updated; UpdateProfileInput accepts the new offset with 1..30 validation. Settings → Notifications UI (PR-4): - New reminder categories: overdue / due_today / due_warning. Legacy toggles (tomorrow, due_today_evening, weekly) removed and the legacy pref keys are explicitly deleted from the email_preferences object on next save so they don't linger. - New "Vorwarnung (Tage vorher)" input (1..30, required), wired into the PATCH /api/me payload as reminder_warning_offset_days. - Times-section copy refreshed: "Morgen-Slot" / "Abend-Slot (Eskalation)" with new hint text reflecting the bundled-digest model. - DE + EN i18n strings added/updated. Tests: - TestCategorize, TestVisibleForCategory, TestBuildDigestSubject lock the boundary, recipient-rule, and subject-ladder logic. - TestRunSlotForUser (live DB, skipped without TEST_DATABASE_URL) covers the morning/evening flow, slot+date dedup, and off-slot tick. - TestRunSlotForUser_EmptyDigest enforces the no-spam rule. - TestDeliverDigest_RendersTemplate runs the new template on the digestRow shape so a typo would fail before any SMTP I/O. - TestRenderTemplateDeadlineDigest replaces the deleted reminder/weekly template tests. go build/vet/test + bun run build all clean. |
||
|
|
b21dacf15c |
feat(t-paliad-063): adopt HLC brand palette across paliad
Replace ad-hoc lime/forest-green system with the official 4-color HLC palette. Lime + midnight are the primary pair; cyan + cream supporting. Tokens - :root now exposes --hlc-lime, --hlc-midnight, --hlc-cyan, --hlc-cream plus channel-token siblings (--hlc-*-rgb) so tints can be expressed as rgb(var(--hlc-*-rgb) / a) without hex literals. - --color-bg → cream, --color-text/--color-hero-bg → midnight, --color-accent → lime, --color-accent-dark → midnight (foreground on lime; passes WCAG AA where #fff failed). - New --sidebar-* tokens for the dark sidebar surface. Sweep (frontend/src/styles/global.css) - Replaced every hard-coded #c6f41c / #65a30d / #84cc16 / #b8e616 / #4d7c0f / #1a2e1a / #1a1a2e / #1a2e05 with the matching var(...). - rgba(101,163,13,a) and rgba(198,244,28,a) collapsed to rgb(var(--hlc-lime-rgb) / a). - text-on-lime now uses var(--color-accent-dark) instead of #fff; btn-danger keeps white on red. Sidebar reskin (cronus's audit, F-30) - Background: midnight; text: cream (muted via cream-channel alpha); active/hover: lime. Border + hover use cream-channel alphas so no rgba hex creep on the dark surface. Brand assets - manifest.json theme_color → lime, background_color → cream. - icon.svg / icon-maskable.svg base recoloured to lime + midnight glyph. - 32× <meta name="theme-color"> across pages updated to #BFF355. - Email templates (base.html, invitation.html) lime accent updated; mail_service_test.go expectation tracks the new hex. Deferred / out of scope - PNG icons under public/icons/ are baked artefacts; regen left to the next deploy. - Categorical chip colours (office tints, traffic-light red/amber/green, termin-type hues) are functional, not brand, and deliberately untouched. - Dark mode is not in scope. Verified - bun run build clean. - go build ./... clean; mail render tests pass. - Visual sweep at 1280×900 against frontend/dist via Playwright on /, /login, /dashboard, /projects, /agenda, /team, /fristenrechner, /glossary — sidebar midnight + lime active, cream page bg, white cards, midnight text on lime CTAs. Supersedes audit findings F-14, F-30, F-31. |
||
|
|
e68ff5b434 |
feat(reminders): per-user send times + due-today evening sweep (t-paliad-048)
Reminders used to fire whenever the hourly ticker happened to scan after a user's first eligible event — m got mail at 02:28. We now gate delivery to a user-chosen hour-of-day in their local timezone. * Migration 022 adds reminder_morning_time / reminder_evening_time / reminder_timezone (defaults 09:00, 16:00, Europe/Berlin). * New "due_today_evening" reminder kind with its own template — fires only for due_date = today AND status = pending, in the evening slot. * Reminder service computes user-local hour each tick and skips users outside their slot. SQL widens to a 3-day band; in-process filter narrows to per-user local date. * Settings → Notifications gains time inputs and a timezone field. * Tests: pure (inSlot, slotForKind, matchesLocalDueDate) plus a live-DB TestReminderSlots covering morning, evening, outside-slot, and the completed-deadline case. |
||
|
|
d4abfb7299 |
fix(reminders): align SQL aliases with renamed struct tags
The German→English rename (t-paliad-025) renamed the projects table and ReminderService struct fields, but the SQL aliases in sendPerFrist / sendWeekly still spelled `frist_title`, `akte_aktenzeichen`, and `akte_title`. sqlx.SelectContext could not map them to the `deadline_title` / `project_reference` / `project_title` `db:` tags, so every hourly reminder scan returned a "missing destination name" error and emails silently stopped going out. This commit: * renames struct fields AkteAktenzeichen/AkteTitle on fristReminderRow and weeklyRow to ProjectReference/ProjectTitle and updates the `db:` tags to project_reference / project_title. * rewrites the SELECT aliases (deadline_title, project_reference, project_title) to match. * propagates the new keys through deliverFristReminder / deliverWeekly into the email template data and renames the matching variables in deadline_reminder.html and deadline_weekly.html. * updates mail_service_test.go fixtures to the new keys. * adds TestSendPerFrist_ScansCleanly — a live-DB regression test (skips without TEST_DATABASE_URL) that seeds a project + deadline and asserts sendPerFrist / sendWeekly scan without error, so a future tag/alias drift fails CI instead of going silent. |
||
|
|
49c6bc75ca |
refactor(rename): handler functions, routes, legacy 301 redirects
Second rename pass closing the backend cleanup:
* handler functions (handleListProjekte, handleCreateFrist, …) renamed
to English equivalents so every symbol in the handler package matches
the URL/entity it serves.
* services.FristStatusFilter + filter constants renamed to
DeadlineStatusFilter / DeadlineFilterOverdue etc.
* services.TerminListFilter / TerminCalDAVPusher / TerminSummaryCounts
renamed to AppointmentListFilter / AppointmentCalDAVPusher /
AppointmentSummaryCounts.
* GlossarTerm/GlossarSuggestion/glossarTerms → Glossary*.
* CourtsFeedback/CourtsResponse (formerly Gerichte*).
* handlers.Services.{Projekt,Parteien,Frist,Termin,Notiz,Dezernat} →
{Project,Party,Deadline,Appointment,Note,Department}; dbServices
struct + consumers likewise.
* email templates: {{.FristURL}} → {{.DeadlineURL}}, {{.FristenURL}} →
{{.DeadlinesURL}}.
* links.go category IDs: gerichte → courts.
* cmd/server/main.go local vars: projektSvc/terminSvc/dezernatSvc →
projectSvc/appointmentSvc/departmentSvc.
Routes:
* removed all /api/akten alias routes (API clients use /api/projects now).
* removed /api/akten/*/deadlines, /*/notes, /*/parties, /*/appointments,
/*/checklists, /*/events, /*/summary alias variants.
* new internal/handlers/redirects.go registers 301 Moved Permanently
redirects for every legacy German GET path: /akten, /projekte, /fristen,
/termine, /notizen, /einstellungen, /checklisten, /dezernate, /parteien,
/gerichte, /glossar. Sub-paths + query strings are preserved so old
bookmarks keep working.
Kept in German (product names, per task spec):
* /tools/fristenrechner, /tools/kostenrechner, /tools/gebuehrentabellen
* FristenrechnerService / KostenrechnerService types
* User.Dezernat + paliad.users.dezernat free-text legacy column (separate
from the new paliad.departments entity).
go build / vet / test clean.
|
||
|
|
11217f7bfa |
feat: email service — SMTP + deadline reminders + invitations (t-paliad-021)
- internal/services/mail_service.go: SMTP/TLS sender (implicit TLS on 465), html/template rendering, branded base layout + content templates, silent no-op when SMTP_* unset. - internal/services/reminder_service.go: hourly scanner for Fristen that are overdue / due tomorrow / due within the week (Monday digest). Dedup via paliad.reminder_log (24h window). - internal/services/invite_service.go: POST /api/invite flow with domain whitelist, in-memory 10/day/user rate limit, audit row in paliad.invitations. - internal/handlers/invite.go: POST + GET /api/invite handlers. - Sidebar "Kolleg:in einladen" button + modal on every page. - migration 016: paliad.reminder_log, paliad.invitations, users.lang column. - docker-compose: SMTP_* + PALIAD_BASE_URL env vars. - docs/feature-roadmap.md: documented Supabase auth-SMTP routing as open question; current pilot keeps identity mails on Supabase default sender. Rationale: get Paliad off Supabase's best-effort outbound for the inbox-facing stuff (reminders, invitations) and move deadline nudges from passive dashboard to active email. Custom Supabase auth SMTP is blocked on the shared ydb.youpc.org instance — deferred until Paliad has its own project or GoTrue webhook relay. |