Account Panels, Evaluation Scoring & Reconciliation
How the Account view is assembled (every panel + its data source), the scoring that evaluates an account / an AM portfolio / what's at risk, the manager settings that tune that scoring, and the Google Ads spend↔contract reconciliation. Several pieces are flagged (in development). Verified against the codebase 2026-05-20.
1. Account-level panels & their source
The Account view (src/screens/AccountView.jsx) renders a drag-orderable grid of
cards. One hook — src/hooks/useDashboardData.js — fans out the fetches; route
constants live in src/apiService.js. ${D} = the Worker
(fcr-dashboard-api), ${N8N_BASE} = n8n (fcrmedia.app.n8n.cloud/webhook).
| Panel (card) | Component | Fetched from | Route | Underlying data |
|---|---|---|---|---|
| Revenue Analysis | RevenuePanel |
client-side | — revenueAnalysis.js over the CRM account |
CRM RIs (recurring / setup / one-time) |
| CRM / Dynamics | CrmPanel |
n8n | dashboard-account |
Dynamics CRM + BQ GP listings |
| HubSpot | HubSpotPanel |
Worker | dashboard-hubspot |
BQ hubspot_deals |
| Teamwork Tickets | TicketsPanel |
Worker | dashboard-tickets |
BQ teamwork_open_tickets |
| Teamwork Projects | ProjectsPanel |
Worker | dashboard-teamwork-projects |
BQ project tasks/milestones |
| InSites Audit | InsitesPanel |
Worker | dashboard-insites-bq-lookup + dashboard-insites-poll |
BQ InSites inventory + live poll (KV-cached) |
| GMB Activity Report | GbpActivityPanel |
Worker | copysheet-dashboard-gbp (.activity) |
BQ GMB performance |
| Google My Business | GbpPanel |
Worker → n8n → n8n | copysheet-dashboard-gbp → dashboard-serp-gbp-read → Dashboard-listing-detail |
BQ GMB (managed) → stored SERP snapshot → live SERP fallback |
| GBP Services Gap | GbpServicesPanel |
Worker | dashboard-gbp-services |
BQ |
| Category & Service Gaps | CategoryGapsPanel |
Worker | dashboard-category-gaps |
BQ CATEGORY_* benchmarks |
| Google Ads | GoogleAdsPanel |
Worker | copysheet-ads-stats |
BQ Google Ads transfer (retail spend) |
| SitePro Sites | SiteProPanel |
Worker | dashboard-sitepro |
BQ MegaDoc cache |
| Google Analytics | GA4Panel |
Worker | copysheet-ga4-stats + copysheet-ga4-traffic-sources |
BQ GA4 |
| Search Console | GSCPanel |
Worker | copysheet-gsc-stats |
BQ GSC |
| Ahrefs SEO | AhrefsPanel |
Worker | dashboard-ahrefs-seo |
Ahrefs API v3 (only if account holds an SEO product) |
| Golden Pages Listings | GpListingsPanel |
n8n (account envelope) | dashboard-account |
BQ GP listings |
| Category Keywords | CategoryKeywordsPanel |
Worker | dashboard-category-keywords |
BQ KEYWORD_INTELLIGENCE |
| Keyword Gap Analysis | KeywordGapPanel |
Worker | dashboard-keyword-gap |
BQ |
| Call Tracking | CallTrackingPanel |
Worker | dashboard-iovox |
BQ iovox |
| Yext Publisher Listings | YextPanel |
Worker | dashboard-yext-listings (+ live dashboard-yext-sync) |
BQ cache + Yext API (only if SayMore/Yext products) |
| Google Merchant Centre | GmcPanel |
Worker | dashboard-gmc-stats |
BQ GOOGLE_MC_AUTO |
Header / Health enrichments (not cards): Built-website check
(dashboard-built-websites), Reviews (dashboard-reviews), data-freshness
(dashboard-data-freshness) — all Worker/BQ. Health Vitals (the strip under
the header) and Opportunity Signals are computed client-side (see §2).
Pattern: the CRM account, the GBP SERP fallback, and the GP listings are the only account-panel reads still routed through n8n (
dashboard-account,dashboard-serp-gbp-read,Dashboard-listing-detail). Everything else is Worker → BigQuery.
2. Account evaluation (scoring & "what's at risk")
Three independent, client-side evaluators. All read tunable parameters from the manager config (§3).
2a. Account health score — src/healthScore.js
computeHealthScore(account) → 0–100, label Strong/Moderate/Weak/Critical.
Weighted factors (default weights, must sum to 100):
| Factor | Weight | Logic |
|---|---|---|
| Product diversity | 35 | coverage across Ads / SEO / Web / Listings / Social |
| Revenue level | 25 | banded by total revenue (€500→€5k) |
| Payment status | 15 | active Direct Debit > Credit Card |
| Order recency | 15 | last order within 1/2/3 yrs |
| Active order count | 10 | 1 / 2 / 3+ active orders |
Used per-account in the AM portfolio view (AMView.jsx) and behind the
Health Vitals strip.
2b. AM portfolio score — src/amScore.js
computeAMScore(am) → 0–100 across five dimensions (default weights):
Portfolio Health 30 (product coverage − suspension ratio), Growth 25 (MRR
YoY vs floor/ceiling), Retention 20 (1 − suspension rate), Revenue Density
15 (MRR/account vs ceiling), Engagement 10 (tickets, open deals, call-tracking
& review coverage). amScoreBreakdown() drives the stacked bars in
ManagersView.jsx; an AM scoring below atRiskThreshold (default 40) is
flagged at risk.
2c. Opportunity signals / risk engine — src/opportunityEngine.js (in development — currently disabled)
analyzeOpportunities() cross-references every data source into a sorted list of
41 signals in three classes — upsell (17), retention (11, the "at risk"
signals), action (13) — each with priority, talking point, € impact and an
actionRoute (client_conversation / am_ticket / optimiser_ticket /
proposal / benchmark). Examples: ad-spend declining 3 mo, contract expiring
with no renewal deal, open Credit-Control ticket + suspension, budget-deployment
gap (billed > retail spend), low GMB rating, traffic/clicks decline.
The on-screen Opportunity Signals card is temporarily disabled in
AccountView.jsx("disabled during n8n migration") — the engine + all thresholds/toggles still exist and feed the AI advisor/insights.
3. Manager settings — src/config/managerConfig.js + src/components/ConfigPanel.jsx (in development — hidden tab)
Everything in §2 is tunable. ConfigPanel (rendered inside the Managers tab,
which is hidden: true in Dashboard.jsx) lets a manager edit:
- Health weights (the 5 factors in §2a, must sum to 100)
- AM score weights (the 5 dimensions in §2b, must sum to 100)
- AM score params —
atRiskThreshold(40),densityCeiling(500),growthFloor(−10),growthCeiling(20) - Signal thresholds — ~25 knobs (ad CTR floor, utilisation min, bounce max, avg-position max, min reviews/rating, answer-rate bands, InSites score mins, HubSpot disengagement days, contract-expiry windows 30/60/90)
- Signal toggles — on/off for each of the 41 signals
Storage: browser localStorage key dashboard-manager-config (per-user,
not server-side), read via getConfig() / written via saveConfig().
Because config is local, settings don't yet sync across users/devices — promoting this to a shared store is the obvious next step.
4. Google Ads reconciliation — spend vs contract value (in development — hidden tab)
The Spend tab (src/screens/SpendView.jsx, hidden: true) compares CRM
contracted order value (DMS - SEA) against actual Google Ads retail spend,
per account and in aggregate, surfacing the exposure (contracted − actual)
and a % under-deployment, plus DEA per-month allocation, opening balance, and
month pacing.
- Per account:
ADS_SPEND_URL→dashboard-ads-spend - All accounts list:
ADS_SPEND_ALL_URL→dashboard-ads-spend-all
Both Worker handlers are deliberate stubs that
throwto trigger the n8n fallback — the live spend-reconciliation logic (date-aware MRR, DEA allocation, opening balance, cancelled lines) currently lives in n8n. There's a standing TODO to port that logic back into the Worker handlers (worker/src/handlers/ads-spend.js,ads-spend-all.js).
Related: cross-view MRR reconciliation
Separately, the Reconciliation tab (ReconciliationView.jsx →
dashboard-reconciliation, Worker/BQ) checks that the four billing views
agree — AM Portfolio vs Solution Portfolio vs Revenue Bridge vs the
active_clients benchmark — by product group, and (in mode=diff) lists
accounts present in one view but not another, or with an MRR delta > €1. See
billing-rules-comparison.md.
Sources: AccountView.jsx, useDashboardData.js, apiService.js,
healthScore.js, amScore.js, opportunityEngine.js, config/managerConfig.js,
ConfigPanel.jsx, SpendView.jsx, worker/src/handlers/{reconciliation,ads-spend,ads-spend-all}.js.
FCR Dashboard documentation · generated from docs/ · keep counts verified, not guessed.