Derived from the operator's reference report (~/Downloads/Example of Reporting* — a 6-section
visual dashboard; images decoded to .local_media/report_ref/image1..6.png). This is the design
the Office Reports tab grows toward. v1 (GA4 + GSC, the two live sources) ships first; this
spec is the full target the connectors + comparison engine fill in as channels light up.
Governing rule: the framework ships now with HONEST empty-states; numbers populate as (a) UTM
is stamped at publish, (b) channel keys/auth land, (c) launch traffic flows. **Never fabricate a
number.** A zero with "awaiting launch / connect <X>" is correct; a made-up number is a defect.
Six sections. The signature pattern is identical in all of them: **every metric is a comparison
(YoY / WoW / TY-vs-LY) shown as a big number + a colored delta arrow (↗ green up / ↘ red down),
never a bare number — and every channel rolls up to REVENUE.**
Tapcart[their e-comm app] · Organic Search · Social · Unclassified) × **Sessions TY/LY/YoY% AND
Revenue TY/LY/YoY%**, with a Total row.
· Last-Touch Revenue · Revenue-per-Send, each TY/LY/YoY% and LW/WoW%; + **Email Revenue by
Type** (Campaign vs Flow).
Spend**, TW/LY/YoY%.
trend sparkline + delta. Pulled from the platforms, NOT GA4.
For a SERVICES business (not e-comm): "revenue/AOV/Rev-per-Session" map to **Stripe deal value +
Cal.com bookings**, not cart AOV. "Tapcart" has no analog → drop it; our channel list is the 9 social
+ Email + SMS + Direct + Organic + Referral + (later) Paid.
Mirror of how content-index.json is the content spine: ONE accumulating metrics file is the spine;
every surface bakes from it; connectors write slices; the dashboard never calls an API per render.
content/_generated/metrics-snapshot.json (APPEND-ONLY by date)Must accumulate history so WoW/YoY are computable (you can't compare to last week if you overwrote it).
Schema = a list of rows:
{ "date": "2026-06-08", "channel": "instagram", "campaign": "studio-launch",
"cell": "W23-studio-launch-beat-1", "metric": "impressions", "value": 1234 }
Keys: date (day) · channel (ga4|gsc|email|sms|<9 social>|stripe|calcom|pagespeed) · campaign ·
cell (UTM manualAdContent) · metric · value. Connectors upsert by (date,channel,campaign,cell,metric).
Roll-ups (WoW/YoY, per-channel, per-funnel-stage) are computed at bake time from this one file.
engine/integrations/metrics/<name>.py, each gated, each graceful-empty| Connector | Source | Metrics | Status / gate |
|---|---|---|---|
ga4.py | GA4 Data API (token live) | sessions, users, channelGroup, hostName, pagePath, conversions+revenue (if events configured), CWV | ✅ token — wire conversions/revenue once GA4 events exist |
gsc.py | Search Console (token live) | clicks, impressions, CTR, position, top query/page | ✅ token |
resend.py | Resend API (key present) | sent, delivered, open, click, unsub, bounce → open/click rate, rev-per-send (via UTM join) | key present, pull unwired |
twilio.py | Twilio API | sent, delivered, link-clicks → CTR/CVR/ROAS/spend | needs creds |
stripe.py | Stripe MCP/API | revenue, AOV (deal value), new customers, MRR | Stripe MCP available — the money layer |
calcom.py | Cal.com API | bookings/consults (the services "conversion") | needs key |
social.py | Meta Graph (FB/IG), YouTube Analytics, LinkedIn, X, TikTok, Pinterest, Bluesky — OR Blotato analytics if exposed | impressions, views, engagement, follows, profile-visits | biggest gap — none wired; check Blotato analytics read first |
pagespeed.py | PageSpeed Insights / CrUX (free, no key) | site speed / Core Web Vitals, daily | free — wire early (the "Site Speed" section) |
Each connector: pull(date_range) -> list[row]; missing key → returns [] (the dashboard renders the
honest empty-state). A nightly/build-time scripts/pull_metrics.py runs all available connectors,
upserts into the snapshot. Office bakes from the snapshot (no live API at render → fast, deterministic).
engine/lib/metrics_report.pyPure functions over the snapshot: kpi(metric, channel?, period) → `{value, prev, yoy_pct, wow_pct,
dir}; channel_table(metrics, period) → the §2 grid; delta_arrow(pct)` → the colored ↗/↘ render.
GA4/GSC can compute YoY/WoW NOW via dual date-range queries even before history accrues; other
channels compute from the accumulating snapshot.
engine/lib/utm.py is wired but NOT invoked in distribute.py/publish_run.py. Until every
published link carries utm_source/medium/campaign/content(=cell), the channel→campaign→cell→revenue
join (the right half of the reference) can't populate. → stamp UTM at publish (a prerequisite task).
funnel.yaml (Reach → Capture → Convert)Map each metric into a stage so the report answers "did the calendar move the needle," end-to-end:
Each stage rolls per-campaign + per-week, joined on UTM — the scorecard, not a traffic mirror.
gtm.yaml per-quarter kpi_emphasisRender KPIs as actual-vs-target (the per-quarter operational goals already in gtm.yaml), so the
dashboard is a scorecard with green/red against plan, not just a mirror.
/reports (hub) — Website KPI cards (delta-arrow) + the channel-breakdown table (sessions+revenue,YoY) + the per-week PLAN-vs-RESULT calendar correlation + links to the per-site sub-pages.
/reports/xyz, /reports/com — full per-site dashboards (KPIs · acquisition · hostName subdomaintransparency · top pages · GSC queries/pages · site speed).
/reports/email, /reports/sms, /reports/social— each mirroring the reference's dedicated section.
(GA4/GSC dual-range), the channel table w/ Sessions+Revenue comparison columns (revenue cols render
"awaiting conversion tracking" until UTM+Stripe land), the per-channel section scaffold w/ honest
empty-states.
metrics-snapshot.json + metrics_report.py).pagespeed.py (free), stripe.py (MCP), resend.py (key) →populate Site-Speed + revenue + Email sections.
v1 renders (real .com numbers + honest .xyz empty-state). Each connector: graceful-empty without its
key; with its key, writes dated rows to the snapshot; the comparison engine renders the delta arrow.
No secret/token ever written into apps/office/public/. Every empty cell carries a "connect <X> /
awaiting launch" reason — zero fabricated numbers.