csm-activation-rolluprun.md00_intake/stub.mdThe ActivationStatusTable shows one row per CSM/region with totalAccounts, a RAG breakdown
(green/amber/red), goLivesNext7Days, atRiskNow and a human summary — all dummy
(apps/web/lib/mock/csm.ts activationStatus). There is no
aggregation layer: nothing rolls a CSM's individual accounts into a portfolio RAG, counts go-lives in a
window, or summarises the worst issues. The per-account health these counts are built from is computed
by the customer epic's health engine, not here.
@sustentus/services/server that, for each CSM (and
region), rolls up their owned active-project leads into: totalAccounts, RAG counts
(green/amber/red from each account's computed overallStatus), goLivesNext7Days (accounts
whose goLiveDate is within 7 days), and atRiskNow (amber + red).summary narrative from the CSM's worst open issues (e.g. "Customer approval
blocking 3 accounts — 2 SLA breaches in delivery").overallStatus,
goLiveDate), not a re-implemented heuristic.summary is generated from the CSM's real open issues; with no issues it reads as healthy, not a
mock string.changeVsYesterday renders gracefully as absent/zero until csm-portfolio-snapshot lands.changeVsYesterday day-over-day delta — that needs the daily snapshot (csm-portfolio-snapshot,
feature 3); this feature leaves a clean seam for it.customer-dashboard-data/project-health-engine.customer-dashboard-data/project-health-engine: if it has landed, read its
overallStatus/goLiveDate/riskStatus per lead; if not, agree a minimal read interface so this
feature is not blocked — but do not fork the heuristic.summary reads open blockers, so it reads best after csm-blocker-queue (feature 4); design it
to degrade to a health-only narrative when blocker data isn't available — keep depends-on as the
foundation only.touches: packages/services/src/server (portfolio rollup derivation),
apps/web/components/dashboard/csm/activation-status-table.tsx.01_define/output/spec.mdactivationStatus mock)The CSM Go live control dashboard's activation-status table shows one row per CSM/region —
totalAccounts, a RAG breakdown (green/amber/red), goLivesNext7Days, atRiskNow and a human
summary — entirely from dummy data (apps/web/lib/mock/csm.ts activationStatus, filtered by a
hard-coded MOCK_CSM = "Sarah Chen" in csm-mock-sections.tsx). The csm-portfolio-foundation
feature (now merged) made /csm a real server-rendered, per-CSM page and stood up the
csmPortfolioService envelope of the signed-in CSM's owned active-project accounts — but the
activation table is still one of the mock sections it deliberately left untouched. There is no
aggregation layer that rolls those owned accounts into a portfolio RAG, counts go-lives in a window,
or summarises the worst issues. This is feature 2 of the csm-dashboard-data epic, advancing
Build the Bridge / Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow by
making the CSM exception-management view run on real per-tenant, per-portfolio delivery data. The CSM
is the exception manager for delivery; this table is their portfolio triage entry point, so it must
reflect reality.
Extend the merged csmPortfolioService (packages/services/src/db/services/csm-portfolio) with
a portfolio-aggregation rollup that reuses the foundation's tenant-scoped, ownership-filtered
active-project account set (the same query behind getPortfolioForCsm) and rolls it up, per CSM,
into one activation row:
totalAccounts — count of the CSM's owned active-project leads (tenant-scoped).green / amber / red) — tallied from each account's computed overallStatus
supplied by the customer epic's health engine (see read interface below).goLivesNext7Days — accounts whose goLiveDate falls within 7 days of now.atRiskNow — amber + red.summary — a short narrative derived from the CSM's worst real open issues (e.g. "Customer
approval blocking 3 accounts — 2 SLA breaches in delivery"); with no open issues it reads as
healthy, never a canned mock string.Scope follows the foundation's existing toggle: scope: "mine" (the ?scope=all toggle's default)
returns only the signed-in CSM's row; scope: "tenant" (?scope=all) returns one row per CSM in the
tenant. Fetch the rollup in the server page (/csm/dashboard) alongside the existing portfolio
envelope and pass the real rows into the activation table — today the table is rendered inside the
csm-mock-sections.tsx client island from mock activationStatus + MOCK_CSM; this feature feeds it
server-derived data and removes that mock path. Delete the activationStatus mock array.
Health read interface (do not fork the heuristic). The rollup reads per-lead computed health
owned by customer-dashboard-data/project-health-engine — overallStatus (→ green/amber/red),
goLiveDate, and riskStatus per active-project lead — and never recomputes its own RAG/go-live
heuristic. The foundation's accounts[] already carry the lead's workflow status, not this
computed health, so the rollup reads the health fields directly. The health engine has not landed
yet, so this spec fixes the minimal read contract (those three fields per active-project lead) so
Build is not blocked, and the rollup degrades gracefully: a lead missing overallStatus is
excluded from the RAG tally, and a lead missing goLiveDate contributes zero to goLivesNext7Days,
rather than the service inventing a heuristic.
csm-portfolio-foundation provides lead.csm (ref user, role csm,
indexed { csm: 1, status: 1 }), region at lead.location.region, the server-rendered
/csm/dashboard page, the ?scope=all ("mine" vs "tenant") toggle, and csmPortfolioService /
CsmPortfolioEnvelope exposed via @sustentus/services/server. This feature adds the rollup to
that service and wires the activation table; it does not rebuild ownership, region, the page
shell, the toggle, or the envelope.customer-dashboard-data/project-health-engine per the
read interface above.totalAccounts, RAG counts, goLivesNext7Days, atRiskNow), reusing the foundation's
tenant-scoped ownership query — no value comes from apps/web/lib/mock/csm.ts
activationStatus, and MOCK_CSM no longer drives the table.overallStatus / goLiveDate via the read interface — the service does not re-implement a RAG
or go-live heuristic.atRiskNow equals amber + red; goLivesNext7Days counts accounts whose goLiveDate is
within 7 days of now.summary is generated from the CSM's real open issues; with no open issues it reads as
healthy, not a mock string.scope: "mine" returns only the signed-in CSM's row; scope: "tenant" (?scope=all) returns
one row per CSM in the tenant — matching the foundation's existing toggle.changeVsYesterday renders gracefully as absent / zero (no mock delta) until
csm-portfolio-snapshot lands.activationStatus mock array is removed from
apps/web/lib/mock/csm.ts.changeVsYesterday day-over-day delta — needs the daily snapshot (csm-portfolio-snapshot,
feature 3); leave a clean seam (absent/zero) for it.project-health-engine; it never forks the
heuristic.lead.csm ownership, region, the server page shell, the
scope toggle, or the csmPortfolioService envelope.csm-mock-sections.tsx mock island this round.apps/dashboards — that app stays the dummy source of truth.project-health-engine's spec when it lands; this spec fixes the contract —
overallStatus / goLiveDate / riskStatus per active-project lead — so Build is not blocked and
the rollup degrades gracefully if a field is absent. The activation summary reads best after
csm-blocker-queue (feature 4); it degrades to a health-only narrative when blocker data is
unavailable, so depends-on stays the foundation only.)02_build/output/notes.mdfeat: csm-activation-rollup — per-CSM activation rollup on the portfolio service,
feat: csm-activation-rollup — wire the activation table to real rollup datapackages/services/src/db/services/csm-portfolio/index.ts: added getActivationRollup(tenantId, csmUserId, scope) to csmPortfolioService, plus the CsmActivationRow / CsmActivationRag types.
It reuses the foundation's tenant-scoped ownership filter (isActiveProject + csm), groups the
CSM's active-project leads by owner, and rolls each group into one row: totalAccounts, RAG counts
(from each lead's computed overallStatus), goLivesNext7Days (leads whose goLiveDate is within
7 days), atRiskNow (amber + red), changeVsYesterday (0 — seam for the snapshot feature), and a
summary. summary is built from real open blockers via blockerService.listOpenByLeads (dominant
blocking party + accounts held up + escalations), degrading to a health-only line ("N at risk" /
"All accounts on track") when there are none. scope: "mine" yields the signed-in CSM's row;
"tenant" yields one row per owning CSM.overallStatus / goLiveDate are read defensively
off the lead (the fields project-health-engine owns). They aren't in the schema yet, so today they
read as absent and the rollup degrades gracefully — real account counts, zeroed RAG/go-live — rather
than re-deriving health. When the engine lands and populates those fields, the counts light up with
no change here.packages/services/src/db/services/index.ts: export the new CsmActivationRow / CsmActivationRag
types (barrelled to @sustentus/services/server).apps/web/app/(app)/csm/dashboard/page.tsx: resolvePortfolio → resolveDashboard, now fetching
the portfolio envelope and the activation rollup together (Promise.all) for the request scope, and
passing the rows into CsmMockSections.apps/web/components/dashboard/csm/csm-mock-sections.tsx: takes activationRows and feeds them to
the activation table. Dropped the MOCK_CSM constant, the activationStatus mock import, and the
client myAccountsOnly state/filter — scope is now the foundation's server ?scope=all toggle.apps/web/components/dashboard/csm/activation-status-table.tsx: exports the ActivationRow type;
removed its redundant "My accounts / Team view" toggle (the foundation's page-level toggle is the one
scope control). Row-click still drives the blocker filter.apps/web/lib/mock/csm.ts: removed the activationStatus mock array (the blockers / stageSLA
mocks remain for the still-deferred sections).activationStatus mock, MOCK_CSM removed.overallStatus / goLiveDate via the read interface —
no re-implemented heuristic (zeroed until project-health-engine lands).atRiskNow = amber + red; goLivesNext7Days counts goLiveDate within 7 days of now.summary is generated from real open blockers; degrades to healthy/at-risk line with none.scope: "mine" returns the signed-in CSM's row; scope: "tenant" (?scope=all) one row per CSM.changeVsYesterday is 0 (no mock delta) until csm-portfolio-snapshot lands.activationStatus mock array is removed.project-health-engine lands, RAG/at-risk/go-live counts render as 0 by design (graceful
degradation) — the rows still show real account totals and real blocker-derived summaries.region is the CSM's predominant account region (mode), since the foundation
models region per-lead (lead.location.region), not per-CSM.?scope=all toggle, to avoid two competing controls.03_release/output/changelog.mdPublished as apps/help/app/changelog/2026-06-23-csm-activation-rollup/page.mdx (personas: csm).
Your go live status table now rolls up your real portfolio instead of sample numbers:
03_release/output/investor-update.mdWho it's for: Customer success managers (CSM) What shipped: The CSM activation table now rolls up each CSM's own accounts — totals, at-risk count, go-lives due this week, and a summary from real open blockers — replacing placeholder data. Why it matters: Build the Bridge, Q2 2026 Objective 3 (validate technical infrastructure & payout flow): CSMs push stuck accounts to go live on real signals.
Dig deeper: <merged-PR URL> · <changelog entry URL>