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.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
31
frontend/src/branding.ts
Normal file
31
frontend/src/branding.ts
Normal file
@@ -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";
|
||||
@@ -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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
// 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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
// 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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
// 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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
// 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.",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { h } from "../jsx";
|
||||
import { FIRM } from "../branding";
|
||||
|
||||
// Reusable Project form body. Renders the field grid only — the surrounding
|
||||
// <form>, submit/cancel buttons and the form-msg paragraph belong to the
|
||||
@@ -55,7 +56,7 @@ export function ProjectFormFields(): string {
|
||||
<input
|
||||
type="text"
|
||||
id="project-ref"
|
||||
placeholder="z.B. HL-2026-0042"
|
||||
placeholder={`z.B. ${FIRM}-2026-0042`}
|
||||
data-i18n-placeholder="projekte.field.reference.placeholder"
|
||||
/>
|
||||
</div>
|
||||
@@ -83,8 +84,8 @@ export function ProjectFormFields(): string {
|
||||
</div>
|
||||
</div>
|
||||
<p className="form-hint" data-i18n="projekte.field.clientmatter.hint">
|
||||
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).`}
|
||||
</p>
|
||||
|
||||
<div className="form-field">
|
||||
@@ -101,7 +102,7 @@ export function ProjectFormFields(): string {
|
||||
<input
|
||||
type="url"
|
||||
id="project-netdocs"
|
||||
placeholder="https://netdocs.hoganlovells.com/..."
|
||||
placeholder="https://netdocs.example.com/..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { h, Fragment } from "../jsx";
|
||||
import { FIRM } from "../branding";
|
||||
|
||||
const ICON_HOME = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>';
|
||||
const ICON_CALC = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="14" x2="8" y2="14.01"/><line x1="12" y1="14" x2="12" y2="14.01"/><line x1="16" y1="14" x2="16" y2="14.01"/><line x1="8" y1="18" x2="16" y2="18"/></svg>';
|
||||
@@ -196,7 +197,7 @@ export function Sidebar({ currentPath, authenticated = true }: SidebarProps): st
|
||||
<button className="modal-close" id="invite-modal-close" type="button" aria-label="Close">×</button>
|
||||
</div>
|
||||
<p data-i18n="invite.modal.body" className="invite-modal-body">
|
||||
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.`}
|
||||
</p>
|
||||
<form id="invite-form" className="akten-form" autocomplete="off">
|
||||
<div className="form-field">
|
||||
|
||||
@@ -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 = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><path d="M8 13l1.5 5 1.5-4 1.5 4 1.5-5"/></svg>';
|
||||
|
||||
@@ -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 {
|
||||
<div className="tool-header">
|
||||
<h1 data-i18n="downloads.heading">Downloads</h1>
|
||||
<p className="tool-subtitle" data-i18n="downloads.subtitle">
|
||||
Dateien und Vorlagen für das HL Patent-Team.
|
||||
{`Dateien und Vorlagen für das ${FIRM} Patent-Team.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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 = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/><path d="M8 7h6"/><path d="M8 11h4"/></svg>';
|
||||
const ICON_FILE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg>';
|
||||
@@ -24,7 +25,7 @@ export function renderIndex(): string {
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<PWAHead />
|
||||
<title data-i18n="index.title">Paliad — Patentwissen für Hogan Lovells</title>
|
||||
<title data-i18n="index.title">{`Paliad — Patentwissen für ${FIRM}`}</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
</head>
|
||||
<body className="has-sidebar">
|
||||
@@ -33,9 +34,9 @@ export function renderIndex(): string {
|
||||
<main>
|
||||
<section className="hero">
|
||||
<div className="container">
|
||||
<h1>Patent Knowledge<br /><span className="hero-accent" data-i18n="index.hero.accent">für Hogan Lovells</span></h1>
|
||||
<h1>Patent Knowledge<br /><span className="hero-accent" data-i18n="index.hero.accent">{`für ${FIRM}`}</span></h1>
|
||||
<p className="hero-sub" data-i18n="index.hero.sub">
|
||||
Leitfäden, Vorlagen und Dokumente für das HL Patent-Team.
|
||||
{`Leitfäden, Vorlagen und Dokumente für das ${FIRM} Patent-Team.`}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -52,7 +53,7 @@ export function renderIndex(): string {
|
||||
<div className="card">
|
||||
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_FILE }} />
|
||||
<h2 data-i18n="index.templates.title">Vorlagen</h2>
|
||||
<p data-i18n="index.templates.desc">Standardisierte Vorlagen für Schriftsätze, Korrespondenz und interne Dokumente. HL Patents Style Guide.</p>
|
||||
<p data-i18n="index.templates.desc">{`Standardisierte Vorlagen für Schriftsätze, Korrespondenz und interne Dokumente. ${FIRM} Patents Style Guide.`}</p>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
@@ -113,8 +114,8 @@ export function renderIndex(): string {
|
||||
<div className="grid grid-2">
|
||||
<a href="/files/hl-patents-style.dotm" className="card card-link">
|
||||
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_DOWNLOAD }} />
|
||||
<h2 data-i18n="index.style.title">HL Patents Style</h2>
|
||||
<p data-i18n="index.style.desc">Word-Vorlage im HL Patents Style. Formatierung, Schriftarten und Makros für standardisierte Schriftsätze.</p>
|
||||
<h2 data-i18n="index.style.title">{`${FIRM} Patents Style`}</h2>
|
||||
<p data-i18n="index.style.desc">{`Word-Vorlage im ${FIRM} Patents Style. Formatierung, Schriftarten und Makros für standardisierte Schriftsätze.`}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="10" x2="8" y2="10.01"/><line x1="12" y1="10" x2="12" y2="10.01"/><line x1="16" y1="10" x2="16" y2="10.01"/><line x1="8" y1="14" x2="8" y2="14.01"/><line x1="12" y1="14" x2="12" y2="14.01"/><line x1="16" y1="14" x2="16" y2="14.01"/><line x1="8" y1="18" x2="16" y2="18"/></svg>';
|
||||
|
||||
@@ -233,7 +234,7 @@ export function renderKostenrechner(): string {
|
||||
|
||||
<div className="print-footer" id="print-footer">
|
||||
<span data-i18n="kosten.print.disclaimer">Dieses Dokument dient ausschließlich der internen Verwendung und stellt keine Rechtsberatung dar. Alle Angaben ohne Gewähr.</span>
|
||||
<span>© 2026 Paliad — Hogan Lovells</span>
|
||||
<span>{`© 2026 Paliad — ${FIRM}`}</span>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
|
||||
@@ -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 "<!DOCTYPE html>" + (
|
||||
@@ -44,7 +45,7 @@ export function renderLogin(loginJs: string): string {
|
||||
<button type="submit" className="login-button" data-i18n="login.register.submit">Registrieren</button>
|
||||
</form>
|
||||
|
||||
<p className="login-hint" data-i18n="login.hint">{"Nur f\u00FCr autorisierte HLC-E-Mail-Adressen."}</p>
|
||||
<p className="login-hint" data-i18n="login.hint">{`Nur f\u00FCr autorisierte ${FIRM}-E-Mail-Adressen.`}</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
32
internal/branding/firm.go
Normal file
32
internal/branding/firm.go
Normal file
@@ -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"
|
||||
}
|
||||
32
internal/branding/firm_test.go
Normal file
32
internal/branding/firm_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{{define "content"}}
|
||||
{{if eq .Lang "en"}}
|
||||
<h1 style="margin:0 0 16px 0;font-size:20px;line-height:1.3;color:#1c1917;">{{.InviterName}} invites you to Paliad</h1>
|
||||
<p style="margin:0 0 12px 0;">Paliad is the patent practice platform for HLC — matter management, deadline calculations, knowledge tools, and more.</p>
|
||||
<p style="margin:0 0 12px 0;">Paliad is the patent practice platform for {{.Firm}} — matter management, deadline calculations, knowledge tools, and more.</p>
|
||||
{{if .Message}}
|
||||
<div style="background:#f5f5f4;border-left:3px solid #BFF355;padding:14px 16px;margin:20px 0;border-radius:0 6px 6px 0;font-size:14px;line-height:1.55;color:#44403c;white-space:pre-wrap;">{{.Message}}</div>
|
||||
{{end}}
|
||||
<p style="margin:20px 0 24px 0;">Sign up with your HLC email to get started:</p>
|
||||
<p style="margin:20px 0 24px 0;">Sign up with your {{.Firm}} email to get started:</p>
|
||||
<p style="margin:0;">
|
||||
<a href="{{.RegisterURL}}" style="display:inline-block;background:#1c1917;color:#ffffff;padding:12px 24px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px;">
|
||||
Join Paliad
|
||||
@@ -14,11 +14,11 @@
|
||||
<p style="margin:24px 0 0 0;font-size:12px;color:#78716c;">Sent to {{.ToEmail}} by {{.InviterEmail}}. If you didn't expect this invitation, you can ignore it.</p>
|
||||
{{else}}
|
||||
<h1 style="margin:0 0 16px 0;font-size:20px;line-height:1.3;color:#1c1917;">{{.InviterName}} lädt Sie zu Paliad ein</h1>
|
||||
<p style="margin:0 0 12px 0;">Paliad ist die Patent-Praxis-Plattform für HLC — Aktenverwaltung, Fristenberechnung, Wissenswerkzeuge und mehr.</p>
|
||||
<p style="margin:0 0 12px 0;">Paliad ist die Patent-Praxis-Plattform für {{.Firm}} — Aktenverwaltung, Fristenberechnung, Wissenswerkzeuge und mehr.</p>
|
||||
{{if .Message}}
|
||||
<div style="background:#f5f5f4;border-left:3px solid #BFF355;padding:14px 16px;margin:20px 0;border-radius:0 6px 6px 0;font-size:14px;line-height:1.55;color:#44403c;white-space:pre-wrap;">{{.Message}}</div>
|
||||
{{end}}
|
||||
<p style="margin:20px 0 24px 0;">Registrieren Sie sich mit Ihrer HLC-E-Mail-Adresse:</p>
|
||||
<p style="margin:20px 0 24px 0;">Registrieren Sie sich mit Ihrer {{.Firm}}-E-Mail-Adresse:</p>
|
||||
<p style="margin:0;">
|
||||
<a href="{{.RegisterURL}}" style="display:inline-block;background:#1c1917;color:#ffffff;padding:12px 24px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px;">
|
||||
Zu Paliad anmelden
|
||||
|
||||
Reference in New Issue
Block a user