From 495e51947570ee984b55a6ace445906d039e9b1e Mon Sep 17 00:00:00 2001 From: m Date: Tue, 28 Apr 2026 22:44:06 +0200 Subject: [PATCH] feat(t-paliad-065): firm-agnostic branding via single FIRM_NAME constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .claude/CLAUDE.md | 5 +- cmd/server/main.go | 5 ++ frontend/build.ts | 16 +++++ frontend/src/branding.ts | 31 ++++++++++ frontend/src/client/i18n.ts | 62 ++++++++++--------- frontend/src/components/ProjectFormFields.tsx | 9 +-- frontend/src/components/Sidebar.tsx | 3 +- frontend/src/downloads.tsx | 9 ++- frontend/src/index.tsx | 13 ++-- frontend/src/kostenrechner.tsx | 3 +- frontend/src/login.tsx | 3 +- frontend/src/styles/global.css | 8 ++- internal/branding/firm.go | 32 ++++++++++ internal/branding/firm_test.go | 32 ++++++++++ internal/handlers/admin_users.go | 3 +- internal/handlers/auth.go | 14 +++-- internal/handlers/courts.go | 7 ++- internal/handlers/files.go | 13 +++- internal/handlers/invite.go | 5 +- internal/handlers/offices.go | 2 +- internal/models/models.go | 2 +- internal/offices/offices.go | 4 +- internal/services/mail_service.go | 6 ++ internal/services/mail_service_test.go | 4 ++ internal/templates/email/invitation.html | 8 +-- 25 files changed, 229 insertions(+), 70 deletions(-) create mode 100644 frontend/src/branding.ts create mode 100644 internal/branding/firm.go create mode 100644 internal/branding/firm_test.go diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 42ebff7..a8dfb4e 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -29,7 +29,7 @@ Paliad — the patent paladin. All-in-one patent practice platform for HLC (form - **Backend:** Go API, `net/http`, `sqlx` for DB access - **Migrations:** `golang-migrate/migrate/v4` with SQL files embedded via `embed.FS`; applied at server startup before the HTTP listener binds. Migration tracker is `paliad.paliad_schema_migrations` (avoids collision with other apps on the shared `public.schema_migrations`). - **Database:** youpc Supabase Postgres (port 11833), `paliad` schema. Team-based RLS via `paliad.can_see_project(project_id)` — visibility determined by team membership (direct + inherited up the project tree). See `docs/design-data-model-v2.md`. -- **Auth:** Supabase (youpc instance) — password-based, `@hoganlovells.com` gate (TBD: update to `@hlc.*` post-merger) +- **Auth:** Supabase (youpc instance) — password-based, email-domain gate via `ALLOWED_EMAIL_DOMAINS` (default `hoganlovells.com,hlc.com,hlc.de`). The whitelist references real DNS domains and rotates independently from `FIRM_NAME` (display name). - **Hosting:** Dokploy compose on mlake (72.62.52.189), compose ID `Zx147ycurfYagKRl_Zzyo` - **Domains on Dokploy:** paliad.de (primary, Let's Encrypt), patholo.de (legacy), patholo.msbls.de (internal) - **Deploy:** push to main → Gitea webhook → Dokploy auto-deploy @@ -47,6 +47,7 @@ Paliad — the patent paladin. All-in-one patent practice platform for HLC (form | `PALIAD_BASE_URL` | optional | Public origin used in email links. Defaults to `https://paliad.de`; override for staging/preview. | | `SMTP_HOST` / `SMTP_PORT` / `SMTP_USERNAME` / `SMTP_PASSWORD` / `SMTP_FROM` / `SMTP_FROM_NAME` / `SMTP_USE_TLS` | for email | SMTP credentials for Paliad's transactional mail (reminders, invitations). Port 465 uses implicit TLS. `MailService` silently no-ops when any required var is missing — the server still boots for knowledge-platform-only deployments. | | `ANTHROPIC_API_KEY` | not used today | Reserved for Phase H (AI Frist-Extraktion) which is deferred per m's 2026-04-16 decision. Do not set. | +| `FIRM_NAME` | optional (default `HLC`) | Display name of the firm Paliad is being branded for in this deployment. Read once at process start by `internal/branding.Name` (Go) and inlined into client bundles by `frontend/build.ts` (TypeScript). Powers every user-facing surface — landing hero, page titles, login hint, Downloads page, footer, invitation/reminder email bodies. The `ALLOWED_EMAIL_DOMAINS` whitelist is a separate concern (real DNS domains, not display name) and rotates independently. | > *Note on `DATABASE_URL`:* "Work without DB" ≠ "ungated". All knowledge-platform routes (Kostenrechner, Glossar, Links, Gebührentabellen, Checklisten, Gerichte, Downloads) are still behind the auth gate (302 to `/login` for anon visitors); only `/`, `/login`, `/logout`, and `/assets/*` are public. The `gateOnboarded` middleware additionally blocks unonboarded users from app pages but does NOT gate the knowledge-platform pages. @@ -54,7 +55,7 @@ Paliad — the patent paladin. All-in-one patent practice platform for HLC (form - **Gitea:** `mAi/paliad` on mgit.msbls.de (renamed from mAi/patholo — auto-redirects) - **DNS:** paliad.de → 72.62.52.189 (via Hostinger) -- **Branding:** lime green accent (`#c6f41c`), sidebar layout, DE/EN i18n +- **Branding:** lime green accent (`#c6f41c`), sidebar layout, DE/EN i18n. Firm-agnostic: every user-facing firm reference is rendered from `internal/branding.Name` (Go) / `frontend/src/branding.ts` (TypeScript). Default "HLC", overridable via `FIRM_NAME`. See t-paliad-065. ## Phase status diff --git a/cmd/server/main.go b/cmd/server/main.go index 051e219..3e64e35 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -16,6 +16,7 @@ import ( _ "time/tzdata" "mgit.msbls.de/m/patholo/internal/auth" + "mgit.msbls.de/m/patholo/internal/branding" "mgit.msbls.de/m/patholo/internal/db" "mgit.msbls.de/m/patholo/internal/handlers" "mgit.msbls.de/m/patholo/internal/services" @@ -27,6 +28,10 @@ func main() { port = "8080" } + // Surface the firm name in the boot log so a deployer can confirm + // FIRM_NAME took effect without curl-ing a rendered page. + log.Printf("branding: firm=%q (override with FIRM_NAME)", branding.Name) + supabaseURL := os.Getenv("SUPABASE_URL") supabaseAnonKey := os.Getenv("SUPABASE_ANON_KEY") if supabaseURL == "" || supabaseAnonKey == "" { diff --git a/frontend/build.ts b/frontend/build.ts index be3ec53..bde6df1 100644 --- a/frontend/build.ts +++ b/frontend/build.ts @@ -55,11 +55,20 @@ const BUILD_FORMAT = "iife" as const; // today with minify: true) or `(function(){...})()`. Match either prologue. const IIFE_PROLOGUE = /^(\(\(\)\s*=>\s*\{|\(function\s*\(\s*\)\s*\{)/; +// Resolve FIRM_NAME once so both the client bundle's `define` substitution +// and the server-side TSX render see the same value. Mirrors the server's +// internal/branding/firm.go default — the two MUST stay in sync because +// users compare a rendered email body against a rendered HTML page and a +// drifted default would produce two different firm names per deploy. +const FIRM_NAME = (process.env.FIRM_NAME ?? "").trim() || "HLC"; + async function build() { // Clean dist/ await rm(DIST, { recursive: true, force: true }); await mkdir(join(DIST, "assets"), { recursive: true }); + console.log(`branding: firm="${FIRM_NAME}" (override with FIRM_NAME env)`); + // Bundle client-side JS const result = await Bun.build({ entrypoints: [ @@ -107,6 +116,13 @@ async function build() { // depends on IIFE wrapping. Reuses the single-source-of-truth constant // so the post-build guard below can detect a format swap. format: BUILD_FORMAT, + // Inline the resolved firm name into every browser bundle. branding.ts + // reads `process.env.FIRM_NAME`, which Bun's bundler does NOT replace by + // default for browser targets — so without `define`, client code would + // see undefined and fall back to "HLC" regardless of FIRM_NAME. + define: { + "process.env.FIRM_NAME": JSON.stringify(FIRM_NAME), + }, }); if (!result.success) { diff --git a/frontend/src/branding.ts b/frontend/src/branding.ts new file mode 100644 index 0000000..1d6709b --- /dev/null +++ b/frontend/src/branding.ts @@ -0,0 +1,31 @@ +// frontend/src/branding.ts — single source of truth for the firm name +// Paliad's UI renders. Mirrors internal/branding/firm.go on the server. +// +// At build time this resolves twice: +// 1. In the server-side render path (build.ts → renderXxx() returning HTML) +// Bun is running under Node, so process.env.FIRM_NAME is the real env +// var the deployer set; this file is loaded as a regular ESM module. +// 2. In the bundled client modules (e.g. client/i18n.ts) Bun.build replaces +// `process.env.FIRM_NAME` with a string literal via the `define` option +// configured in build.ts. Browsers never see process.env — every +// reference is statically substituted before the bundle is emitted. +// +// Both paths default to "HLC" when FIRM_NAME is unset. +// +// IMPORTANT: do NOT guard the read with `typeof process !== "undefined"` or +// any check on `process` itself. The minifier rewrites that guard into a +// short-string lexical comparison (`typeof process < "u"`) which evaluates +// false in the browser and would short-circuit the value back to "HLC" even +// when define has substituted the env var. The bare `process.env.FIRM_NAME` +// reference is only safe because build.ts's `define` rewrites it away +// completely for browser bundles. +// +// Why a runtime constant rather than i18n placeholder substitution: every +// Paliad surface (HTML title, hero headline, email body, PDF footer) has the +// firm name baked in literally; threading {{firm}} placeholders + a +// formatter through every t() call would be a far larger churn for the same +// firm-agnostic outcome. Re-deploying with FIRM_NAME=Acme rebuilds every +// asset with the new name in one step. + +const RAW: string = (process.env.FIRM_NAME ?? "").trim(); +export const FIRM: string = RAW !== "" ? RAW : "HLC"; diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index b2e7bbc..904b618 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -1,6 +1,8 @@ // i18n — Client-side internationalization for paliad // Supports DE (German) and EN (English) +import { FIRM } from "../branding"; + export type Lang = "de" | "en"; const STORAGE_KEY = "paliad-lang"; @@ -62,13 +64,13 @@ const translations: Record> = { "footer.text": "\u00a9 2026 Paliad \u2014 ein Werkzeug von", // Landing page - "index.title": "Paliad \u2014 Patentwissen f\u00fcr Hogan Lovells", - "index.hero.accent": "f\u00fcr Hogan Lovells", - "index.hero.sub": "Leitf\u00e4den, Vorlagen und Dokumente f\u00fcr das HL Patent-Team.", + "index.title": `Paliad \u2014 Patentwissen f\u00fcr ${FIRM}`, + "index.hero.accent": `f\u00fcr ${FIRM}`, + "index.hero.sub": `Leitf\u00e4den, Vorlagen und Dokumente f\u00fcr das ${FIRM} Patent-Team.`, "index.guides.title": "Leitf\u00e4den", "index.guides.desc": "Praxisleitf\u00e4den zu Verfahren vor dem EPA, BPatG und UPC. Schritt-f\u00fcr-Schritt-Anleitungen f\u00fcr typische Workflows.", "index.templates.title": "Vorlagen", - "index.templates.desc": "Standardisierte Vorlagen f\u00fcr Schrifts\u00e4tze, Korrespondenz und interne Dokumente. HL Patents Style Guide.", + "index.templates.desc": `Standardisierte Vorlagen f\u00fcr Schrifts\u00e4tze, Korrespondenz und interne Dokumente. ${FIRM} Patents Style Guide.`, "index.documents.title": "Dokumente", "index.documents.desc": "Referenzmaterialien, Checklisten und Arbeitshilfen f\u00fcr den Praxisalltag im Patentrecht.", "index.tools": "Werkzeuge", @@ -79,8 +81,8 @@ const translations: Record> = { "index.glossar.title": "Patentglossar", "index.glossar.desc": "Zweisprachiges DE/EN-Glossar der wichtigsten Begriffe im Patentrecht. Durchsuchbar nach Kategorien.", "index.downloads": "Downloads", - "index.style.title": "HL Patents Style", - "index.style.desc": "Word-Vorlage im HL Patents Style. Formatierung, Schriftarten und Makros f\u00fcr standardisierte Schrifts\u00e4tze.", + "index.style.title": `${FIRM} Patents Style`, + "index.style.desc": `Word-Vorlage im ${FIRM} Patents Style. Formatierung, Schriftarten und Makros f\u00fcr standardisierte Schrifts\u00e4tze.`, "index.offices": "Standorte", "index.office.munich": "M\u00fcnchen", "index.office.duesseldorf": "D\u00fcsseldorf", @@ -102,7 +104,7 @@ const translations: Record> = { "login.confirm.placeholder": "Passwort wiederholen", "login.minchars": "Mind. 8 Zeichen", "login.register.submit": "Registrieren", - "login.hint": "Nur f\u00fcr autorisierte HLC-E-Mail-Adressen.", + "login.hint": `Nur f\u00fcr autorisierte ${FIRM}-E-Mail-Adressen.`, "login.error.connection": "Verbindungsfehler. Bitte versuchen Sie es erneut.", "login.error.mismatch": "Passw\u00f6rter stimmen nicht \u00fcberein.", "login.error.minlength": "Passwort muss mindestens 8 Zeichen lang sein.", @@ -211,9 +213,9 @@ const translations: Record> = { // Downloads "downloads.title": "Downloads \u2014 Paliad", "downloads.heading": "Downloads", - "downloads.subtitle": "Dateien und Vorlagen f\u00fcr das HL Patent-Team.", - "downloads.style.title": "HL Patents Style", - "downloads.style.desc": "Word-Vorlage im HL Patents Style. Formatierung, Schriftarten und Makros f\u00fcr standardisierte Schrifts\u00e4tze.", + "downloads.subtitle": `Dateien und Vorlagen f\u00fcr das ${FIRM} Patent-Team.`, + "downloads.style.title": `${FIRM} Patents Style`, + "downloads.style.desc": `Word-Vorlage im ${FIRM} Patents Style. Formatierung, Schriftarten und Makros f\u00fcr standardisierte Schrifts\u00e4tze.`, "downloads.btn": "Herunterladen", // Links @@ -396,7 +398,7 @@ const translations: Record> = { "gerichte.field.fax": "Fax", "gerichte.field.filing": "Einreichung", "gerichte.field.notes": "Praktische Hinweise", - "gerichte.field.hlContact": "HL-Ansprechpartner", + "gerichte.field.hlContact": `${FIRM}-Ansprechpartner`, "gerichte.feedback.btn": "Korrektur vorschlagen", "gerichte.feedback.title": "Korrektur vorschlagen", "gerichte.feedback.court": "Gericht", @@ -451,7 +453,7 @@ const translations: Record> = { "akten.field.title": "Titel", "akten.field.title.placeholder": "Kurzbezeichnung des Mandats", "akten.field.ref": "Aktenzeichen", - "akten.field.ref.placeholder": "z.\u202fB. HL-2026-0042", + "akten.field.ref.placeholder": `z.\u202fB. ${FIRM}-2026-0042`, "akten.field.office": "Federf\u00fchrendes B\u00fcro", "akten.field.status": "Status", "akten.field.court": "Gericht (optional)", @@ -874,10 +876,10 @@ const translations: Record> = { "projekte.field.title": "Titel", "projekte.field.title.placeholder": "z.B. Siemens AG | Siemens v. Huawei | EP 1 234 567", "projekte.field.reference": "Interne Referenz (optional)", - "projekte.field.reference.placeholder": "z.B. HL-2026-0042", + "projekte.field.reference.placeholder": `z.B. ${FIRM}-2026-0042`, "projekte.field.client_number": "Client-Nr. (7 Ziffern)", "projekte.field.matter_number": "Matter-Nr. (7 Ziffern)", - "projekte.field.clientmatter.hint": "HLC-Billing-Nummern. Format CCCCCCC.MMMMMMM. Client-Nr. wird an Unterprojekte vererbt (\u00fcberschreibbar).", + "projekte.field.clientmatter.hint": `${FIRM}-Billing-Nummern. Format CCCCCCC.MMMMMMM. Client-Nr. wird an Unterprojekte vererbt (\u00fcberschreibbar).`, "projekte.field.billing_reference": "Billing-Referenz (optional)", "projekte.field.netdocuments_url": "netDocuments-URL (optional)", "projekte.field.industry": "Branche", @@ -1014,7 +1016,7 @@ const translations: Record> = { // Invitation modal (sidebar) "invite.button": "Kolleg:in einladen", "invite.modal.title": "Kolleg:in zu Paliad einladen", - "invite.modal.body": "Senden Sie eine Einladung an eine HLC-E-Mail-Adresse. Die Empf\u00e4nger:in erh\u00e4lt einen Registrierungslink.", + "invite.modal.body": `Senden Sie eine Einladung an eine ${FIRM}-E-Mail-Adresse. Die Empf\u00e4nger:in erh\u00e4lt einen Registrierungslink.`, "invite.modal.email": "E-Mail-Adresse", "invite.modal.message": "Pers\u00f6nliche Nachricht (optional)", "invite.modal.message.placeholder": "Hi, ich nutze Paliad f\u00fcr die Aktenverwaltung \u2014 schau es dir mal an.", @@ -1307,13 +1309,13 @@ const translations: Record> = { "footer.text": "\u00a9 2026 Paliad \u2014 a tool by", // Landing page - "index.title": "Paliad \u2014 Patent Knowledge for Hogan Lovells", - "index.hero.accent": "for Hogan Lovells", - "index.hero.sub": "Guides, templates, and documents for the HL patent team.", + "index.title": `Paliad \u2014 Patent Knowledge for ${FIRM}`, + "index.hero.accent": `for ${FIRM}`, + "index.hero.sub": `Guides, templates, and documents for the ${FIRM} patent team.`, "index.guides.title": "Guides", "index.guides.desc": "Practical guides for proceedings before the EPO, Federal Patent Court, and UPC. Step-by-step instructions for typical workflows.", "index.templates.title": "Templates", - "index.templates.desc": "Standardised templates for briefs, correspondence, and internal documents. HL Patents Style Guide.", + "index.templates.desc": `Standardised templates for briefs, correspondence, and internal documents. ${FIRM} Patents Style Guide.`, "index.documents.title": "Documents", "index.documents.desc": "Reference materials, checklists, and practical aids for day-to-day patent practice.", "index.tools": "Tools", @@ -1324,8 +1326,8 @@ const translations: Record> = { "index.glossar.title": "Patent Glossary", "index.glossar.desc": "Bilingual DE/EN glossary of key patent law terminology. Searchable by category.", "index.downloads": "Downloads", - "index.style.title": "HL Patents Style", - "index.style.desc": "Word template in HL Patents style. Formatting, fonts, and macros for standardised briefs.", + "index.style.title": `${FIRM} Patents Style`, + "index.style.desc": `Word template in ${FIRM} Patents style. Formatting, fonts, and macros for standardised briefs.`, "index.offices": "Offices", "index.office.munich": "Munich", "index.office.duesseldorf": "D\u00fcsseldorf", @@ -1347,7 +1349,7 @@ const translations: Record> = { "login.confirm.placeholder": "Repeat password", "login.minchars": "Min. 8 characters", "login.register.submit": "Register", - "login.hint": "Only for authorised HLC email addresses.", + "login.hint": `Only for authorised ${FIRM} email addresses.`, "login.error.connection": "Connection error. Please try again.", "login.error.mismatch": "Passwords do not match.", "login.error.minlength": "Password must be at least 8 characters.", @@ -1456,9 +1458,9 @@ const translations: Record> = { // Downloads "downloads.title": "Downloads \u2014 Paliad", "downloads.heading": "Downloads", - "downloads.subtitle": "Files and templates for the HL patent team.", - "downloads.style.title": "HL Patents Style", - "downloads.style.desc": "Word template in HL Patents style. Formatting, fonts, and macros for standardised briefs.", + "downloads.subtitle": `Files and templates for the ${FIRM} patent team.`, + "downloads.style.title": `${FIRM} Patents Style`, + "downloads.style.desc": `Word template in ${FIRM} Patents style. Formatting, fonts, and macros for standardised briefs.`, "downloads.btn": "Download", // Links @@ -1641,7 +1643,7 @@ const translations: Record> = { "gerichte.field.fax": "Fax", "gerichte.field.filing": "Filing", "gerichte.field.notes": "Practical notes", - "gerichte.field.hlContact": "HL contact", + "gerichte.field.hlContact": `${FIRM} contact`, "gerichte.feedback.btn": "Suggest a correction", "gerichte.feedback.title": "Suggest a correction", "gerichte.feedback.court": "Court", @@ -1696,7 +1698,7 @@ const translations: Record> = { "akten.field.title": "Title", "akten.field.title.placeholder": "Short name for the matter", "akten.field.ref": "Reference number", - "akten.field.ref.placeholder": "e.g. HL-2026-0042", + "akten.field.ref.placeholder": `e.g. ${FIRM}-2026-0042`, "akten.field.office": "Owning office", "akten.field.status": "Status", "akten.field.court": "Court (optional)", @@ -2115,10 +2117,10 @@ const translations: Record> = { "projekte.field.title": "Title", "projekte.field.title.placeholder": "e.g. Siemens AG | Siemens v. Huawei | EP 1 234 567", "projekte.field.reference": "Internal reference (optional)", - "projekte.field.reference.placeholder": "e.g. HL-2026-0042", + "projekte.field.reference.placeholder": `e.g. ${FIRM}-2026-0042`, "projekte.field.client_number": "Client no. (7 digits)", "projekte.field.matter_number": "Matter no. (7 digits)", - "projekte.field.clientmatter.hint": "HLC billing numbers. Format CCCCCCC.MMMMMMM. Client no. is inherited by sub-projects (overridable).", + "projekte.field.clientmatter.hint": `${FIRM} billing numbers. Format CCCCCCC.MMMMMMM. Client no. is inherited by sub-projects (overridable).`, "projekte.field.billing_reference": "Billing reference (optional)", "projekte.field.netdocuments_url": "netDocuments URL (optional)", "projekte.field.industry": "Industry", @@ -2255,7 +2257,7 @@ const translations: Record> = { // Invitation modal (sidebar) "invite.button": "Invite a colleague", "invite.modal.title": "Invite a colleague to Paliad", - "invite.modal.body": "Send an invitation to an HLC email address. The recipient will receive a registration link.", + "invite.modal.body": `Send an invitation to an ${FIRM} email address. The recipient will receive a registration link.`, "invite.modal.email": "Email address", "invite.modal.message": "Personal message (optional)", "invite.modal.message.placeholder": "Hi, I'm using Paliad for matter management \u2014 take a look.", diff --git a/frontend/src/components/ProjectFormFields.tsx b/frontend/src/components/ProjectFormFields.tsx index ecfc990..00d94d6 100644 --- a/frontend/src/components/ProjectFormFields.tsx +++ b/frontend/src/components/ProjectFormFields.tsx @@ -1,4 +1,5 @@ import { h } from "../jsx"; +import { FIRM } from "../branding"; // Reusable Project form body. Renders the field grid only — the surrounding //
, submit/cancel buttons and the form-msg paragraph belong to the @@ -55,7 +56,7 @@ export function ProjectFormFields(): string { @@ -83,8 +84,8 @@ export function ProjectFormFields(): string {

- HLC-Billing-Nummern. Format CCCCCCC.MMMMMMM. Client-Nr. wird an Unterprojekte vererbt - (überschreibbar). + {`${FIRM}-Billing-Nummern. Format CCCCCCC.MMMMMMM. Client-Nr. wird an Unterprojekte vererbt + (überschreibbar).`}

@@ -101,7 +102,7 @@ export function ProjectFormFields(): string {
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 4d6aac3..0485056 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,4 +1,5 @@ import { h, Fragment } from "../jsx"; +import { FIRM } from "../branding"; const ICON_HOME = ''; const ICON_CALC = ''; @@ -196,7 +197,7 @@ export function Sidebar({ currentPath, authenticated = true }: SidebarProps): st

- Senden Sie eine Einladung an eine HLC-E-Mail-Adresse. Die Empfänger:in erhält einen Registrierungslink. + {`Senden Sie eine Einladung an eine ${FIRM}-E-Mail-Adresse. Die Empfänger:in erhält einen Registrierungslink.`}

diff --git a/frontend/src/downloads.tsx b/frontend/src/downloads.tsx index 49e0706..524c757 100644 --- a/frontend/src/downloads.tsx +++ b/frontend/src/downloads.tsx @@ -3,6 +3,7 @@ import { Sidebar } from "./components/Sidebar"; import { BottomNav } from "./components/BottomNav"; import { Footer } from "./components/Footer"; import { PWAHead } from "./components/PWAHead"; +import { FIRM } from "./branding"; const ICON_WORD = ''; @@ -15,14 +16,16 @@ interface DownloadFile { descDE: string; } +// URL slug stays "hl-patents-style.dotm" — it's a stable public identifier +// that bookmarks point at; the user-facing title/description are firm-agnostic. const files: DownloadFile[] = [ { href: "/files/hl-patents-style.dotm", icon: ICON_WORD, titleKey: "downloads.style.title", - titleDE: "HL Patents Style", + titleDE: `${FIRM} Patents Style`, descKey: "downloads.style.desc", - descDE: "Word-Vorlage im HL Patents Style. Formatierung, Schriftarten und Makros für standardisierte Schriftsätze.", + descDE: `Word-Vorlage im ${FIRM} Patents Style. Formatierung, Schriftarten und Makros für standardisierte Schriftsätze.`, }, ]; @@ -49,7 +52,7 @@ export function renderDownloads(): string {

Downloads

- Dateien und Vorlagen für das HL Patent-Team. + {`Dateien und Vorlagen für das ${FIRM} Patent-Team.`}

diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 74c91b4..bf2f685 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -2,6 +2,7 @@ import { h } from "./jsx"; import { Sidebar } from "./components/Sidebar"; import { Footer } from "./components/Footer"; import { PWAHead } from "./components/PWAHead"; +import { FIRM } from "./branding"; const ICON_BOOK = ''; const ICON_FILE = ''; @@ -24,7 +25,7 @@ export function renderIndex(): string { - Paliad — Patentwissen für Hogan Lovells + {`Paliad — Patentwissen für ${FIRM}`} @@ -33,9 +34,9 @@ export function renderIndex(): string {
-

Patent Knowledge
für Hogan Lovells

+

Patent Knowledge
{`für ${FIRM}`}

- Leitfäden, Vorlagen und Dokumente für das HL Patent-Team. + {`Leitfäden, Vorlagen und Dokumente für das ${FIRM} Patent-Team.`}

@@ -52,7 +53,7 @@ export function renderIndex(): string {

Vorlagen

-

Standardisierte Vorlagen für Schriftsätze, Korrespondenz und interne Dokumente. HL Patents Style Guide.

+

{`Standardisierte Vorlagen für Schriftsätze, Korrespondenz und interne Dokumente. ${FIRM} Patents Style Guide.`}

@@ -113,8 +114,8 @@ export function renderIndex(): string { diff --git a/frontend/src/kostenrechner.tsx b/frontend/src/kostenrechner.tsx index 9155a98..64d4ce5 100644 --- a/frontend/src/kostenrechner.tsx +++ b/frontend/src/kostenrechner.tsx @@ -3,6 +3,7 @@ import { Sidebar } from "./components/Sidebar"; import { BottomNav } from "./components/BottomNav"; import { Footer } from "./components/Footer"; import { PWAHead } from "./components/PWAHead"; +import { FIRM } from "./branding"; const ICON_CALC = ''; @@ -233,7 +234,7 @@ export function renderKostenrechner(): string {
diff --git a/frontend/src/login.tsx b/frontend/src/login.tsx index a407a8c..0e75cb7 100644 --- a/frontend/src/login.tsx +++ b/frontend/src/login.tsx @@ -2,6 +2,7 @@ import { h } from "./jsx"; import { Header } from "./components/Header"; import { Footer } from "./components/Footer"; import { PWAHead } from "./components/PWAHead"; +import { FIRM } from "./branding"; export function renderLogin(loginJs: string): string { return "" + ( @@ -44,7 +45,7 @@ export function renderLogin(loginJs: string): string { -

{"Nur f\u00FCr autorisierte HLC-E-Mail-Adressen."}

+

{`Nur f\u00FCr autorisierte ${FIRM}-E-Mail-Adressen.`}

diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 415504a..d18e2fb 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -1,7 +1,11 @@ -/* Paliad — Patent Knowledge for HLC */ +/* Paliad — Patent Knowledge platform. + Firm name is rendered at runtime from FIRM_NAME (see internal/branding + + frontend/src/branding.ts). Default: "HLC". */ :root { - /* HLC brand palette (4 colors). + /* Brand palette (4 colors). Token names use the --hlc- prefix as a + stable internal identifier — not a firm-specific reference; renaming + the prefix would touch every CSS rule for no user-visible benefit. Lime + midnight are the primary pair. Cyan + cream are supporting. */ --hlc-lime: #BFF355; --hlc-midnight: #002236; diff --git a/internal/branding/firm.go b/internal/branding/firm.go new file mode 100644 index 0000000..9f7bc2b --- /dev/null +++ b/internal/branding/firm.go @@ -0,0 +1,32 @@ +// Package branding is the single source of truth for the firm name that +// Paliad's UI, emails, and download metadata render. Paliad is firm-agnostic +// (per the project CLAUDE.md — "survives firm renames"); reading the name +// through this package keeps every surface in sync and lets a redeploy with +// a different FIRM_NAME repoint the whole product without code changes. +// +// Default is "HLC" (current firm). Override with the FIRM_NAME env var. +// +// History: until 2026-04-16 this codebase shipped "Hogan Lovells" / "HL" +// hard-coded across server templates and the frontend. The merger announced +// that month made those references stale and the rebrand to Paliad-the-name +// + branding.Name-as-runtime-value followed (t-paliad-065). +package branding + +import ( + "os" + "strings" +) + +// Name is the firm Paliad is being branded for in this deployment. Read once +// at process start so handler hot paths don't pay the env-lookup cost. +// +// Consumers must treat it as a constant for the lifetime of the process — if +// FIRM_NAME changes on disk, that's a redeploy, not a hot-reload. +var Name = resolveName() + +func resolveName() string { + if v := strings.TrimSpace(os.Getenv("FIRM_NAME")); v != "" { + return v + } + return "HLC" +} diff --git a/internal/branding/firm_test.go b/internal/branding/firm_test.go new file mode 100644 index 0000000..d11df02 --- /dev/null +++ b/internal/branding/firm_test.go @@ -0,0 +1,32 @@ +package branding + +import ( + "testing" +) + +func TestResolveName(t *testing.T) { + cases := []struct { + name string + env string + envSet bool + expected string + }{ + {"unset → default HLC", "", false, "HLC"}, + {"empty string → default HLC", "", true, "HLC"}, + {"whitespace only → default HLC", " ", true, "HLC"}, + {"override applied", "Acme Patents", true, "Acme Patents"}, + {"override trimmed", " Acme ", true, "Acme"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.envSet { + t.Setenv("FIRM_NAME", tc.env) + } else { + t.Setenv("FIRM_NAME", "") + } + if got := resolveName(); got != tc.expected { + t.Errorf("resolveName() = %q, want %q", got, tc.expected) + } + }) + } +} diff --git a/internal/handlers/admin_users.go b/internal/handlers/admin_users.go index 8ea88d7..a969033 100644 --- a/internal/handlers/admin_users.go +++ b/internal/handlers/admin_users.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" + "mgit.msbls.de/m/patholo/internal/branding" "mgit.msbls.de/m/patholo/internal/services" ) @@ -59,7 +60,7 @@ func handleAdminCreateUser(w http.ResponseWriter, r *http.Request) { } if !isAllowedEmailDomain(input.Email) { writeJSON(w, http.StatusForbidden, map[string]string{ - "error": "email domain not on the HLC allow-list", + "error": "email domain not on the " + branding.Name + " allow-list", }) return } diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index 9753cf0..981bea3 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -8,6 +8,7 @@ import ( "strings" "mgit.msbls.de/m/patholo/internal/auth" + "mgit.msbls.de/m/patholo/internal/branding" ) func handleLoginPage(w http.ResponseWriter, r *http.Request) { @@ -37,7 +38,7 @@ func handleAPILogin(w http.ResponseWriter, r *http.Request) { } if !isAllowedEmailDomain(req.Email) { - writeJSON(w, http.StatusForbidden, map[string]string{"error": "Zugang nur für autorisierte HLC-E-Mail-Adressen."}) + writeJSON(w, http.StatusForbidden, map[string]string{"error": "Zugang nur für autorisierte " + branding.Name + "-E-Mail-Adressen."}) return } @@ -78,7 +79,7 @@ func handleAPIRegister(w http.ResponseWriter, r *http.Request) { } if !isAllowedEmailDomain(req.Email) { - writeJSON(w, http.StatusForbidden, map[string]string{"error": "Registrierung nur für autorisierte HLC-E-Mail-Adressen."}) + writeJSON(w, http.StatusForbidden, map[string]string{"error": "Registrierung nur für autorisierte " + branding.Name + "-E-Mail-Adressen."}) return } @@ -110,10 +111,13 @@ func handleLogout(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/login", http.StatusFound) } -// isAllowedEmailDomain gates sign-in/register to the HLC email domains. +// isAllowedEmailDomain gates sign-in/register to the firm's email domains. // Whitelist is configurable via ALLOWED_EMAIL_DOMAINS (comma-separated), -// defaulting to hoganlovells.com,hlc.com,hlc.de so existing Hogan Lovells -// addresses keep working during the post-merger transition. +// defaulting to hoganlovells.com,hlc.com,hlc.de so legacy and post-merger +// addresses keep working until IT finishes the domain consolidation. +// Note: this whitelist intentionally references real DNS domains, not +// branding.Name — the firm's email domains and the firm's display name are +// separate concerns and rotate on different cadences. func isAllowedEmailDomain(email string) bool { parts := strings.SplitN(email, "@", 2) if len(parts) != 2 { diff --git a/internal/handlers/courts.go b/internal/handlers/courts.go index f79b678..f47e857 100644 --- a/internal/handlers/courts.go +++ b/internal/handlers/courts.go @@ -13,8 +13,9 @@ import ( "mgit.msbls.de/m/patholo/internal/auth" ) -// Court represents a court, division, or registry relevant to HL's patent practice. -// Fields left empty where details could not be reliably verified at build time. +// Court represents a court, division, or registry relevant to the firm's +// patent practice. Fields left empty where details could not be reliably +// verified at build time. type Court struct { ID string `json:"id"` NameDE string `json:"nameDE"` @@ -32,7 +33,7 @@ type Court struct { Filing string `json:"filing,omitempty"` // e-filing system / accepted formats NotesDE string `json:"notesDE,omitempty"` NotesEN string `json:"notesEN,omitempty"` - HLContact string `json:"hlContact,omitempty"` // placeholder, populated later + HLContact string `json:"hlContact,omitempty"` // firm-internal contact at this court; field name kept for API stability post-rebrand Source string `json:"source,omitempty"` // internal reference URL, not rendered } diff --git a/internal/handlers/files.go b/internal/handlers/files.go index 4bc83f8..5aaaf2f 100644 --- a/internal/handlers/files.go +++ b/internal/handlers/files.go @@ -9,6 +9,8 @@ import ( "net/url" "sync" "time" + + "mgit.msbls.de/m/patholo/internal/branding" ) const ( @@ -25,10 +27,19 @@ type fileEntry struct { FilePath string } +// fileRegistry maps the public download slug to the upstream Gitea object. +// +// RawURL / FilePath reference the actual file in mWorkRepo and must match the +// blob's name there exactly; renaming would 404 the proxy. DownloadName is +// what the browser saves the file as — that's a branding surface, so it +// renders branding.Name instead of the upstream filename. +// +// The URL slug ("hl-patents-style.dotm") is preserved as a stable public +// identifier so existing bookmarks keep working post-rebrand. var fileRegistry = map[string]fileEntry{ "hl-patents-style.dotm": { RawURL: "https://mgit.msbls.de/m/mWorkRepo/raw/branch/main/6%20-%20material/Templates/Word/HL%20Patents%20Style.dotm", - DownloadName: "HL Patents Style.dotm", + DownloadName: branding.Name + " Patents Style.dotm", ContentType: "application/vnd.ms-word.template.macroEnabled.12", RepoOwner: "m", RepoName: "mWorkRepo", diff --git a/internal/handlers/invite.go b/internal/handlers/invite.go index d7d1ab8..73af715 100644 --- a/internal/handlers/invite.go +++ b/internal/handlers/invite.go @@ -5,6 +5,7 @@ import ( "errors" "net/http" + "mgit.msbls.de/m/patholo/internal/branding" "mgit.msbls.de/m/patholo/internal/services" ) @@ -24,7 +25,7 @@ type inviteResponse struct { // POST /api/invite — send a branded invitation email to a colleague. // -// Auth: any onboarded user may invite. The HLC email-domain whitelist +// Auth: any onboarded user may invite. The firm email-domain whitelist // (ALLOWED_EMAIL_DOMAINS) is enforced in-service so callers can't bypass it // by bypassing the handler. // @@ -82,7 +83,7 @@ func handleInvite(w http.ResponseWriter, r *http.Request) { }) case errors.Is(err, services.ErrInviteDomainBlocked): writeJSON(w, http.StatusForbidden, map[string]string{ - "error": "recipient domain not on the HLC allow-list", + "error": "recipient domain not on the " + branding.Name + " allow-list", }) case errors.Is(err, services.ErrInviteRateLimited): w.Header().Set("Retry-After", "3600") diff --git a/internal/handlers/offices.go b/internal/handlers/offices.go index 3a85c21..5bb2dd8 100644 --- a/internal/handlers/offices.go +++ b/internal/handlers/offices.go @@ -6,7 +6,7 @@ import ( "mgit.msbls.de/m/patholo/internal/offices" ) -// GET /api/offices — returns the canonical HLC office list with DE + EN +// GET /api/offices — returns the canonical firm office list with DE + EN // labels. Backed by internal/offices (single source of truth, also used by // the Akte create / edit validation). func handleListOffices(w http.ResponseWriter, r *http.Request) { diff --git a/internal/models/models.go b/internal/models/models.go index 61f5c31..9215917 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -75,7 +75,7 @@ type Project struct { Country *string `db:"country" json:"country,omitempty"` BillingReference *string `db:"billing_reference" json:"billing_reference,omitempty"` - // ClientMatter numbers — external HLC billing/DMS identifiers. + // ClientMatter numbers — external billing/DMS identifiers used by the firm. // Child rows inherit client_number from the root by default (resolved at // read time by the service); a child with its own client_number overrides. // matter_number is assigned independently at any level. diff --git a/internal/offices/offices.go b/internal/offices/offices.go index 4801e2c..03e3e0e 100644 --- a/internal/offices/offices.go +++ b/internal/offices/offices.go @@ -1,10 +1,10 @@ -// Package offices is the single source of truth for the HLC office list. +// Package offices is the single source of truth for the firm's office list. // // The keys here must stay in sync with the CHECK constraint on // paliad.users.office and paliad.akten.owning_office (migration 001). package offices -// Office is a single HLC office with its i18n-ready labels. +// Office is a single firm office with its i18n-ready labels. type Office struct { Key string `json:"key"` LabelDE string `json:"label_de"` diff --git a/internal/services/mail_service.go b/internal/services/mail_service.go index c7c0537..9d04e8b 100644 --- a/internal/services/mail_service.go +++ b/internal/services/mail_service.go @@ -29,6 +29,7 @@ import ( "strings" "time" + "mgit.msbls.de/m/patholo/internal/branding" "mgit.msbls.de/m/patholo/internal/templates" ) @@ -184,9 +185,14 @@ func (s *MailService) RenderTemplate(in TemplateData) (string, error) { return "", fmt.Errorf("parse template %s: %w", contentFile, err) } + // Firm is injected from branding.Name so every email template can render + // the current firm name via {{.Firm}} without each caller threading it in. + // Caller-provided Data still wins (in.Data is copied last) — useful in + // tests that want to assert a specific firm string. payload := map[string]any{ "Lang": lang, "Subject": in.Subject, + "Firm": branding.Name, } maps.Copy(payload, in.Data) diff --git a/internal/services/mail_service_test.go b/internal/services/mail_service_test.go index 9819d5c..6c4d968 100644 --- a/internal/services/mail_service_test.go +++ b/internal/services/mail_service_test.go @@ -126,6 +126,10 @@ func TestRenderTemplateInvitation(t *testing.T) { for _, want := range []string{ "Anna Schmidt", "invites you", "Have a look at Paliad.", "https://paliad.de/login", "colleague@hlc.com", + // Branding placeholder: {{.Firm}} should resolve to the configured + // firm name (defaults to "HLC"). Catches accidental deletion of the + // template placeholder when nobody set FIRM_NAME in the test env. + "platform for HLC", } { if !strings.Contains(html, want) { t.Errorf("rendered html missing %q", want) diff --git a/internal/templates/email/invitation.html b/internal/templates/email/invitation.html index 144de4e..6a3273a 100644 --- a/internal/templates/email/invitation.html +++ b/internal/templates/email/invitation.html @@ -1,11 +1,11 @@ {{define "content"}} {{if eq .Lang "en"}}

{{.InviterName}} invites you to Paliad

-

Paliad is the patent practice platform for HLC — matter management, deadline calculations, knowledge tools, and more.

+

Paliad is the patent practice platform for {{.Firm}} — matter management, deadline calculations, knowledge tools, and more.

{{if .Message}}
{{.Message}}
{{end}} -

Sign up with your HLC email to get started:

+

Sign up with your {{.Firm}} email to get started:

Join Paliad @@ -14,11 +14,11 @@

Sent to {{.ToEmail}} by {{.InviterEmail}}. If you didn't expect this invitation, you can ignore it.

{{else}}

{{.InviterName}} lädt Sie zu Paliad ein

-

Paliad ist die Patent-Praxis-Plattform für HLC — Aktenverwaltung, Fristenberechnung, Wissenswerkzeuge und mehr.

+

Paliad ist die Patent-Praxis-Plattform für {{.Firm}} — Aktenverwaltung, Fristenberechnung, Wissenswerkzeuge und mehr.

{{if .Message}}
{{.Message}}
{{end}} -

Registrieren Sie sich mit Ihrer HLC-E-Mail-Adresse:

+

Registrieren Sie sich mit Ihrer {{.Firm}}-E-Mail-Adresse:

Zu Paliad anmelden