Skip to Content

← All archived runs

Run: csm-activation-rollup

run.md

Run: csm-activation-rollup

  • branch: claude/upbeat-gates-dxjj9m
  • pr: #510

00_intake/stub.md

Stub: CSM dashboard — activation-status portfolio rollup

  • feature-slug: csm-activation-rollup
  • epic: csm-dashboard-data
  • personas: CSM
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: csm-portfolio-foundation
  • sequence: 2 of 5

Problem

The 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.

Proposed change

  • Build a portfolio-aggregation derivation in @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).
  • Derive the per-row summary narrative from the CSM's worst open issues (e.g. "Customer approval blocking 3 accounts — 2 SLA breaches in delivery").
  • Wire the ActivationStatusTable to this output (My-accounts row by default; all CSMs when the toggle is widened).

Acceptance criteria (rough)

  • Each activation row is a real per-CSM/region rollup of owned accounts — RAG counts, total, go-lives-next-7-days, at-risk-now — no mock.
  • RAG and go-live counts derive from the customer epic's computed health (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.

Out of scope (this feature)

  • The changeVsYesterday day-over-day delta — that needs the daily snapshot (csm-portfolio-snapshot, feature 3); this feature leaves a clean seam for it.
  • Re-computing per-account health — this reads customer-dashboard-data/project-health-engine.

Notes for Define

  • Coordinate with 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.
  • The 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.md

Spec: CSM dashboard — activation-status portfolio rollup

  • slug: csm-activation-rollup
  • personas: CSM
  • touches: packages/services/src/db/services/csm-portfolio (activation rollup on the portfolio service), apps/web/app/(app)/csm/dashboard/page.tsx, apps/web/components/dashboard/csm/csm-mock-sections.tsx, apps/web/components/dashboard/csm/activation-status-table.tsx, apps/web/lib/mock/csm.ts (remove activationStatus mock)
  • complexity: standard

Problem

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.

Proposed change

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).
  • RAG counts (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-engineoverallStatus (→ 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.

Dependencies (context for Build — not this feature's work)

  • Merged — build on it: 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.
  • Read (coordinated, not yet landed): customer-dashboard-data/project-health-engine per the read interface above.

Acceptance criteria

  • Each activation row is a real per-CSM rollup of that CSM's owned active-project leads (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.
  • RAG counts and go-live counts derive from the customer health engine's per-lead 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.
  • All queries are tenant-scoped; the activationStatus mock array is removed from apps/web/lib/mock/csm.ts.

Out of scope

  • The changeVsYesterday day-over-day delta — needs the daily snapshot (csm-portfolio-snapshot, feature 3); leave a clean seam (absent/zero) for it.
  • Re-computing per-account health / RAG — this reads project-health-engine; it never forks the heuristic.
  • Rebuilding anything the foundation owns — lead.csm ownership, region, the server page shell, the scope toggle, or the csmPortfolioService envelope.
  • The blockers queue, action strip, and communication sections — separate features (4, 5); they stay in the csm-mock-sections.tsx mock island this round.
  • Replicating any change back into apps/dashboards — that app stays the dummy source of truth.

Open questions

  • none. (Non-blocking note: the exact field names of the health read interface finalise against 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.md

Build notes: csm-activation-rollup

  • commits: feat: csm-activation-rollup — per-CSM activation rollup on the portfolio service, feat: csm-activation-rollup — wire the activation table to real rollup data

What changed

  • packages/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.
  • Health read interface (no forked heuristic): 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: resolvePortfolioresolveDashboard, 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).

Acceptance criteria status

  • Each activation row is a real per-CSM rollup of owned active-project leads, reusing the foundation's tenant-scoped ownership query — no activationStatus mock, MOCK_CSM removed.
  • RAG and go-live counts derive only from 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.
  • All queries are tenant-scoped; the activationStatus mock array is removed.

Verify result

  • mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. Release reads them back from the PR's check runs.

Notes for review

  • Until 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.
  • One row is per CSM; region is the CSM's predominant account region (mode), since the foundation models region per-lead (lead.location.region), not per-CSM.
  • The activation table's own scope toggle was removed in favour of the foundation's single page-level ?scope=all toggle, to avoid two competing controls.

03_release/output/changelog.md

Changelog: csm-activation-rollup

Published as apps/help/app/changelog/2026-06-23-csm-activation-rollup/page.mdx (personas: csm).


See real go live status across your accounts

Your go live status table now rolls up your real portfolio instead of sample numbers:

  • See how many accounts you own, how many are at risk now, and how many are due to go live in the next seven days.
  • Each row's summary is drawn from your accounts' real open blockers — who is holding things up — and reads as healthy when there is nothing outstanding.
  • The rollup follows the same "My accounts" and "All accounts" toggle, so you can read it for your own portfolio or the wider team.

03_release/output/investor-update.md

CSMs now triage their portfolio on real go live data

Who 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>