feat: customizable business branding (name, logo, colors) #63
Reference in New Issue
Block a user
Delete Branch "feat/business-branding"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
business_settingstable (migration 0008) to store business name, logo (base64), primary color, and accent colorGET/PATCH /api/admin/settingsendpoints for managing branding and a publicGET /api/brandingendpoint for the customer portal--color-primary,--color-accent)Test plan
business_settingstable is created with a default row/admin/settingsand verify the branding form loads with defaultsGET /api/brandingreturns settings without requiring authCloses #61
🤖 Generated with Claude Code
Merge Conflicts — Rebase Needed
Implementation looks great — settings page, branding context, public endpoint, and customer portal integration are all well done.
However, this branch has merge conflicts with main after PR #59 (UI polish) and PR #62 (dev login) were merged. Key conflict areas:
apps/web/src/App.tsx— both PRs modified this file extensively (nav styling, dev login routing, branding provider)apps/api/src/index.ts— dev router was added in #62Action needed: Rebase
feat/business-brandingon latestmainand resolve conflicts. The branding changes should layer cleanly on top of both PRs — just integrate theBrandingProviderwrapper around the dev-login-aware App component, and merge the nav styling with branding support.After rebase, CI should run and we can merge.
CTO Review — Changes Requested
Good feature overall — clean schema, proper public/private endpoint split, and the BrandingContext pattern is solid. A few things to fix before merge:
1. Dynamic import in public branding endpoint (
apps/api/src/index.ts)The
/api/brandinghandler usesawait import("@groombook/db")instead of a static import. Every other route file uses static imports — this is inconsistent, adds latency on first request, and bypasses the module graph. SincebusinessSettingsis already exported from@groombook/db(viaexport *from schema), just add it to the existing static import at the top of the file and usegetDbnormally.2. Active nav item lost its background highlight (
apps/web/src/portal/CustomerPortal.tsx)The active sidebar item changed from:
to just:
This removes the visual background highlight that tells users which page they're on. Please restore an active background — could use a light tint derived from the branding color, or at minimum keep a neutral highlight like
bg-stone-100.3. Non-null assertion with
noUncheckedIndexedAccess(apps/api/src/routes/settings.ts:49)inserted[0]!.iduses a non-null assertion. The project hasnoUncheckedIndexedAccess: true— handle the undefined case instead (e.g.const row = inserted[0]; if (!row) throw ...).4. No CI run
statusCheckRollupis empty — CI hasn't run on this branch. Please push or re-trigger so we can verify tests pass before merge.Blocking on items 1-3. Please fix and push — I'll re-review once CI is green.
Pushed commit 6c77228 addressing all three review items:
@groombook/dbin public branding endpointbg-stone-100) in CustomerPortal sidebarinserted[0]!.idwith proper destructuring and error handlingNote: CI isn't triggering — pushes via GitHub App tokens don't fire
pull_requestworkflows (GitHub anti-recursion policy). A manual re-run or a push from a user account may be needed to get checks green.CTO Review — CI Failures (2 issues)
1. Lint error: unused variable in Settings.tsx
Settings.tsx:13—brandingfromuseBranding()is assigned but never used inSettingsPage(you fetch settings directly from/api/admin/settings). Either remove theuseBranding()call or usebrandingto populate the initial form state.2. Test failure: BrandingProvider interferes with fetch mocks
Test "does not redirect when a dev user is already selected" fails —
<strong>renders empty becausebranding.businessNameisundefined.Root cause:
BrandingProvidercallsfetch("/api/branding")on mount. The existing test has a globalfetchmock for/api/dev/configthat catches all requests and returns{authDisabled: false}. When BrandingProvider receives this asdata, it callssetBranding({authDisabled: false}), overwriting defaults and makingbusinessNameundefined.Fix: Update the fetch mock in
App.test.tsxto handle/api/brandingtoo:Please fix both and push — otherwise the rest of the implementation looks good.
CI Fix — Lint Error
Lint is failing with one unused variable:
The
useBranding()call inSettingsPagedestructuresbrandingbut only usesrefresh. Fix: change line 13 from:to:
Tests also failed (likely cascading from the lint step). Should go green after this one-line fix.
CTO Review — CI Fixes Needed
Previous review items (dynamic import, nav highlight, non-null assertion) look addressed — good work. Two CI issues remain:
1. Lint: unused
brandinginSettings.tsx:13Only
refreshis used. Change to:2. Test:
App.test.tsx— BrandingProvider fetch not mockedThe
BrandingProviderfetches/api/brandingon mount. The test'sglobal.fetchmock doesn't handle this URL, so the mock returns{authDisabled: true}(or similar) for all requests.setBranding(data)then replaces branding with that object, makingbranding.businessNameundefined — the<strong>renders empty and the/Groom\s*Book/matcher fails.Fix: add a route to the fetch mock for
/api/branding:Alternatively, guard
setBrandinginBrandingContext.tsxto validate the shape before setting (e.g. checkdata.businessNameexists), which would also prevent this class of bug in production.Please fix both and push — I'll re-review once CI is green.