Channel Health Scores Missing for Website, LandingPage, and Google Ads
Date: 2026-05-05
Status: ✅ Fixed
Affected channels: Website, LandingPage, Google Ads
Symptoms
The Channel Health dashboard (/channels/health) showed “Not scored yet” for Website, LandingPage, Google Ads, and GitHub channels even when those channels were connected and had data. Facebook, GSC, GA, GBP, and LinkedIn displayed scores correctly.
Google Ads was the most confusing case — it showed “Last scored: 5 May 2026” (the insight job had run) but still displayed “Not scored yet” (the score itself was null).
Root Causes
Three separate bugs — one per channel type.
1. Google Ads — insight ran but healthScore was null
google-ads-insights.worker.ts called runInsightWorker without passing healthScore or healthDimensions. The computeCampaignScore function existed in health-score-calculator.ts but was never called by this worker. So the ChannelInsight record was created with status: "done" and a completedAt timestamp, but healthScore was always null.
Files:
packages/agents/src/workers/insights/google-ads-insights.worker.ts
2. Website — scores in WebsiteInsight, not ChannelInsight
website-insights.worker.ts writes scores to the WebsiteInsight model (overallScore, seoScore, contentScore, technicalScore). The buildHealthSummary function in channel-health-summary.ts only reads from ChannelInsight. So even after a successful crawl and analysis, the score was invisible to the health dashboard.
Files:
apps/api/src/routers/tenant/channel-health-summary.ts
3. LandingPage — scores in LandingPage, not ChannelInsight
landing-page-scorer.worker.ts writes scores to individual LandingPage records (seoScore, conversionScore, brandScore). There is no connectedChannelId on LandingPage and no corresponding ChannelInsight record. buildHealthSummary had no way to surface these scores.
Files:
apps/api/src/routers/tenant/channel-health-summary.ts
4. GitHub — by design, no fix needed
No insights worker exists for GitHub and there are no meaningful performance metrics to score. “Not scored yet” is the correct state.
Fixes
Fix 1: Google Ads worker — compute and pass healthScore
Added computeHealthScore() to google-ads-insights.worker.ts:
- Moved
syncCampaignMetricsto run beforerunInsightWorker(it was previously a post-insight side-effect), so the DB has freshCampaignMetricsrows before the score is computed. - Added
computeHealthScore()which queriesCampaignExternalMappingto get campaign IDs for the channel, then aggregatesCampaignMetricsfor current (last 30d), previous (30–60d), and all-time windows. - Calls
computeCampaignScore()fromhealth-score-calculator.tsand passes the result ashealthScore+healthDimensionstorunInsightWorker.
If no CampaignExternalMapping records exist (no campaigns synced yet), computeCampaignScore receives all-zero inputs and returns overall: 10 (“Insufficient data” path).
Fix 2 & 3: buildHealthSummary — add type-specific score lookups
Refactored buildHealthSummary in apps/api/src/routers/tenant/channel-health-summary.ts:
- Load channels first, then extract
websiteChannelIdsand checkhasLandingPageChannel. - Add two parallel queries alongside the existing
ChannelInsightquery:WebsiteInsight: fetch the latest completed record perconnectedChannelIdfor Website channels, selectoverallScore+completedAt.LandingPage: aggregate_avg { seoScore, conversionScore }and_max { brandScoredAt }for approved pages with a non-nullseoScore.
- Added
resolveScore(channel)helper that picks the right data source per channel type:"Website"→WebsiteInsight.overallScore"LandingPage"→Math.round((avgSeoScore + avgConversionScore) / 2)- everything else →
ChannelInsight.healthScore
- Used
resolveScore()throughout: inconnectedScores(overall average),channelDatamapping, and the attention items loop.
This also fixes the same gap in the DM portal — it imports buildHealthSummary from the same file.
Testing
- Website score appears immediately once any
WebsiteInsightrecord withstatus: "done"exists for the channel. - LandingPage score appears once any landing page reaches
status: "approved"and has been scored by thelanding-page-scorerworker. - Google Ads score appears on the next insight run (re-enqueue
agent__google-ads-insightsor wait for the scheduler).
Pattern Note
Channels whose insight workers use a separate model (not ChannelInsight) must be handled explicitly in buildHealthSummary. Current non-standard channels:
| Channel type | Score model | Score field(s) |
|---|---|---|
Website | WebsiteInsight | overallScore |
LandingPage | LandingPage | avg of seoScore + conversionScore |
| all others | ChannelInsight | healthScore |
Any future channel whose worker does not write to ChannelInsight must add a corresponding branch to resolveScore().