csm-blocker-queuerun.md00_intake/stub.mdThe GoLiveBlockersTable and the ActionStripSection are the heart of the dashboard and are entirely
dummy (apps/web/lib/mock/csm.ts blockers, stageSLA). Each row
is one blocking issue across a CSM's portfolio with: account, stage (BRD | Delivery | Awaiting
CSAT), status (red | amber), blocker text, who (Customer | Expert | Vendor | Partner), owner,
impact, timeStuckDays, escalationCount, value (revenue at risk), daysToValue, and a resolve
action. The page sorts by SLA breach → closest-to-breach → highest value, and the action strip derives
actor/escalation/oldest counts from the set. None of the underlying data is portfolio-queryable today;
the blocker, escalation and SLA concepts are owned by the sibling epics.
@sustentus/services/server: all open blockers across the
signed-in CSM's owned accounts (reusing the foundation's portfolio read), each carrying who, owner,
impact, escalationCount (from the blocker/escalation model) and stage + timeStuckDays (from the
SLA timing layer).value (revenue at risk — the account's contract value) and daysToValue
(the account's daysToGoLive), reusing the customer epic's finance + health derivations.timeStuckDays − stageSLA[stage], breached first, then
closest-to-breach, then highest value) server-side or from the real stageSLA targets.value (revenue at risk) and daysToValue are real, reused from the finance + health derivations
— not re-modelled here.sla-stage-targets, not the mock stageSLA map.customer-dashboard-data/project-blockers, expert-dashboard-data/work-blockers-escalations and
expert-dashboard-data/sla-stage-targets; this feature reads them.csm-outreach-thread (feature 5).project-blockers +
work-blockers-escalations (blocker + who + escalation) and sla-stage-targets (per-stage targets +
time-in-stage) have landed; read them rather than forking. The mock's who includes Partner —
ensure the extended actor enum covers it.value reuses customer-dashboard-data/customer-finance-summary (totalContract over quotes/
invoices); daysToValue reuses project-health-engine daysToGoLive.touches: packages/services/src/server (portfolio blocker query + SLA sort + resolve),
apps/web/components/dashboard/csm/{go-live-blockers-table,blocker-table-row,action-strip-section}.tsx.01_define/output/spec.mdThe CSM "Go live control" dashboard's two headline sections — the go-live blockers table and the
action strip — are entirely dummy, reading from apps/web/lib/mock/csm.ts (blockers,
stageSLA). The CSM is the platform's exception manager: per the roles
doc they "handle exceptions, not the standard
flow," pushing the 5–10% of stuck accounts through to go-live. Without a real, portfolio-wide blocker
queue sorted by SLA urgency, a CSM can't see which accounts across their book are breaching and worth
chasing first. This advances Build the Bridge → Q2 2026 Objective 3 (Validate technical
infrastructure & payout flow) (okrs/2026-Q2):
the delivery → go-live stretch only works if the human exception path runs on real data.
Every upstream this feature reads has now landed, so it composes them rather than re-building:
csm-portfolio-foundation (PR #512) — lead.csm ownership + csmPortfolioService.getPortfolioForCsm(tenantId, csmUserId, scope)
returning a CsmPortfolioEnvelope (header + accounts[], each { id, account, region, status, customerId }),
with the "My accounts" (mine) vs "all" (tenant) scope toggle. This is the read the queue hangs off.blocker.ts model + BlockerService (raise/escalate/resolve/listOpenByLeads) — PR #482 / #464,
carrying label, owner, impact, status, escalationCount, escalated, lastEscalatedAt.sla-definition.ts + SlaConfigService + deriveLeadSlaTimingForLeads() (per-stage targets +
time-in-stage / overdue) — PR #463.customer-finance-summary (totalContract aggregate over quotes/invoices, per customer) — PR #502.Replace the mock in the blockers table and action strip with a real portfolio blocker read for the signed-in CSM, composed from the landed services. Functionally:
@sustentus/services/server: reuse csmPortfolioService's CSM-scoped
account selection (same scope the page already resolves — mine vs tenant), then
BlockerService.listOpenByLeads() over that account set to get every open blocker. Each row carries
the blocker's label, owner/actor, impact, escalationCount, plus stage + timeStuckDays
from deriveLeadSlaTimingForLeads(), and the account name/region from the portfolio envelope.lead (_id), but CsmPortfolioAccount does not
surface the lead id today. Extend the foundation's envelope with a leadId per account (a small,
coordinated addition to csmPortfolioService — reusing its existing scoped lead query, not
forking it) so blockers join cleanly to portfolio accounts.stageSLA map: breached first
(timeStuckDays − targetDays > 0), then closest-to-breach, then highest value.BlockerService.resolve(), which closes the shared
blocker record (so the customer/expert views stay consistent) and drops the row off the queue; the
action-strip counts recompute.owner enum to include Vendor (coordinated with the
blocker-model feature) so the action strip's blocked-by-Vendor count is real; the per-row displayed
actor maps from owner.value (revenue at risk) is derived from the landed
customer-finance-summary totalContract, keyed by each account's customerId from the portfolio
envelope; daysToValue degrades to "—" until project-health-engine lands. No fabricated numbers —
a row renders without these when the source isn't there.csmPortfolioService.getPortfolioForCsm → BlockerService.listOpenByLeads), each row showing
actor/owner, impact, stage and time-stuck — no data from lib/mock/csm.ts.mine shows the CSM's owned accounts'
blockers, tenant shows the whole tenant's.sla-definition targets
(deriveLeadSlaTimingForLeads): breached first, then closest-to-breach, then highest value — not
the mock stageSLA map.BlockerService.resolve(), closes the shared record, removes the row
from the queue, and recomputes the action-strip counts.owner enum includes Vendor, and the blocked-by-Vendor action-strip count reflects
real Vendor-owned blockers.value is derived from the real finance aggregate per account's customerId, and daysToValue
shows "—" when project-health-engine is absent — neither is mocked or fabricated.owner += Vendor; leadId on the portfolio account
envelope) — everything else is read.project-health-engine or a portfolio finance aggregate — daysToValue degrades and
value is derived per-account from the landed customer finance summary.csm-outreach-thread (feature 5).csm-activation-rollup /
csm-portfolio-snapshot.main (the foundation merged in
#512) so csmPortfolioService and the blocker/SLA/finance services are present to build against.02_build/output/notes.md@sustentus/services/server)db/models/blocker.ts: extended the BlockerOwner enum + BLOCKER_OWNERS with Vendor, so the
action strip's blocked-by-Vendor count reflects real vendor-owned blockers (the one coordinated model
touch the spec allows).db/services/csm-portfolio/index.ts: added leadId to CsmPortfolioAccount (sourced from the
lead's _id in the existing scoped query — no new query), so portfolio-dependent features can join
per-lead records to accounts. The foundation's envelope, extended not forked.db/services/csm-blocker-queue/ (new): csmBlockerQueueService.getBlockerQueueForCsm(tenantId, csmUserId, scope) composes the landed services — reuses csmPortfolioService for the scoped account
set (mine/tenant), blockerService.listOpenByLeads for open blockers, deriveLeadSlaTimingForLeads
for stage + time-in-stage + per-stage target, and a best-effort accepted-quote total (proposalService
→ quoteService) for value. Returns { rows, stageSLA }. daysToValue is null (no
project-health-engine) and value is null when no finance source — both render "—", never fabricated.CsmBlockerQueue, CsmBlockerQueueRow) from the services barrel.apps/web)app/(app)/csm/dashboard/page.tsx: fetches the blocker queue alongside the portfolio (one resolver,
Promise.all) and renders the new CsmSections with real rows + stageSLA + the resolve action.app/(app)/csm/dashboard/actions.ts (new): resolveBlockerAction server action — tenant- and
CSM-scoped blockerService.resolve, returns { ok } so the queue can drop the row.components/dashboard/csm/csm-sections.tsx (new, replaces csm-mock-sections.tsx): the action strip
and go-live blockers table run on the real queue (client-side sort by SLA breach → closest → highest
value, action-strip tallies, optimistic resolve-then-drop). The activation rollup and communication
thread stay on mock — they belong to csm-activation-rollup / csm-outreach-thread.components/dashboard/csm/blocker-table-row.tsx: BlockerRow now carries region (secondary label),
stage: string, nullable daysToValue/value (render "—"), and a wired Resolve button.components/dashboard/csm/go-live-blockers-table.tsx: the SLA footer is built from the real stageSLA
targets, not the hardcoded mock map.csmPortfolioService →
blockerService.listOpenByLeads), each row showing actor/owner, impact, stage and time-stuck — no
lib/mock/csm.ts data in the table.mine vs tenant flows from the page's ?scope= param into the
portfolio read, so the queried account set (and its blockers) changes.sla-definition targets (deriveLeadSlaTimingForLeads):
breached → closest-to-breach → highest value.BlockerService.resolve() (via the server action), closes the shared record, drops
the row and recomputes the action-strip tallies.owner enum includes Vendor; blocked-by-Vendor counts real vendor-owned blockers.value from the real accepted-quote total per blocked lead; daysToValue shows "—" (no
project-health-engine) — neither fabricated.CsmSections); the activation table and communication stay mock in the same component, in the
original order (action strip → activation → blockers → communication). The mock activation table's
CSM filter no longer cross-filters the real blocker rows (it only filtered mock identities).value cost: revenue-at-risk is resolved once per blocked lead (not per blocker, not per
account) via the accepted quote — best-effort per the spec. If a portfolio finance read lands later,
swap valueForLead for it.slaStage (Request/BRD/Bidding/Delivery/Billing/CSAT), falling
back to the status display name when a status doesn't map to an SLA stage; such rows simply never flag
a breach (no configured target).03_release/output/changelog.mdPublished at apps/help/app/changelog/2026-06-23-csm-blocker-queue/page.mdx.
You can now see every open go-live blocker across your accounts in a single queue, sorted so the closest SLA breach and the most revenue at risk sit at the top. Each row shows who is blocking, how long it has been stuck, and the stage it is held up in — and you can mark a blocker resolved the moment it clears, which drops it from the queue and updates the at-a-glance counts of who is blocking what.
03_release/output/investor-update.mdWho it's for: Customer success managers What shipped: A live queue of every open go-live blocker across a CSM's accounts, sorted by SLA breach and revenue at risk, with one-click resolve. Why it matters: Faster unblocking keeps lead-to-payout cycle time down — our Build the Bridge goal to validate the delivery infrastructure.
Each row's urgency and revenue-at-risk are computed from live SLA and finance data, not estimates.
Dig deeper: https://github.com/sustentus/sustentus/pull/511 · https://help.sustentus.com/changelog/2026-06-23-csm-blocker-queue
03_release/output/release.mdba7bdc3 onto main (2026-06-23)Vendor to the
BlockerOwner enum broke lead-blockers' hand-narrowed BlockerItem.owner; fixed by typing it from
the canonical BlockerOwner. One earlier web preview failure was a Turbopack module-eval error in the
unrelated /api/brd/chat route (cache flake); re-triggered via the main merge.csmPortfolioService, blockerService, SLA timing, finance); the new csmBlockerQueueService, the
owner += Vendor enum extension and the leadId/csm envelope fields are internal and not described
at service/field granularity in apps/docs/app/technical/**. No app, route, env var, or architecture change.feature-role-matrix/projects already grants the CSM
"View project" / "Resolve blocker"; this feature surfaces those on the dashboard against real data, it
adds no new persona capability or permission.apps/help/app/changelog/2026-06-23-csm-blocker-queue/) +
investor draft in this PR.web (platform.sustentus.com) and help (help.sustentus.com) both green on ba7bdc3.csm-activation-rollup + project-health-engine merged to main and touched the same CSM dashboard
files. Resolved by merging both real wirings into one CsmSections component: the activation rollup
table (real, from getActivationRollup), the action strip + go-live blockers table (real, this feature)
and the still-mock communication thread. Threaded the owning CSM onto the portfolio account + blocker
row so the activation table's CSM filter narrows the real blocker queue (tenant scope).
/code-review (high effort) over the feature surface — 3 findings:
stageSLA
entry fell back to sla = 0, so any timeStuckDays > 0 flagged a false SLA breach (corrupting the
breach-first sort) and isApproaching (>= sla − 1 → >= −1) was always true. Added guarded,
shared overdueDays / slaRemaining helpers (exported from blocker-table-row, reused in the
csm-sections sort): a missing target is never overdue and sorts after targeted stages.Your team / Partner owners (accepted — to spec). The strip surfaces exactly
the three figures the spec names (blocked-by-Customer/-Expert/-Vendor); the component has three
external-actor columns by design. count is all open blockers; the three are an intentional subset.value ??/+ precedence is correct; bandToStatus null handling; lead-id
map keying via .toString(); the CsmBlockerQueueRow ↔ BlockerRow structural contract; the
barrel ordering fix; filterCsm ↔ row.csm both via fullName.csmPortfolioService →
blockerService.listOpenByLeads), no mock data.mine/tenant scope toggle.sla-definition targets.owner enum includes Vendor; blocked-by-Vendor counts real vendor-owned blockers.value from the real finance aggregate; daysToValue shows "—" (no project-health-engine) —
neither fabricated. (project-health-engine has since merged to main; daysToValue can be wired in a
follow-up — this run ships the degrade as specced.)