Phase 6a of the dashboard overhaul (plan:
~/.claude/plans/thsi-is-a-app-zippy-reef.md). Closes the
white-labelling story — every brief gets its own accent, heading font,
background preset, and agency label, inherited by every report
generated from it. Logo upload lands in Phase 6b.
Brief schema (v2/server/schemas/brief.ts):
- New BRIEF_THEME Zod object: accent_hex (#rrggbb), accent_2_hex
(optional, auto-derived), heading_font (fraunces|playfair|inter|
space-grotesk), background (cream|paper|ink), agency_name (≤40 chars),
logo_path (Phase 6b placeholder).
- Brief row gets a new theme JSONB column. Idempotent boot-time
ALTER TABLE IF NOT EXISTS picks up the column on existing prod DBs;
init.sql also updated for fresh installs.
Server (v2/server/):
- DAO: setBriefTheme(id, theme | null), BriefRow.theme typed.
- New endpoints (editor role required):
PUT /api/briefs/:id/theme — write theme JSON
DELETE /api/briefs/:id/theme — reset to defaults (NULL)
- publicBrief() exposes theme so the operator-app prefills the editor.
Pipeline (v2/pipeline/):
- New lib/colors.ts: deriveAccent2(hex) — HSL math to compute a darker
companion accent when the picker only specified one. Mirrors the
Original-project relationship between sienna #c2602a and oxblood
#8a3a1a.
- Stage 10 takes a third optional param (theme: BriefTheme | null) and
injects dataset.theme into dataset_v2.json with accent_2 always
populated. Cli.ts call sites pass briefRow.theme.
Dashboard SPA (v2/templates/dashboard_template/):
- Types extended with DatasetTheme.
- App.tsx applies theme at boot via document.documentElement.style
.setProperty before first render — avoids a colour-flash on any
non-default theme. FONT_STACKS map heads_font enum to the actual
CSS font stack (fonts already preloaded in index.html).
- Background preset 'paper' lightens; 'ink' flips the surface/text
axis for dark-deck reports (still picks up the brand accent).
- Topbar renders agency_name + logo placeholder. Falls back to
"SOCIAL LISTENING" eyebrow when no theme is set.
Operator app (v2/operator-app/):
- New ThemeEditor component on the brief edit page:
- 8 accent preset swatches (Sienna/Oxblood/Forest/Slate/Olive/
Wine/Plum/Ink) + custom hex input.
- 4 heading-font tiles each rendering "The Branded Glass Moment"
in the candidate stack — WYSIWYG without a separate preview.
- 3 background presets (Cream/Paper/Ink) shown as colour swatches.
- Agency name text input (≤40 chars).
- Save / Reset to defaults.
- New hooks: useUpdateBriefTheme, useResetBriefTheme.
What's intentionally NOT in this phase:
- Logo upload (Phase 6b — needs multipart parser, sharp downscaling,
SVG sanitisation).
- Free-form CSS textarea (out of scope by design — maintenance trap).
- Custom body / line / format / maturity colours (categorical signals;
changing them breaks comparability across reports).
Both Vite builds pass. tsc --noEmit clean. mom_compare unit test
fixture untouched (no schema break).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 1000 likes / 10000 plays defaults are calibrated for top-of-funnel
beauty/fitness scrapes; in narrower TikTok niches almost every video lands
below them and Stage 2 returns 0 keepers. Defaults dropped to 100 likes /
1000 plays across:
- server/schemas/brief.ts (Zod default)
- db/init.sql (column default for new DBs)
- examples/dove-demo-brief.json
- operator-app's brief-form initial values
- operator-app's "Load Dove demo" inline brief
Stage 3 empty-pass1 error now reads pass1/spend_log.json and reports the
actual scrape breakdown — total $ spent, total raw videos returned, and
how many got dropped by each floor (zero-plays / min_plays / min_likes /
min_stl_pct). So instead of a generic "lower the floor", the user sees:
"Spent $5.42 across 7 scrapes; 1400 videos returned. Dropped: 12 zero-plays,
1305 below min_plays=10000, 31 below min_likes=1000."
Existing briefs are unaffected (column default applies to NEW rows). For the
in-flight Dove2 run the user can edit the brief and lower the floor, then
click Retry pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
V2 lives entirely under v2/ and is built around three asks the team raised
about V1: per-video assets sometimes drifted onto the wrong trend, hashtag
scrapes returned junk that wasn't filterable per-client, and there was no
multi-user model behind Microsoft SSO.
Highlights:
- Stable TikTok numeric-id key for every per-video asset; URL form drift is
logged loudly to drift_log.jsonl and never silently nulls assets. Stage 5
manifest hard-gates Stage 6 if any selected video is missing any required
asset; --drop-failing auto-backfills from the next-best recipe candidates.
- Per-brief engagement floor (min_likes / min_plays / min_stl_pct), applied
at Apify scrape time and re-validated locally; spend_log.json records
raw_returned vs kept_after_floor per scrape.
- Users + teams + memberships with owner/admin/editor/viewer roles; SSO
upserts a user keyed on Azure oid, auto-creates a personal team, and a
super-admin is bootstrapped via BOOTSTRAP_SUPER_ADMIN_EMAIL on first
sign-in. Phase A integration test: 16/16 pass.
- 10-stage TS pipeline (brief → seed → scrape1 → select → scrape2 →
validate → analyse → insights → trends → qa → build) wired through one
CLI; each stage idempotent + resumable from disk via .state sentinels.
§4.5 rubrics shipped under prompts/ and loaded into Claude calls.
- React 18 + Vite + TS + Tailwind operator SPA: brief intake form,
team management, super-admin user list, help/FAQ ported from V1.
- Separate Docker Compose project (name: social-reporting-v2, port 3457,
Postgres 5437) with deploy/setup-v2.sh, deploy-v2.sh, rollback-to-v1.sh
scripts that take over V1's /social-reports URL and let us roll back.
Verification: 62 unit tests pass (auth/session, ids extractor with full URL
fixture, engagement floor, recipes, manifest, linking-fix, MoM compare).
Live smoke run on a Dove brief: 1400 raw → 253 kept (82% culled) → 21
fully-bundled videos → 25 editorial trends across 8 brief-driven categories,
with drift=0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>