vendor-customer-healthrun.md00_intake/stub.mdThe customer-health section (customer-health-section.tsx)
shows CSAT by revenue (avg CSAT, revenue-weighted CSAT, revenue below threshold, revenue awaiting
CSAT), a service-outcome health rate, the shared GRR, and a forward-looking churn prediction
(revenue at risk next period, accounts at risk, confidence) — all from the mock. There is no engine that
weights CSAT by revenue or projects next-period churn. It needs the revenue map (feature 2) and the risk
signals (feature 6), plus the csat model.
csat model over the window — avg CSAT, revenue-weighted CSAT
(weighted by feature 2's per-customer revenue), revenue below a CSAT threshold, and revenue
awaiting CSAT (delivered, no CSAT yet); plus change vs prior.touches: packages/services/src/server (health + heuristic-prediction rollup on the envelope),
reads packages/services/src/db/models/csat.ts + the feature-6 risk output,
apps/web/components/dashboard/vendor/customer-health-section.tsx.01_define/output/spec.mdloadSlaBreachCustomers into getCustomerHealth's at-risk signal set + the service-health "within SLA" criterion), apps/web/app/(app)/vendor/csat/page.tsx + apps/web/app/(app)/vendor/health/page.tsx (new detail routes) + supporting presentational components under apps/web/components/dashboard/vendor/, reads apps/web/app/(app)/vendor/dashboard/page.tsx (the existing window resolution it reuses)The vendor dashboard's customer-health section (customer-health-section.tsx) renders CSAT by revenue, a service-outcome health rate, the shared GRR, and a forward-looking heuristic churn prediction. The original run (PR #531) shipped this section wired to real data — but it landed before its two upstream siblings, so it carried two documented interim reads. Both are now resolvable:
vendor-revenue-retention) has landed and is already consumed — the dashboard page
computes getRevenueRetention(...) and passes the real grr into the section, and per-customer revenue
already flows through the shared loadCustomerWindow predicate feature 2 defines. Nothing remains to
wire here.vendor-revenue-risk) has now landed too. It exposes a loadSlaBreachCustomers helper
whose own docstring states feature 7 "consumes the same helper once it folds slaBreach into its signal
set." Until now that signal was stubbed, so the churn at-risk set was only {lowCSAT, ageing>30d} (a
fully-tripped account maxed at 2/3) and the service-health bar's stated "within SLA" criterion was not
actually enforced.Separately, the section's two cards link to /vendor/csat and /vendor/health ("View CSAT
detail" / "View breakdown"), but those routes don't exist — clicking them 404s.
This revision closes both gaps. It still advances Build the Bridge / Q2 2026 Objective 1 — Establish Product-Market Fit with Vendor Partners: completing the risk model and giving the section real drill-down destinations makes the vendor cockpit a trustworthy retention tool rather than one with a stubbed signal and dead links.
slaBreach into getCustomerHealth (consume, do not fork). Call the existing
private loadSlaBreachCustomers helper (computing the summed pre-delivery SLA target the same way
getTimeToValue does, via slaConfigService.getSlaDefinitions) and add it as the third risk signal, so:{lowCSAT, ageing>30d, slaBreach} model and the /3
signal-weighting reaches the intended 3/3 for a fully-tripped account;CSAT ≥ 4.0 and within SLA and not ageing/at-risk;/vendor/csat — expands the CSAT-by-revenue rollup: avg CSAT (+ change vs prior window),
revenue-weighted CSAT, revenue below the 4.0/5 cutoff, revenue awaiting CSAT — shown larger, each with
a short definition/methodology line./vendor/health — expands the service-health rollup: the health rate (+ change), the three
service-outcome criteria explained, and the churn-prediction figures (accounts at risk, revenue at risk
next period, confidence band with the documented rule).getCustomerHealth(tenantId, window) over the same VendorPeriodWindow the
dashboard resolves from its date-range selector, stay tenant-scoped, and degrade gracefully on an
empty/sparse window (no NaN, no crash). This is an aggregate breakdown only — no per-customer
tables (see Out of scope).csat model and per-customer revenue over the selected window.accountsAtRisk, revenueAtRiskNextPeriod
(signal-weighted /3 sum), confidence (High/Medium/Low per the coverage rule).< 4.0 on the /5 scale — feature 6's constant, read here.loadSlaBreachCustomers over the
summed pre-delivery sla-stage-targets budget — both consumed, not re-derived.slaBreach (via loadSlaBreachCustomers), giving
the full {lowCSAT, ageing>30d, slaBreach} model; the /3 signal weighting reaches 3/3 for an
account tripping all three, and the "slaBreach pending" code caveats are gone.CSAT ≥ 4.0
and within SLA and not ageing/at-risk — and still shows change vs the prior window./vendor/csat renders an aggregate CSAT breakdown (avg CSAT + change, revenue-weighted CSAT, revenue
below 4.0/5, revenue awaiting CSAT) with a definition line per metric; the card's "View CSAT detail"
link resolves (no 404)./vendor/health renders an aggregate service-health breakdown (health rate + change, the three
criteria explained, and the churn-prediction figures with the confidence rule); the card's "View
breakdown" link resolves (no 404).VendorPeriodWindow), and degrade gracefully on an empty/sparse window (no NaN, no crash).slaBreach is consumed via
feature 6's helper and GRR/per-customer revenue via feature 2; neither model is forked.sla-stage-targets;
this feature reads them.apps/dashboards (the dummy source-of-truth).main, so the interim reads this run
removes are fully resolvable; nothing here is left for Build to decide.02_build/output/notes.mdslaBreach, do not fork)packages/services/src/db/services/vendor/index.ts — getCustomerHealth:slaConfigService.getSlaDefinitions(tenantId) alongside the existing reads and derives
slaTargetDays (summed PRE_DELIVERY_SLA_STAGES targets) the same way getTimeToValue /
getRevenueRisk do.loadSlaBreachCustomers helper for both the current and prior
windows (prior window ends at currentStart - 1ms so a boundary delivery isn't double-counted).{lowCSAT, ageing>30d, slaBreach} model: slaBreach is the
third signal and the customer universe is current ∪ ageing ∪ slaBreach, so an account flagged only
by an SLA-breaching delivery (no in-window revenue) still counts toward accountsAtRisk (contributing
0 revenue). The /3 weighting now reaches 3/3 for a fully-tripped account.CSAT ≥ 4.0 and within SLA
and not ageing — for both the current and prior windows (so the change figure is like-for-like).apps/web/components/dashboard/vendor/customer-health-section.tsx — added an optional range prop and
append ?range=<key> to the two card links (/vendor/csat, /vendor/health) so the detail pages open
scoped to the dashboard's selected window.apps/web/app/(app)/vendor/dashboard/vendor-dashboard-view.tsx — pass range={range} into the section.apps/web/app/(app)/vendor/csat/page.tsx (new) — aggregate CSAT breakdown: average CSAT (+ change vs
prior), revenue-weighted CSAT, revenue below the 4.0/5 cutoff, revenue awaiting CSAT — each with a
one-line definition. Reads getCustomerHealth(tenantId, window) over the resolved VendorPeriodWindow.apps/web/app/(app)/vendor/health/page.tsx (new) — aggregate service-health breakdown: the health rate
(+ change), the three service-outcome criteria explained, and the churn-prediction figures (accounts at
risk, revenue at risk next period, confidence band with the documented rule).getCustomerHealth rollup, stay tenant-scoped, and
resolve the window from ?range= via resolveVendorDateRange exactly as the dashboard page does..catch returning a zero-state
VendorCustomerHealth, and getCustomerHealth already guards every divisor (percent / weighted means),
so an empty or sparse window renders zeros, never NaN or a crash.slaBreach via loadSlaBreachCustomers; full
{lowCSAT, ageing>30d, slaBreach} model, /3 weighting reaches 3/3, "pending" caveats removed./vendor/csat renders the aggregate CSAT breakdown with a definition line per metric; the card's
"View CSAT detail" link resolves (no 404)./vendor/health renders the aggregate service-health breakdown (health rate + change, the three
criteria, churn figures with the confidence rule); the card's "View breakdown" link resolves.VendorPeriodWindow, forwarded via ?range=), and degrade gracefully on an empty/sparse window.vendorService, resolveVendorDateRange,
VendorCustomerHealth, fmt, Trend, getTenantOrNull) and the service change reuses the existing
loadSlaBreachCustomers / slaConfigService / PRE_DELIVERY_SLA_STAGES already used by sibling methods.getTimeToValue / getRevenueRisk (three call sites now share the
pattern; a future extract could DRY it, out of scope here).EMPTY_CUSTOMER_HEALTH zero-state, matching the dashboard
page's existing local pattern.03_release/output/changelog.mdLive entry: apps/help/app/changelog/2026-06-25-vendor-customer-health/page.mdx
Drill into customer health, and see SLA breaches in your churn risk
The customer health cards on your vendor dashboard now go deeper, and your churn risk is more complete:
03_release/output/investor-update.mdWho it's for: Vendor partners What shipped: Two new detail pages behind the dashboard's customer-health cards, plus churn risk that now counts missed SLAs alongside low CSAT and stalled projects. Why it matters: A more trustworthy retention cockpit with no dead links — advancing Build the Bridge, Q2 2026 Objective 1: establish product-market fit with vendor partners.
Dig deeper: <merged-PR URL> · <changelog entry URL>
03_release/output/release.mdgetCustomerHealth signature is unchanged)apps/docs/app/business/feature-role-matrix/vendors/page.mdx — added a row for the new
vendor customer-health detail views (CSAT & service-quality breakdown)apps/help/app/changelog/2026-06-25-vendor-customer-health/)/code-review medium (complexity: standard) — 8 finder angles → verify.
currentStart - 1ms) correctly avoids double-counting vs. the
inclusive loadDeliveredSpans range; the {lowCSAT, ageing, slaBreach} signal set and per-window
health-bar exclusion are correct; all new-page imports resolve; getCustomerHealth signature
unchanged and all three callers compile.EMPTY_CUSTOMER_HEALTH zero-state was triplicated across the two
new detail pages and the dashboard page. Resolved by exporting a single emptyVendorCustomerHealth
from @sustentus/services (next to the VendorCustomerHealth type) and importing it in all three
pages.slaBreach via loadSlaBreachCustomers; full
{lowCSAT, ageing>30d, slaBreach} model, /3 weighting reaches 3/3, caveats removed — verified
by reading the rewritten getCustomerHealth against the sibling SLA-target derivation./vendor/csat renders the aggregate CSAT breakdown with a definition line per metric; link
resolves./vendor/health renders the aggregate service-health breakdown + churn figures with the
confidence rule; link resolves.?range= forwarded), and degrade gracefully on
an empty window (zero-state + guarded divisors → no NaN).