csm-portfolio-foundationrun.md00_intake/stub.mdThe CSM "Go live control" dashboard (apps/web/app/(app)/csm/dashboard/page.tsx)
is a "use client" page reading entirely from apps/web/lib/mock/csm.ts,
with a hard-coded MY_CSM = "Sarah Chen". Two things block any section from being real: (1) the page is
not scoped to the signed-in CSM, and (2) there is no way for a CSM to own an account — the lead
model has manager, expert, vendor, customer but no csm reference, so "My accounts" and every
per-CSM rollup have nothing to group by. Region grouping exists on the account (lead.location.region)
but is unused.
csm reference to the lead model (a user whose role is csm), so an active-project
lead can be owned by a CSM; index it for per-CSM queries. Surface the existing location.region./csm (and /csm/dashboard) to a server-fetched page scoped to the signed-in CSM, with
the "My accounts" vs all toggle resolving to a real query (lead.csm = me vs the wider tenant
set) instead of the client-side MY_CSM string filter.@sustentus/services/server that the activation rollup, blocker queue and outreach sections all
read from (one portfolio query → the CSM's active-project leads).csm owner (user, role csm); the field is queryable/indexed per tenant./csm is server-rendered and scoped to the signed-in CSM; "My accounts" shows only their owned
accounts, the toggle widens to the tenant set.lib/mock/csm.lead.csm set explicitly, or defaulted from region/territory? At
minimum the field + index must exist; seeding can be manual.touches: packages/services/src/db/models/lead.ts (add csm ref + index),
packages/services/src/server (portfolio read-service),
apps/web/app/(app)/csm/{page.tsx,dashboard/page.tsx} (server-fetch + scope).01_define/output/spec.mdThe web CSM "Go live control" dashboard (apps/web/app/(app)/csm/dashboard/page.tsx) is a
"use client" page that reads entirely from apps/web/lib/mock/csm.ts, with a hard-coded
MY_CSM = "Sarah Chen" and a client-side filter on that name. Two things block any section from being
real: (1) the page is not scoped to the signed-in CSM, and (2) there is no way for a CSM to own an
account — the lead model has manager, expert, vendor, customer references but no csm, so
"My accounts" and every per-CSM rollup have nothing to group by. This is sequence 1 of 5 in the
csm-dashboard-data epic and the prerequisite for the other four sections (activation rollup, daily
snapshot, blocker queue, outreach). It advances Build the Bridge / Q2 2026 Objective 3 — Validate
Technical Infrastructure & Payout Flow by proving the CSM dashboard can resolve real, tenant- and
CSM-scoped portfolio data end to end.
csm reference to the lead model — a user whose role is csm (ref: "user", mirroring
the existing manager field), with an index { csm: 1, status: 1 } to match the other ownership
indexes, so an active-project lead can be owned by a CSM and queried per tenant. Assignment is
seeded / back-office this round (no UI) — the dashboard reads the assignment, it does not manage
it. The account region already exists on lead.location.region; the portfolio service surfaces it (no
new field)./csm and /csm/dashboard to a server component that resolves the Clerk-authenticated
CSM → their tenant (getTenant) → the CSM app user (resolveAppUserForTenant, role === "csm"),
reusing the same helpers the customer dashboard already uses. The page header renders the real
signed-in CSM identity, not the hard-coded MY_CSM string.csmPortfolioService in @sustentus/services/server
(new db/services/csm-portfolio/ following the established index.ts class + instance.ts singleton
pattern, exported through db/services/index.ts). It returns one typed portfolio envelope — the
single fetch entry every other section in the epic extends rather than re-querying leads:header: the CSM's display name + initials.accounts[]: the CSM's active-project leads (lead.isActiveProject = true) → { id (requestId), account (customer company / lead title), region, status, customerId }."mine" filters lead.csm = <signed-in CSM>; "tenant" returns the whole
tenant's active-project set. Both are always tenant-isolated.mine vs tenant) so the rendered account set is real and the toggle has an observable
effect (mine → the CSM's owned accounts; all → the tenant-wide set). The client-side MY_CSM string
filter and the myAccountsOnly name filter are removed.apps/web/lib/mock/csm.ts. The activation-rollup
metrics, the blocker queue + action strip, and the communication section stay on the mock this round —
each is wired to real data by its own feature (2, 4, 5). Render a loading state while the portfolio
resolves and a defined empty state ("no accounts assigned yet") when the CSM owns no accounts — no
crash, no mock fallback for the header/portfolio.lead can be assigned a csm owner (a user with role csm); the field is indexed and
queryable per tenant ({ csm: 1, status: 1 })./csm (and /csm/dashboard) is server-rendered and scoped to the signed-in CSM; the header shows
the real signed-in CSM, not the hard-coded Sarah Chen.csmPortfolioService returns a single typed portfolio envelope (header + accounts[] with
region) from @sustentus/services/server, scoped by tenant: scope: "mine" returns only leads
where lead.csm is the signed-in CSM, scope: "tenant" widens to the tenant's active-project set
— a CSM can never see another tenant's accounts.apps/web/lib/mock/csm.ts; the still-mocked sections
(activation table, blockers, communication) continue to render unchanged pending their own features.changeVsYesterday,
the narrative summary) — feature 2 (csm-activation-rollup); the activation table stays mock-driven
this round.changeVsYesterday a real delta — feature 3
(csm-portfolio-snapshot).csm-blocker-queue); stays mock this round.csm-outreach-thread); stays mock this round.csm onto existing leads.02_build/output/notes.mdfeat: csm-portfolio-foundation — lead.csm ownership + portfolio read-service,
feat: csm-portfolio-foundation — server-render /csm scoped to the signed-in CSMpackages/services/src/db/models/lead.ts: added a csm reference (ref: "user", mirroring
manager) to both ILeadPopulated (PopulatedUser) and ILead (ObjectId, added to the Omit),
the schema field, and the index { csm: 1, status: 1 } (alongside the existing manager/expert/vendor
ownership indexes). Region is unchanged — it already lives on lead.location.region.packages/services/src/db/services/csm-portfolio/{index.ts,instance.ts}: new csmPortfolioService
with getPortfolioForCsm(tenantId, csmUserId, scope) returning the typed CsmPortfolioEnvelope
(header = the CSM's name/initials; accounts[] = active-project leads → { id, account, region, status, customerId }). scope: "mine" filters lead.csm = <csm>; scope: "tenant" widens to the
tenant's active-project set. Both filters always carry tenantId, so a CSM can never read another
tenant.packages/services/src/db/services/index.ts: export the new service instance + its types (barrelled
to @sustentus/services/server via db/index.ts).apps/web/app/(app)/csm/dashboard/page.tsx: rewritten from a "use client" mock page into a server
component. Resolves the tenant (getTenant) + signed-in CSM (resolveAppUserForTenant,
role === "csm"), reads ?scope=all (→ tenant, default mine), fetches the envelope, and renders
the real header (signed-in CSM name) + the PortfolioPanel. Returns CsmDashboardEmpty when there is
no tenant in session or the user is not a CSM.apps/web/components/dashboard/csm/portfolio-panel.tsx: new server component — the real accounts
table (account / region / status / project id) plus the "My accounts" vs "all" scope toggle (next/link
to /csm and /csm?scope=all). Renders an inline empty state when the scoped set is empty.apps/web/components/dashboard/csm/csm-mock-sections.tsx: the previous client page body (action strip,
activation rollup table, go-live blockers, communication) extracted verbatim into a client island so
its shared useState (filters / resolved / selected) keeps working unchanged. The hard-coded
identity is gone from the page; a local MOCK_CSM constant still drives the mock activation-table
filter until the csm-activation-rollup feature wires it to real data.apps/web/components/dashboard/csm/empty-state.tsx: new CsmDashboardEmpty for the no-tenant /
non-CSM guard case.csm owner (user, role csm); indexed/queryable per tenant —
lead.csm + { csm: 1, status: 1 }./csm (and /csm/dashboard) is server-rendered and scoped to the signed-in CSM; the header shows
the real signed-in CSM, not Sarah Chen.csmPortfolioService returns a single typed envelope (header + accounts[] with region) from
@sustentus/services/server, tenant-scoped, mine vs tenant.apps/web/lib/mock/csm.ts held no header fields to remove —
the hard-coded MY_CSM lived in the page, so the equivalent removal happened there. The mock data
arrays still feed the deferred sections and are untouched.searchParam (?scope=all) re-fetch, server-side — it links to the canonical
/csm path (not /csm/dashboard), so toggling from the alias route normalises the URL to /csm.MOCK_CSM = "Sarah Chen" in csm-mock-sections.tsx is intentionally retained — it only filters mock
data and disappears when csm-activation-rollup / csm-blocker-queue replace those sections.lead.csm is seeded / back-office this round (no UI) per the spec; the dashboard only
reads it.03_release/output/changelog.mdCSMs can now sign in to a go live dashboard that is scoped to them: it shows the real accounts they own (with region and status) and a toggle between "My accounts" and the wider team's active set, replacing the shared placeholder data.
03_release/output/investor-update.mdWho it's for: Customer success managers (CSM) What shipped: The CSM go live dashboard is now scoped to the signed-in CSM and lists their real owned accounts, with a toggle between their portfolio and the whole team's. Why it matters: First real, per-CSM account ownership — groundwork for Build the Bridge, Q2 2026 Objective 3 (validate technical infrastructure & payout flow).
Accounts can now be owned by a CSM and queried per tenant, the foundation every CSM rollup builds on.
Dig deeper: <merged-PR URL> · <changelog entry URL>
03_release/output/release.mdlead.csm field and the internal csmPortfolioService are not described at field/service granularity in apps/docs/app/technical/**; no app, route, env var, or architecture change.feature-role-matrix/leads permissions are unchanged (CSMs already view/own leads); CSM-to-account assignment is back-office with no UI this round, so the source-of-truth pages don't lag.apps/help/app/changelog/2026-06-22-csm-portfolio-foundation/) + investor draft in this PRresolvePortfolio (apps/web/app/(app)/csm/dashboard/page.tsx) — a rejection inside getPortfolioForCsm escaped the try/catch and would throw out of the component instead of degrading to the empty state. Fixed on branch (return await).fullName/initialsFor helpers in csm-portfolio/index.ts — accepted: matches the existing per-service pattern (customer-project, sdm, expert-workbench); not worth a new shared abstraction in a foundation slice.header.initials not yet read by the page — accepted: it's the spec'd envelope shape (header: name + initials) for downstream rollup features to consume, not dead code./csm (not /csm/dashboard) — accepted: both routes render the same server component; the URL normalises to the canonical path.csm owner; indexed/queryable per tenant — lead.csm + { csm: 1, status: 1 }./csm server-rendered + scoped to the signed-in CSM; header shows the real CSM.csmPortfolioService returns the typed envelope from @sustentus/services/server, tenant-scoped mine/tenant.