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-*),hubruns 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) + 4brand.mekaripixel tokens. Color whitelabel is therefore a CSS-var refactor + pixelextendThemeoverride + literal sweep, not a token flip (RFC §1, Decision 1).
Effort Summary
| Task | FE days | BE days | QA days | Total |
|---|---|---|---|---|
T1 — whitelabel_branding flag + BRANDING_URL config (chunk 10-flag, 2-const) | 0.5 | — | — | 0.5 |
T2 — store/branding.js + requests/branding.js + localStorage cache (chunk 2) | 2 | — | — | 2 |
T3 — applyBranding util + color/URL validation + fallbacks (chunks 1, 10-fallback) | 1.5 | — | — | 1.5 |
T4 — Boot plugin + cached-first paint + pixel extendTheme override (chunks 3, 5) | 1.5 | — | 0.5 | 2 |
T5 — App brand color: $q-primary → var(--brand-primary) (chunk 4) | 0.5 | — | — | 0.5 |
T6 — Literal #0274f5 sweep → var(--brand-primary) (chunk 6) | 2.5 | — | 0.5 | 3 |
T7 — <BrandLogo/> + Header/SideNav logo & product name (chunks 8, 9-subset) | 1.5 | — | 0.5 | 2 |
T8 — Runtime head() + nuxt.config defaults + default-tenant parity proof (chunks 7, 9-subset) | 1.5 | — | 0.5 | 2 |
| Grand total | 11.5 | — | 2.5 | 14 |
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: theGET /brandingcontract 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-#0274f5sweep touches ~63 largely untested.vue/.scssfiles (T6 regression risk), and the pixel-v1 timing caveat means a first-ever visit paints default brand for the 4brand.mekarispots 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
| Action | File | What changes |
|---|---|---|
| extend | utils/toggle.js | add isEnableWhitelabelBranding() mirroring isEnableQontakOneSidebar() (utils/toggle.js:11-22): reads ?dev-whitelabel=1 + persists cookie _whitelabel-branding; default false |
| extend | assets/variables/endpoints.js | add export const BRANDING_URL = process.env.HUB_SERVICE_URL + '/api/core/v1/branding' (mirrors CALLS_URL at L170) — confirm base per RFC §5 Q2 |
| create | utils/__test__/toggle.branding.spec.js | flag absent → false; ?dev-whitelabel=1 → true; cookie persists |
Implementation steps
- Explore the relevant codebase area — open
utils/toggle.js:11-22(isEnableQontakOneSidebarcookie/query pattern) andassets/variables/endpoints.js:170(export const X_URL = process.env.HUB_SERVICE_URL + '/api/...'). - Write failing tests (red) —
npx jest utils/__test__/toggle.branding.spec.js: default false, query enables, cookie persists. - Implement — add
isEnableWhitelabelBranding()+BRANDING_URL; keep defaultfalse. - Go green —
npx jest utils/__test__/toggle.branding.spec.js. - Quality gate —
npm run lint:js.
Acceptance criteria
-
isEnableWhitelabelBranding()returnsfalseby default (no regression when unset). -
?dev-whitelabel=1enables it and persists via cookie_whitelabel-branding. -
BRANDING_URLresolves fromprocess.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
| Discipline | Days |
|---|---|
| Frontend | 0.5 |
| Backend | — |
| QA | — |
| Total | 0.5 |
Assumptions: reuses
utils/toggle.jscookie/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
| Action | File | What changes |
|---|---|---|
| create | requests/branding.js | get() → $axios.$get(BRANDING_URL) (no retry); mirrors the requests/* module shape |
| modify | requests/index.js | import + register branding in the default export (beside webchat, L11-21) |
| create | store/branding.js | export 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) |
| create | store/__test__/branding.spec.js | fetch commits payload; getters return values then Qontak defaults when empty; malformed cache = miss (no throw); error → status rejected, no throw |
Implementation steps
- Explore the relevant codebase area — read
store/preferences.js:1-40(module shape,import requests from '../requests',state/getters/mutations/actions) andstore/__test__/usman.spec.js:1(import { state, getters, mutations, actions } from '~/store/...'harness). - Write failing specs (red) —
npx jest store/__test__/branding.spec.js: mockrequests.branding.get; assert commit on 200, defaults when empty, no-throw on error, cache round-trip via alocalStoragestub. - Scaffold — create
requests/branding.js, register inrequests/index.js, createstore/branding.jsstate/getters/mutations. - Wire state — implement
fetchaction + cache helpers; getters expose non-PII branding with Qontak defaults. - Go green —
npx jest store/__test__/branding.spec.js. - Quality gate —
npm run lint:js.
Acceptance criteria
-
store/branding.jsexposesfetchaction + gettersproductName/brandName/assets/links/colors. - On mock 200, state commits payload; on error, status =
rejectedand it does not throw to the caller. -
readCache()returns the last written payload; a malformedlocalStorage['branding'](or wrongschemaVersion) 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
| Discipline | Days |
|---|---|
| Frontend | 2 |
| Backend | — |
| QA | — |
| Total | 2 |
Assumptions: reuses
this.$axios(plugins/axios.js) + therequests/*aggregator; payload is non-PII so cache needs no logout-clear (RFC §2.3). Coverage caveat:jest.config.js:24-31 collectCoverageFromexcludesstore/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_URLconstant).
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
| Action | File | What changes |
|---|---|---|
| create | utils/applyBranding.js | applyBranding(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) |
| create | utils/__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
- Explore the relevant codebase area — confirm there is no existing
:root/setPropertywriter in the repo (greenfield — RFC §2.0); readstore/branding.js(Task 2) for the commit target; readstore/__test__/usman.spec.jsfor the Jest/jsdom harness. - Write failing tests (red) —
npx jest utils/__test__/applyBranding.spec.js: afterapplyBranding(fixture),document.documentElement.style.getPropertyValue('--brand-primary')equals the fixture; an invalid value (e.g.url(evil)) is not written; faviconhrefswapped;fontabsent → no font link;applyBranding(null)throws nothing and writes nothing. - Scaffold — create
utils/applyBranding.jswith the validator helpers + typed shape (JSDoc@typedef Branding). - 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. - Go green —
npx jest utils/__test__/applyBranding.spec.js. - Quality gate —
npm run lint:js.
Acceptance criteria
- Each valid color maps to
--brand-primary/--brand-primary-hoveron:root; the util derives hover if only primary is supplied. - A non-color value is rejected and never reaches
setProperty. -
assets.*/font.cssUrlthat are nothttpson an allowed host are rejected. -
applyBranding(null)(fetch failed, no cache) is a safe no-op — Qontak defaults remain; analyticsbranding.appliedfires 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
| Discipline | Days |
|---|---|
| Frontend | 1.5 |
| Backend | — |
| QA | — |
| Total | 1.5 |
Assumptions: single branding-CDN origin allow-list (RFC §5 Q7 / CSP); no
v-htmlintroduced (RFC §3);--brand-primary-hoverderived 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 (
Brandingshape + 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.mekarispots — 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'] → applyBranding → fetch → 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
| Action | File | What changes |
|---|---|---|
| create | plugins/branding.js | if (isEnableWhitelabelBranding()) { const c = readCache(); if (c) applyBranding(c); store.dispatch('branding/fetch') } — non-blocking; try/catch → warn via datadog, continue |
| modify | plugins/pixel.js | before L201 Vue.use, synchronously read localStorage['branding']; set extendTheme.colors = { brand: { mekari: cached?.colors?.['--brand-primary'] ?? '#651FFF' } } alongside the existing breakpoints |
| modify | nuxt.config.js | register '@/plugins/branding' in the plugins array (L84-98), after @/plugins/pixel and @/plugins/axios |
| create | plugins/__test__/branding.spec.js | flag off → no fetch, :root unmutated; flag on + cache → applies before fetch; fetch reject → no throw, brand unchanged |
Implementation steps
- Explore the relevant codebase area — read
plugins/pixel.js:192-205(customBreakpoints+Vue.use(Pixel, { extendTheme })) andnuxt.config.js:84-98(plugin array, SPA mode so all client-side) andplugins/axios.js(context/$axiosplugin shape). - Write failing test (red) —
npx jest plugins/__test__/branding.spec.js: flag off →store.dispatchnot called and:rootsnapshot == baseline; flag on + seeded cache →applyBrandingruns before the fetch resolves; a rejected fetch produces no unhandled error and leaves the brand unchanged. - Scaffold — create
plugins/branding.js; add the synchronous cache read toplugins/pixel.js. - Wire — register the plugin in
nuxt.config.js; ensure order (pixel + axios before branding). - Go green + build —
npx jest plugins/__test__/branding.spec.jsthennpm run build. - Quality gate —
npm run lint:js.
Acceptance criteria
- Flag off → boot does not fetch
/branding;:rootis 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
/brandingfailure never blocks boot and shows no user-facing error (warn to datadog only). -
plugins/pixel.jsfalls back to#651FFFforbrand.mekariwhen 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
| Discipline | Days |
|---|---|
| Frontend | 1.5 |
| Backend | — |
| QA | 0.5 |
| Total | 2 |
Assumptions: mirrors existing boot-plugin placement; pixel theme is fixed at
Vue.useso the sync cache read is the only way to theme the 4brand.mekarispots 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-primary → var(--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
| Action | File | What changes |
|---|---|---|
| modify | assets/stylesheets/abstracts/_variables.scss | $q-primary: var(--brand-primary, #0274f5); (L6) and $q-primary-hover: var(--brand-primary-hover, #0364d1); (L8) |
Implementation steps
- Explore the relevant codebase area — read
assets/stylesheets/abstracts/_variables.scss:6,8and confirm$q-primaryis consumed via SCSS (124 usages) — the refactor keeps the variable name, only its value source changes. - Change — swap the two literal hex definitions for
var(--brand-*, <hex>). - Verify build + default —
npm run lint:styleandnpm run build; with no--brand-primaryset, computed color must still be#0274f5(fallback). - Quality gate —
npm run lint:style.
Acceptance criteria
-
$q-primary/$q-primary-hoverresolve viavar(--brand-primary[-hover], <today's hex>). - Default tenant (no
--brand-primaryset) renders exactly#0274f5/#0364d1(no visual regression). -
npm run lint:styleandnpm run buildpass.
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
| Discipline | Days |
|---|---|
| Frontend | 0.5 |
| Backend | — |
| QA | — |
| Total | 0.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
| Action | File | What changes |
|---|---|---|
| modify | 53 .vue files (inline style/:style/fill #0274f5) | → var(--brand-primary) |
| modify | 10 .scss files (literal #0274f5) | → var(--brand-primary) (or $q-primary) |
| modify | assets/mixins/messaging/conversation.js:324 | return '#0274f5' → read brand color from the store getter (branding/colors or a --brand-primary read), fallback #0274f5 |
| verify | existing assets/mixins/messaging/__test__/conversation.spec.js | keep green — the default fallback must still return #0274f5 when no branding set (specs at L1009-1339 assert #0274f5) |
Implementation steps
- Explore the relevant codebase area —
grep -rniI '#0274f5' components pages layouts assets(excludingassets/images) to enumerate the 63 CSS/JS spots; separate CSS contexts (→var) from the one JS return (→ getter with hex fallback). - Sweep CSS — replace
.vue/.scssliterals withvar(--brand-primary); keep each element's fallback behavior identical when no tenant var is set. - Sweep JS — change
conversation.js:324to read the branding color with a#0274f5default soconversation.spec.jsstays green (or update the spec to assert the fallback). - Verify no regression —
grep -rniI '#0274f5' components pages layouts assets --include='*.vue' --include='*.scss'(excl. images) → 0;npm test(incl.conversation.spec.js);npm run build. - Quality gate —
npm run lint.
Acceptance criteria
-
#0274f5count in.vue/.scss(excludingassets/imagesSVGs) is 0 after the sweep. -
conversation.jsreturns the tenant brand color when set,#0274f5otherwise;conversation.spec.jspasses (default path unchanged). - Flag off / no tenant var → every swept element still renders
#0274f5(no regression). -
npm testandnpm run buildpass.
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
| Discipline | Days |
|---|---|
| Frontend | 2.5 |
| Backend | — |
| QA | 0.5 |
| Total | 3 |
Assumptions: mechanical but high blast radius across ~63 largely untested
.vuefiles (RFC §2.D) — gated behind the flag and default-fallback; 9 baked-#0274f5SVGs 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-primarymust 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
| Action | File | What changes |
|---|---|---|
| create | components/branding/BrandLogo.vue | props 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) |
| create | components/branding/__test__/BrandLogo.spec.js | renders store logo; falls back to bundled default when store empty |
| modify | components/layouts/Header.vue | replace inline logo <img> at L41, L59 with <BrandLogo/>; route the 10 Qontak literals through the brandName getter |
| modify | components/layouts/SideNav/index.vue | replace logo <img> at L20 with <BrandLogo/>; product name → brandName getter |
Implementation steps
- Explore the relevant codebase area — read
components/layouts/Header.vue:41,59(inline logo<img>+ brand literals) andcomponents/layouts/SideNav/index.vue:20; readstore/__test__/usman.spec.jsfor the component/store test harness andmapGettersusage. - Write failing spec (red) —
npx jest components/branding/__test__/BrandLogo.spec.js: renders tenant logosrc+ product name when store set; store-empty → bundled default logo + "Qontak Chat". - Scaffold — create
BrandLogo.vue(template + props +@errorfallback). - Wire state —
mapGetters('branding', ['assets','brandName']); keep logo dimensions fixed. - Integrate — swap
Header.vue/SideNav/index.vuelogo<img>+ name literals to the component + getter. - Go green —
npx jest components/brandingand re-run any Header/SideNav specs. - Quality gate —
npm 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.vueandSideNav/index.vueno longer contain hardcoded logo<img>paths orQontakproduct-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
| Discipline | Days |
|---|---|
| Frontend | 1.5 |
| Backend | — |
| QA | 0.5 |
| Total | 2 |
Assumptions: bundled default
logo-qontak-default.svgalready exists (verified — used atHeader.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
| Action | File | What changes |
|---|---|---|
| modify | layouts/hub.vue | add a head() component option returning title: brandName, link: [{ rel:'icon', href: favicon }] from mapGetters('branding', …); font <link> handled by applyBranding (T3) |
| modify | nuxt.config.js | leave head.title/favicon (L25, L63) as defaults (still "Qontak Chat" when flag off); no runtime literals removed that would break default |
| create | store/__test__/branding.parity.spec.js | apply 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
- Explore the relevant codebase area — read
nuxt.config.js:24-64(statichead) andlayouts/hub.vue(layout mountingInitComponent); confirm Nuxt 2 uses thehead()component option (nouseHeadin repo). - Write failing test (red) —
npx jest store/__test__/branding.parity.spec.js: two fixtures → distinct--brand-primaryon:root;applyBranding(null)leaves defaults. - Implement
head()— bind title/favicon inlayouts/hub.vueto the branding getters; keepnuxt.config.jshead as fallback. - Go green + build —
npx jest store/__test__/branding.parity.spec.jsthennpm run build. - Quality gate —
npm run lint.
Acceptance criteria
-
document.titleand favicon reflect the store'sproductName/faviconwhen the flag is on; default to "Qontak Chat" /qontak-favicon.icowhen off. - Same
applyBrandingcode path + two fixtures → two distinct:root--brand-primarysets, each equal to its fixture. -
applyBranding(null)(flag off / no payload) → today's Qontak brand exactly (parity proof). -
npm run buildpasses.
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
| Discipline | Days |
|---|---|
| Frontend | 1.5 |
| Backend | — |
| QA | 0.5 |
| Total | 2 |
Assumptions: Nuxt 2
head()component option is the head API (nouseHead/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 lint→npm test→npm run build→npm run analyze(bundle delta) — and confirm green. Post-deploy: watch Datadog RUMbranding_fetch_failed< 5% (15-min window) andbranding.appliedevents (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
extendThemeseed, 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/.scssfiles — 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 /brandingcontract 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
| Story | Reason |
|---|---|
GET /branding endpoint + resolver + Redis + edge allow-list | Out 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 consumption | Out of scope — separate repo (../fe/hub-chat) / separate RFC (rfc-unified-branding-hub-chat-fe.md); keep the payload contract aligned. |
| crm-fe-v3 consumption | Out of scope — separate repo / separate RFC; keep the payload contract aligned. |
Per-tenant color of the 9 baked-#0274f5 SVG assets | Out 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 literals | Deferred — 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 theming | Known 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 enforcement | Deferred — v1 relies on admin/BE-side validation; computed-contrast guard is a future enhancement (RFC §5 Q6). |
| Tenant-admin branding editor UI | Out of scope (RFC §1) — this is a consumer, not an authoring surface. |