Skip to main content

Unified Branding Service (Whitelabel) — legacy hub FE Task Breakdown

RFC: rfc-unified-branding-hub-fe.md Source: rfc-unified-branding-hub-fe.md (§4.C Agent Execution Plan, chunks 1–10) Repo: hub (Nuxt 2 / Vue 2.7 Options API, Vuex 3, @mekari/pixel v1.1.1, Jest) — cross-repo from this docs repo (local: ../fe/hub) Producer contract: GET /branding is built by rfc-unified-branding-service-be.md (out of this repo) Slicing: vertical — one task per execution chunk. Chunk 5 (pixel extendTheme override) is folded into the boot task (T4) since both touch plugins/pixel.js; chunk 10 (flag + fallback) is pulled forward into T1/T3 for a dark-launch-first order. FE-only; the endpoint and other consumers (hub-chat, crm-fe-v3) are out of scope (see Skipped stories). Execute in order — each task's acceptance criteria must pass before the next. All tasks build against a local mock of GET /branding until the BE endpoint ships.

⚠️ This is the hard consumer. Unlike hub-chat (Pixel3 + --mp-colors-*), hub runs pixel v1 with no CSS-variable theming. Brand color = $q-primary: #0274f5 (_variables.scss:6, 124 usages) + 93 literal #0274f5 (53 .vue / 10 .scss / 9 .svg / rest JS) + 4 brand.mekari pixel tokens. Color whitelabel is therefore a CSS-var refactor + pixel extendTheme override + literal sweep, not a token flip (RFC §1, Decision 1).


Effort Summary

TaskFE daysBE daysQA daysTotal
T1 — whitelabel_branding flag + BRANDING_URL config (chunk 10-flag, 2-const)0.50.5
T2 — store/branding.js + requests/branding.js + localStorage cache (chunk 2)22
T3 — applyBranding util + color/URL validation + fallbacks (chunks 1, 10-fallback)1.51.5
T4 — Boot plugin + cached-first paint + pixel extendTheme override (chunks 3, 5)1.50.52
T5 — App brand color: $q-primaryvar(--brand-primary) (chunk 4)0.50.5
T6 — Literal #0274f5 sweep → var(--brand-primary) (chunk 6)2.50.53
T7 — <BrandLogo/> + Header/SideNav logo & product name (chunks 8, 9-subset)1.50.52
T8 — Runtime head() + nuxt.config defaults + default-tenant parity proof (chunks 7, 9-subset)1.50.52
Grand total11.52.514

Confidence: medium. Every layer copies a verified in-repo pattern (store/preferences.js, requests/index.js, plugins/axios.js, plugins/pixel.js:201, store/__test__/usman.spec.js). Medium — not high — because three RFC §5 items can move scope: the GET /branding contract emits Pixel3-named --mp-colors-* colors this repo can't use and needs a plain brand hex (OQ Q1, hard blocker for real integration — not for the mock build), the literal-#0274f5 sweep touches ~63 largely untested .vue/.scss files (T6 regression risk), and the pixel-v1 timing caveat means a first-ever visit paints default brand for the 4 brand.mekari spots until the next load (Decision 2). None blocks building against a mock; each has a sensible default in RFC §5.


Task 1: [FE] whitelabel_branding flag + BRANDING_URL config (chunk 10-flag, chunk 2-const)

A default-off switch and a resolvable branding endpoint exist, so all later work merges safely dark with zero production impact.

Status: ✅ Actionable (flag provisioner — cookie/query toggle vs backend feature_flag_state — is RFC §5 Q4; defaults to the utils/toggle.js cookie/query pattern for stage 1)

Design reference: n/a — design pending (RFC §5 Q3 — no Figma; visual bar is parity with today's layout)

What to build

A whitelabel_branding flag (default off) read through the existing utils/toggle.js cookie+query pattern, plus a BRANDING_URL endpoint constant sourced from process.env.HUB_SERVICE_URL (confirm base for the public/pre-login endpoint — RFC §5 Q2).

Implementation Plan

ActionFileWhat changes
extendutils/toggle.jsadd isEnableWhitelabelBranding() mirroring isEnableQontakOneSidebar() (utils/toggle.js:11-22): reads ?dev-whitelabel=1 + persists cookie _whitelabel-branding; default false
extendassets/variables/endpoints.jsadd export const BRANDING_URL = process.env.HUB_SERVICE_URL + '/api/core/v1/branding' (mirrors CALLS_URL at L170) — confirm base per RFC §5 Q2
createutils/__test__/toggle.branding.spec.jsflag absent → false; ?dev-whitelabel=1true; cookie persists

Implementation steps

  1. Explore the relevant codebase area — open utils/toggle.js:11-22 (isEnableQontakOneSidebar cookie/query pattern) and assets/variables/endpoints.js:170 (export const X_URL = process.env.HUB_SERVICE_URL + '/api/...').
  2. Write failing tests (red)npx jest utils/__test__/toggle.branding.spec.js: default false, query enables, cookie persists.
  3. Implement — add isEnableWhitelabelBranding() + BRANDING_URL; keep default false.
  4. Go greennpx jest utils/__test__/toggle.branding.spec.js.
  5. Quality gatenpm run lint:js.

Acceptance criteria

  • isEnableWhitelabelBranding() returns false by default (no regression when unset).
  • ?dev-whitelabel=1 enables it and persists via cookie _whitelabel-branding.
  • BRANDING_URL resolves from process.env.HUB_SERVICE_URL (base confirmed in §5 Q2).

Test strategy

Unit test the toggle: mock document.cookie + query string; assert the three states. Key assertion: default is false so flag-off = today's behavior.

Effort estimate

DisciplineDays
Frontend0.5
Backend
QA
Total0.5

Assumptions: reuses utils/toggle.js cookie/query pattern (RFC Detail 4.A); no new dependency; endpoint base is a one-line change if §5 Q2 resolves to a different host.

Run to verify

npx jest utils/__test__/toggle.branding.spec.js && npm run lint:js

Depends on

  • None (first task).

Task 2: [FE] store/branding.js + requests/branding.js + localStorage cache (chunk 2)

The tenant's branding has a place to live in the app, fetched once via a state flag and cached for instant paint on the next boot.

Status: ✅ Actionable (base URL + whether the public endpoint should receive an auth header — RFC §5 Q2)

Design reference: n/a — design pending (RFC §5 Q3)

What to build

store/branding.js — a Vuex 3 module (modelled on store/preferences.js) exposing state (payload + status), getters (productName, brandName, assets, links, colors with Qontak defaults), a fetch action via this.$axios, and localStorage cache read/write (branding), plus requests/branding.js registered in requests/index.js.

Implementation Plan

ActionFileWhat changes
createrequests/branding.jsget()$axios.$get(BRANDING_URL) (no retry); mirrors the requests/* module shape
modifyrequests/index.jsimport + register branding in the default export (beside webchat, L11-21)
createstore/branding.jsexport const state = () => ({ payload: null, status: 'idle' }); getters with Qontak fallbacks (productName → 'Qontak Chat'); actions.fetch (commit on success, keep defaults on error — never throw); readCache()/writeCache() guarding JSON.parse; schemaVersion guard discards incompatible blobs (RFC §2.3, §5 Q on schema)
createstore/__test__/branding.spec.jsfetch commits payload; getters return values then Qontak defaults when empty; malformed cache = miss (no throw); error → status rejected, no throw

Implementation steps

  1. Explore the relevant codebase area — read store/preferences.js:1-40 (module shape, import requests from '../requests', state/getters/mutations/actions) and store/__test__/usman.spec.js:1 (import { state, getters, mutations, actions } from '~/store/...' harness).
  2. Write failing specs (red)npx jest store/__test__/branding.spec.js: mock requests.branding.get; assert commit on 200, defaults when empty, no-throw on error, cache round-trip via a localStorage stub.
  3. Scaffold — create requests/branding.js, register in requests/index.js, create store/branding.js state/getters/mutations.
  4. Wire state — implement fetch action + cache helpers; getters expose non-PII branding with Qontak defaults.
  5. Go greennpx jest store/__test__/branding.spec.js.
  6. Quality gatenpm run lint:js.

Acceptance criteria

  • store/branding.js exposes fetch action + getters productName/brandName/assets/links/colors.
  • On mock 200, state commits payload; on error, status = rejected and it does not throw to the caller.
  • readCache() returns the last written payload; a malformed localStorage['branding'] (or wrong schemaVersion) is treated as a miss (no throw).
  • Getters return today's Qontak defaults when payload is null (no-regression path).

Test strategy

Store unit test following the usman.spec.js pattern: import state/getters/actions, mock requests.branding.get and a localStorage stub. Key assertion: getters fall back to 'Qontak Chat' / bundled defaults when payload is null.

Effort estimate

DisciplineDays
Frontend2
Backend
QA
Total2

Assumptions: reuses this.$axios (plugins/axios.js) + the requests/* aggregator; payload is non-PII so cache needs no logout-clear (RFC §2.3). Coverage caveat: jest.config.js:24-31 collectCoverageFrom excludes store/branding.js — add the glob or accept uncounted coverage (RFC §5 Q5).

Run to verify

npx jest store/__test__/branding.spec.js && npm run lint:js

Depends on

  • Task 1 (BRANDING_URL constant).

Task 3: [FE] applyBranding util + color/URL validation + fallbacks (chunks 1, 10-fallback)

The branding payload safely becomes live DOM — --brand-* on :root, a swapped favicon, an optional tenant font — with hostile values rejected before they touch the DOM, and a null payload degrading to today's Qontak brand.

Status: ✅ Actionable

Design reference: n/a — design pending (RFC §5 Q3)

What to build

utils/applyBranding.js (design-doc §5) — writes validated --brand-primary / --brand-primary-hover / --brand-font-family to document.documentElement.style, swaps the favicon <link>, injects the font <link>, and commits to store/branding.js. A hex/rgb() regex + https-host validator gates every value (RFC §3 Security); a null/failed payload leaves today's Qontak defaults untouched.

Implementation Plan

ActionFileWhat changes
createutils/applyBranding.jsapplyBranding(b): map payload colors--brand-primary/--brand-primary-hover (derive hover if BE sends only primary — RFC §2.4 note); validate each against a hex/rgb() regex; validate assets.*/font.cssUrl are https on an allow-listed host; skip missing font/appleTouchIcon; commit to store; emit branding.applied (mixpanel). Null payload → no-op (keep defaults)
createutils/__test__/applyBranding.spec.js:root receives --brand-primary; invalid color rejected; favicon href updated; font link + --brand-font-family set; null/missing fields → no throw, defaults intact

Implementation steps

  1. Explore the relevant codebase area — confirm there is no existing :root/setProperty writer in the repo (greenfield — RFC §2.0); read store/branding.js (Task 2) for the commit target; read store/__test__/usman.spec.js for the Jest/jsdom harness.
  2. Write failing tests (red)npx jest utils/__test__/applyBranding.spec.js: after applyBranding(fixture), document.documentElement.style.getPropertyValue('--brand-primary') equals the fixture; an invalid value (e.g. url(evil)) is not written; favicon href swapped; font absent → no font link; applyBranding(null) throws nothing and writes nothing.
  3. Scaffold — create utils/applyBranding.js with the validator helpers + typed shape (JSDoc @typedef Branding).
  4. Implement behavior — write only individual property values + a stylesheet <link href> (never raw CSS text / innerHTML — RFC §3); commit to the store; fire analytics once.
  5. Go greennpx jest utils/__test__/applyBranding.spec.js.
  6. Quality gatenpm run lint:js.

Acceptance criteria

  • Each valid color maps to --brand-primary / --brand-primary-hover on :root; the util derives hover if only primary is supplied.
  • A non-color value is rejected and never reaches setProperty.
  • assets.* / font.cssUrl that are not https on an allowed host are rejected.
  • applyBranding(null) (fetch failed, no cache) is a safe no-op — Qontak defaults remain; analytics branding.applied fires only on a real apply.

Test strategy

Jest + jsdom asserting DOM side-effects on document.documentElement.style and document.head. Key mock: a color fixture (#7A2FF2) + a hostile value (url(evil)); key assertion: hostile value absent from :root.

Effort estimate

DisciplineDays
Frontend1.5
Backend
QA
Total1.5

Assumptions: single branding-CDN origin allow-list (RFC §5 Q7 / CSP); no v-html introduced (RFC §3); --brand-primary-hover derived client-side if BE omits it (RFC §2.4 note).

Run to verify

npx jest utils/__test__/applyBranding.spec.js && npm run lint:js

Depends on

  • Task 2 (Branding shape + store setter).

Task 4: [FE] Boot plugin + cached-first paint + pixel extendTheme override (chunks 3, 5)

On boot a returning branded tenant paints instantly from cache — including the 4 pixel brand.mekari spots — then revalidates; with the flag off, nothing changes vs today.

Status: ✅ Actionable (pre-login reachability / edge allow-list of /branding — RFC §5 Q2; if deferred, pre-login shows default brand until login)

Design reference: n/a — design pending (RFC §5 Q3)

What to build

plugins/branding.js (flag-gated, runs at boot regardless of auth — RFC Decision 3): read localStorage['branding']applyBrandingfetch → re-apply + re-cache; on error keep current brand, never block boot. Plus a synchronous localStorage['branding'] read in plugins/pixel.js before Vue.use(Pixel, …) so extendTheme.colors.brand.mekari seeds the pixel theme in tenant color at first paint (RFC Decision 2 — the theme is fixed at Vue.use time).

Implementation Plan

ActionFileWhat changes
createplugins/branding.jsif (isEnableWhitelabelBranding()) { const c = readCache(); if (c) applyBranding(c); store.dispatch('branding/fetch') } — non-blocking; try/catch → warn via datadog, continue
modifyplugins/pixel.jsbefore L201 Vue.use, synchronously read localStorage['branding']; set extendTheme.colors = { brand: { mekari: cached?.colors?.['--brand-primary'] ?? '#651FFF' } } alongside the existing breakpoints
modifynuxt.config.jsregister '@/plugins/branding' in the plugins array (L84-98), after @/plugins/pixel and @/plugins/axios
createplugins/__test__/branding.spec.jsflag off → no fetch, :root unmutated; flag on + cache → applies before fetch; fetch reject → no throw, brand unchanged

Implementation steps

  1. Explore the relevant codebase area — read plugins/pixel.js:192-205 (customBreakpoints + Vue.use(Pixel, { extendTheme })) and nuxt.config.js:84-98 (plugin array, SPA mode so all client-side) and plugins/axios.js (context/$axios plugin shape).
  2. Write failing test (red)npx jest plugins/__test__/branding.spec.js: flag off → store.dispatch not called and :root snapshot == baseline; flag on + seeded cache → applyBranding runs before the fetch resolves; a rejected fetch produces no unhandled error and leaves the brand unchanged.
  3. Scaffold — create plugins/branding.js; add the synchronous cache read to plugins/pixel.js.
  4. Wire — register the plugin in nuxt.config.js; ensure order (pixel + axios before branding).
  5. Go green + buildnpx jest plugins/__test__/branding.spec.js then npm run build.
  6. Quality gatenpm run lint:js.

Acceptance criteria

  • Flag off → boot does not fetch /branding; :root is unmodified (snapshot identical to today).
  • Flag on → cached brand applied synchronously (incl. pixel extendTheme.colors.brand.mekari), then network payload re-applied + re-cached.
  • A /branding failure never blocks boot and shows no user-facing error (warn to datadog only).
  • plugins/pixel.js falls back to #651FFF for brand.mekari when no cache is present.

Test strategy

Jest testing the plugin factory with a mocked store/$axios and a localStorage stub. Key assertion: flag-off leaves document.documentElement.style byte-identical to baseline; flag-on applies cache before the awaited fetch resolves.

Effort estimate

DisciplineDays
Frontend1.5
Backend
QA0.5
Total2

Assumptions: mirrors existing boot-plugin placement; pixel theme is fixed at Vue.use so the sync cache read is the only way to theme the 4 brand.mekari spots at first paint (RFC Decision 2); QA validates flag-off no-regression on a brand-heavy screen (inbox/header).

Run to verify

npx jest plugins/__test__/branding.spec.js && npm run build && npm run lint:js

Depends on

  • Tasks 1, 2, 3 (flag, store+cache, applyBranding).

Task 5: [FE] App brand color: $q-primaryvar(--brand-primary) (chunk 4)

The app's own brand color flows from one CSS variable, so a tenant color change recolors ~124 usages at runtime with the SCSS default unchanged.

Status: ✅ Actionable

Design reference: n/a — design pending (RFC §5 Q3)

What to build

Refactor the two brand SCSS variables in _variables.scss to read from CSS custom properties with today's hex as the fallback default — a transparent change (same default #0274f5) that makes all 124 $q-primary usages runtime-themeable.

Implementation Plan

ActionFileWhat changes
modifyassets/stylesheets/abstracts/_variables.scss$q-primary: var(--brand-primary, #0274f5); (L6) and $q-primary-hover: var(--brand-primary-hover, #0364d1); (L8)

Implementation steps

  1. Explore the relevant codebase area — read assets/stylesheets/abstracts/_variables.scss:6,8 and confirm $q-primary is consumed via SCSS (124 usages) — the refactor keeps the variable name, only its value source changes.
  2. Change — swap the two literal hex definitions for var(--brand-*, <hex>).
  3. Verify build + defaultnpm run lint:style and npm run build; with no --brand-primary set, computed color must still be #0274f5 (fallback).
  4. Quality gatenpm run lint:style.

Acceptance criteria

  • $q-primary / $q-primary-hover resolve via var(--brand-primary[-hover], <today's hex>).
  • Default tenant (no --brand-primary set) renders exactly #0274f5 / #0364d1 (no visual regression).
  • npm run lint:style and npm run build pass.

Test strategy

No unit test (SCSS-only). Verified by build + a manual/DOM check that getComputedStyle on a $q-primary-styled element equals #0274f5 with no var set, and the tenant color when --brand-primary is set (covered end-to-end in T8).

Effort estimate

DisciplineDays
Frontend0.5
Backend
QA
Total0.5

Assumptions: pure mechanical refactor; the SCSS default preserves today's brand so this is safe to merge before any runtime var is written; parity validated in T8.

Run to verify

npm run lint:style && npm run build

Depends on

  • None strictly (safe standalone), but pairs with T3 (which writes --brand-primary). Recommended after T3/T4 so the var actually gets set at runtime.

Task 6: [FE] Literal #0274f5 sweep → var(--brand-primary) (chunk 6)

Brand-blue values that bypass the SCSS variable are routed through --brand-primary, so a tenant color reaches the ~63 spots that hard-coded Qontak blue.

Status: ✅ Actionable

Design reference: n/a — design pending (RFC §5 Q3)

What to build

Replace the 93 literal #0274f5 occurrences that bypass $q-primary — 53 in .vue, 10 in .scss, and the JS return in assets/mixins/messaging/conversation.js:324 — with var(--brand-primary) (CSS contexts) or a store getter (JS context). The 9 .svg assets are out of scope (baked fill; recolored only via CDN logo swap — see Skipped stories).

Implementation Plan

ActionFileWhat changes
modify53 .vue files (inline style/:style/fill #0274f5)var(--brand-primary)
modify10 .scss files (literal #0274f5)var(--brand-primary) (or $q-primary)
modifyassets/mixins/messaging/conversation.js:324return '#0274f5' → read brand color from the store getter (branding/colors or a --brand-primary read), fallback #0274f5
verifyexisting assets/mixins/messaging/__test__/conversation.spec.jskeep green — the default fallback must still return #0274f5 when no branding set (specs at L1009-1339 assert #0274f5)

Implementation steps

  1. Explore the relevant codebase areagrep -rniI '#0274f5' components pages layouts assets (excluding assets/images) to enumerate the 63 CSS/JS spots; separate CSS contexts (→ var) from the one JS return (→ getter with hex fallback).
  2. Sweep CSS — replace .vue/.scss literals with var(--brand-primary); keep each element's fallback behavior identical when no tenant var is set.
  3. Sweep JS — change conversation.js:324 to read the branding color with a #0274f5 default so conversation.spec.js stays green (or update the spec to assert the fallback).
  4. Verify no regressiongrep -rniI '#0274f5' components pages layouts assets --include='*.vue' --include='*.scss' (excl. images) → 0; npm test (incl. conversation.spec.js); npm run build.
  5. Quality gatenpm run lint.

Acceptance criteria

  • #0274f5 count in .vue/.scss (excluding assets/images SVGs) is 0 after the sweep.
  • conversation.js returns the tenant brand color when set, #0274f5 otherwise; conversation.spec.js passes (default path unchanged).
  • Flag off / no tenant var → every swept element still renders #0274f5 (no regression).
  • npm test and npm run build pass.

Test strategy

Rely on the existing spec suite for no-regression (esp. conversation.spec.js), plus a grep gate proving zero remaining CSS/JS literals. Key assertion: default fallback still yields #0274f5.

Effort estimate

DisciplineDays
Frontend2.5
Backend
QA0.5
Total3

Assumptions: mechanical but high blast radius across ~63 largely untested .vue files (RFC §2.D) — gated behind the flag and default-fallback; 9 baked-#0274f5 SVGs are intentionally excluded (RFC §5 limitation). QA spot-checks brand-heavy screens for stray blue.

Run to verify

grep -rniI '#0274f5' components pages layouts assets --include='*.vue' --include='*.scss' | grep -v assets/images; npm test && npm run lint && npm run build

Depends on

  • Task 5 (--brand-primary must exist as the sweep target).

Task 7: [FE] <BrandLogo/> + Header/SideNav logo & product name (chunks 8, 9-subset)

The header and side nav show the tenant's logo and product name from the store, with a safe fallback to today's Qontak logo pre-fetch.

Status: ✅ Actionable

Design reference: n/a — design pending (RFC §5 Q3 — parity with today's inline )

What to build

components/branding/BrandLogo.vue reading store/branding.js (assets.logo, productName) with an <img @error> fallback to the bundled default, then swap the inline logo <img> and the Qontak product-name literals in Header.vue and SideNav/index.vue to the component + brandName getter.

Implementation Plan

ActionFileWhat changes
createcomponents/branding/BrandLogo.vueprops variant?: 'default'|'white'|'mono-white', height?, alt?; renders brandingStore.assets.logo; <img @error> → bundled assets/images/brands/new/logo-qontak-default.svg; fixed height (avoid CLS)
createcomponents/branding/__test__/BrandLogo.spec.jsrenders store logo; falls back to bundled default when store empty
modifycomponents/layouts/Header.vuereplace inline logo <img> at L41, L59 with <BrandLogo/>; route the 10 Qontak literals through the brandName getter
modifycomponents/layouts/SideNav/index.vuereplace logo <img> at L20 with <BrandLogo/>; product name → brandName getter

Implementation steps

  1. Explore the relevant codebase area — read components/layouts/Header.vue:41,59 (inline logo <img> + brand literals) and components/layouts/SideNav/index.vue:20; read store/__test__/usman.spec.js for the component/store test harness and mapGetters usage.
  2. Write failing spec (red)npx jest components/branding/__test__/BrandLogo.spec.js: renders tenant logo src + product name when store set; store-empty → bundled default logo + "Qontak Chat".
  3. Scaffold — create BrandLogo.vue (template + props + @error fallback).
  4. Wire statemapGetters('branding', ['assets','brandName']); keep logo dimensions fixed.
  5. Integrate — swap Header.vue/SideNav/index.vue logo <img> + name literals to the component + getter.
  6. Go greennpx jest components/branding and re-run any Header/SideNav specs.
  7. Quality gatenpm run lint.

Acceptance criteria

  • <BrandLogo/> renders the tenant logo + product name when the store is set.
  • With an empty store / flag off, it renders the bundled Qontak default logo and "Qontak Chat" (no broken image, no layout shift).
  • Header.vue and SideNav/index.vue no longer contain hardcoded logo <img> paths or Qontak product-name literals (they read the getter).

Test strategy

Jest component spec (vue-jest) asserting rendered src/text against a mocked branding store, plus the @error fallback path. Key assertion: empty store → bundled default asset + "Qontak Chat".

Effort estimate

DisciplineDays
Frontend1.5
Backend
QA0.5
Total2

Assumptions: bundled default logo-qontak-default.svg already exists (verified — used at Header.vue:41,59, SideNav/index.vue:20); Header/SideNav are the Phase-1 product-name surfaces (the ~390-literal tail is deferred — RFC §5).

Run to verify

npx jest components/branding && npm run lint

Depends on

  • Task 2 (store getters supply logo + brandName).

Task 8: [FE] Runtime head() + nuxt.config defaults + default-tenant parity proof (chunks 7, 9-subset)

The browser tab title, favicon, and font reflect the tenant at runtime — and a proof test shows the same code path renders today's Qontak brand (flag off) vs a tenant brand (flag on), unchanged layout.

Status: ✅ Actionable

Design reference: n/a — design pending (RFC §5 Q3)

What to build

A dynamic head() option in layouts/hub.vue (Nuxt 2 Options API — no useHead) binding document.title/favicon to store/branding.js; keep nuxt.config.js head literals as default-only; and a two-tenant proof + default-parity test (design-doc §6, RFC §4.D).

Implementation Plan

ActionFileWhat changes
modifylayouts/hub.vueadd a head() component option returning title: brandName, link: [{ rel:'icon', href: favicon }] from mapGetters('branding', …); font <link> handled by applyBranding (T3)
modifynuxt.config.jsleave head.title/favicon (L25, L63) as defaults (still "Qontak Chat" when flag off); no runtime literals removed that would break default
createstore/__test__/branding.parity.spec.jsapply two fixtures (purple #7A2FF2 / green #0E9F6E) via applyBranding → two distinct :root --brand-primary values via one code path; applyBranding(null) → Qontak defaults intact

Implementation steps

  1. Explore the relevant codebase area — read nuxt.config.js:24-64 (static head) and layouts/hub.vue (layout mounting InitComponent); confirm Nuxt 2 uses the head() component option (no useHead in repo).
  2. Write failing test (red)npx jest store/__test__/branding.parity.spec.js: two fixtures → distinct --brand-primary on :root; applyBranding(null) leaves defaults.
  3. Implement head() — bind title/favicon in layouts/hub.vue to the branding getters; keep nuxt.config.js head as fallback.
  4. Go green + buildnpx jest store/__test__/branding.parity.spec.js then npm run build.
  5. Quality gatenpm run lint.

Acceptance criteria

  • document.title and favicon reflect the store's productName/favicon when the flag is on; default to "Qontak Chat" / qontak-favicon.ico when off.
  • Same applyBranding code path + two fixtures → two distinct :root --brand-primary sets, each equal to its fixture.
  • applyBranding(null) (flag off / no payload) → today's Qontak brand exactly (parity proof).
  • npm run build passes.

Test strategy

Jest parity spec reusing the T3 harness: two fixtures through one code path assert distinct :root values; the null case asserts default parity. Key assertion: flag-off output is byte-identical to today's Qontak brand.

Effort estimate

DisciplineDays
Frontend1.5
Backend
QA0.5
Total2

Assumptions: Nuxt 2 head() component option is the head API (no useHead/composables in repo — verified); QA runs the RFC §4.D pre-merge sequence and confirms default-tenant pixel parity.

Run to verify

npx jest store/__test__/branding.parity.spec.js && npm run build && npm run lint

Depends on

  • Tasks 2, 3, 7 (store, applyBranding, <BrandLogo/> supply the runtime brand).

Final gate (RFC §4.D), after Tasks 1–8: run the pre-merge sequence in order — npm run lintnpm testnpm run buildnpm run analyze (bundle delta) — and confirm green. Post-deploy: watch Datadog RUM branding_fetch_failed < 5% (15-min window) and branding.applied events (source: cache|network|default) on the pilot tenant.


Ordering rationale

  • Dark-launch first: T1 (flag + BRANDING_URL) lands before anything user-visible, so every later task merges behind a default-off flag with zero production impact.
  • Data-up dependency chain: config (T1) → store+cache (T2) → apply util (T3) → boot wiring + pixel override (T4). T4 orchestrates cache-first paint + revalidate and the pixel extendTheme seed, so it needs T2–T3 complete.
  • Color track runs after the plumbing: T5 ($q-primary→var) must precede T6 (literal sweep) because the sweep targets --brand-primary. T6 is the riskiest task — ~63 largely untested .vue/.scss files — so keep it behind the flag with default-fallback and lean on the existing spec suite + grep gate.
  • Identity surfaces last: T7 (BrandLogo + Header/SideNav) and T8 (runtime head + parity proof) consume the store and close the loop; T8's parity test is the design-doc §6 "same code, two brands" proof and the flag-off no-regression guarantee.
  • T2 is the highest-value task — it owns the fetch, status, getters, and cache everything else consumes; front-load review here.
  • Push externally in parallel: freeze the GET /branding contract with the BE RFC team so it emits a plain brand hex (--brand-primary), not Pixel3-named --mp-colors-* (RFC §5 Q1 — hard blocker for real integration, not for the mock build); confirm the endpoint base + edge allow-list for pre-login (Q2); settle the CSP allow-list (Q7); and name the flag provisioner (Q4). None blocks building against a mock; all gate the pilot-tenant stage.

Skipped stories

StoryReason
GET /branding endpoint + resolver + Redis + edge allow-listOut of repo — built by rfc-unified-branding-service-be.md (qontak.com). This FE work consumes it and builds against a mock until it ships; contract must emit a plain brand hex (RFC §5 Q1).
hub-chat consumptionOut of scope — separate repo (../fe/hub-chat) / separate RFC (rfc-unified-branding-hub-chat-fe.md); keep the payload contract aligned.
crm-fe-v3 consumptionOut of scope — separate repo / separate RFC; keep the payload contract aligned.
Per-tenant color of the 9 baked-#0274f5 SVG assetsOut of scope — baked fill can't be CSS-recolored; only logo/icon assets swapped via CDN URL are tenant-colored (RFC §5 limitation).
The ~390 non-Phase-1 Qontak string literalsDeferred — Phase 1 covers high-visibility surfaces (Header/SideNav/tab/nuxt.config); the tail (405 total; TicketingCrm.vue 31, layouts/hub.vue 24, InitComponent.vue 19, …) is phased in follow-ups (RFC §5).
First-visit pixel brand.mekari themingKnown limitation — pixel theme is fixed at Vue.use, so a first-ever visit (empty cache) shows default for the 4 brand.mekari spots until next load (RFC Decision 2). Not a task; a documented consequence.
Automatic contrast/legibility enforcementDeferred — v1 relies on admin/BE-side validation; computed-contrast guard is a future enhancement (RFC §5 Q6).
Tenant-admin branding editor UIOut of scope (RFC §1) — this is a consumer, not an authoring surface.