Skip to Content

← All archived runs

Run: sdm-area-foundation

run.md

Run: sdm-area-foundation

  • branch: claude/pipeline-sdm-foundation-dywbx5
  • pr: #516

00_intake/stub.md

Stub: SDM dashboard — area foundation + scoping

  • feature-slug: sdm-area-foundation
  • epic: sdm-dashboard-data
  • personas: SDM
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: sdm-supply-taxonomy
  • sequence: 2 of 8

Problem

The SDM "Go live readiness" dashboard (apps/web/app/(app)/sdm/dashboard/page.tsx) is a "use client" page reading entirely from apps/web/lib/mock/sdm.ts, with a hard-coded sdmName: "Marcus Chen". Two things block any section from being real: (1) the page is not scoped to the signed-in SDM, and (2) there is no notion of an SDM's area — an SDM is meant to own a slice of supply (regions and/or platforms), but no assignment exists, so "my area" vs "all" has nothing to resolve against. Every section needs the same area-scoped set of leads + experts to aggregate over.

Proposed change

  • Add an SDM area assignment — the regions and/or platforms (from sdm-supply-taxonomy) a user with role sdm owns — so the dashboard can scope to "my area". Index it for per-SDM queries.
  • Convert /sdm (and /sdm/dashboard) to a server-fetched page scoped to the signed-in SDM, with a "my area" vs "all" toggle resolving to a real query (area-scoped leads/experts vs the wider tenant set) instead of the static mock.
  • Wire the header (the real signed-in SDM identity) and stand up the shared read-service envelope in @sustentus/services/server that the demand, supply, risk, expert-strength and analytics sections all read from (one area query → the SDM's in-scope leads + experts).

Acceptance criteria (rough)

  • A user with role sdm can be assigned an area (regions/platforms); the assignment is queryable/indexed per tenant.
  • /sdm is server-rendered and scoped to the signed-in SDM; "my area" shows only in-scope leads/experts, the toggle widens to the tenant set.
  • The header shows the real signed-in SDM, not the hard-coded "Marcus Chen".
  • A single area read-service returns the SDM's in-scope leads + experts for the other features to aggregate over — no section still imports lib/mock/sdm.

Out of scope (this feature)

  • The demand/supply/risk/strength/analytics sections (features 3–8) — this only stands up the scoping + area assignment the rest read.
  • A UI to assign areas to SDMs (assignment may be seeded/back-office this round); the dashboard reads the assignment, it does not manage it.

Notes for Define

  • Decide assignment semantics: is the area set explicitly per SDM, or derived from region/platform territory? At minimum the assignment + index must exist; seeding can be manual.
  • Confirm the "all" side of the toggle is tenant-scoped (an SDM seeing the whole tenant's supply/demand) — the mock shows multiple platforms/regions, implying a tenant-wide read behind the toggle.
  • touches: packages/services/src/db/models/user.ts (SDM area fields on the sdm discriminator, or a small assignment model), packages/services/src/server (area read-service), apps/web/app/(app)/sdm/{page.tsx,dashboard/page.tsx} (server-fetch + scope).

01_define/output/spec.md

Spec: SDM dashboard — area foundation + scoping

  • slug: sdm-area-foundation
  • personas: SDM
  • touches: packages/services/src/db/models/user.ts, packages/services/src/db/services/sdm-area/, packages/services/src/db/services/index.ts, apps/web/app/(app)/sdm/page.tsx, apps/web/app/(app)/sdm/dashboard/page.tsx, apps/web/lib/mock/sdm.ts
  • complexity: standard

Problem

The web SDM "Go live readiness" dashboard (apps/web/app/(app)/sdm/dashboard/page.tsx) is a "use client" page that reads entirely from apps/web/lib/mock/sdm.ts, with a hard-coded sdmName: "Marcus Chen". Two things block any section from becoming real: (1) the page is not scoped to the signed-in SDM, and (2) there is no notion of an SDM's area — an SDM is meant to own a slice of supply (regions and/or technology platforms), but no assignment exists, so the dashboard's "my area" vs "all" framing has nothing to resolve against. Every section (demand, supply, risk, expert strength, analytics) needs the same area-scoped set of leads + experts to aggregate over. This is sequence 2 of 8 in the sdm-dashboard-data epic — it depends on sdm-supply-taxonomy (sequence 1, which establishes the platform→module key) and is the prerequisite for the other six sections. It advances Build the Bridge / Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow by proving the SDM dashboard can resolve real, tenant- and area-scoped supply/demand data end to end.

Proposed change

  • Add an SDM area assignment to the sdm user discriminator. The SDMSchema discriminator is currently empty (new Schema<ISDMUser>({})), so it is the natural, lean home for the area (mirroring how csm-portfolio-foundation extended an existing model rather than adding a new one). Add an area sub-document holding the two dimensions an SDM owns:

    • regions: string[] — region keys matching lead.location.region (e.g. UK, DACH, APAC, Nordics).
    • platforms: ObjectId[] — references to the technology-platform taxonomy from sdm-supply-taxonomy (ref: "taxonomy"), e.g. SAP / Oracle / Salesforce.

    Index it for per-SDM, tenant-scoped lookup (the SDM is resolved by tenantId + the signed-in user, so the index follows the established multi-tenant pattern). Assignment is seeded / back-office this round — the dashboard reads the area, it does not manage it.

  • Define area membership as a clear, testable rule. A lead or expert is in the SDM's area when it matches every dimension the area specifies: if area.regions is non-empty, its region must be in that set; if area.platforms is non-empty, its resolved platform must be in that set. A dimension left empty is unconstrained. (Platform resolution for leads and experts is the sdm-supply-taxonomy feature's job; this feature consumes that key. If the platform dimension is unset, the region dimension alone scopes the area.)

  • Convert /sdm and /sdm/dashboard to a server component that resolves the Clerk-authenticated SDM → their tenant (getTenant) → the SDM app user (resolveAppUserForTenant, role === "sdm"), reusing the same helpers the CSM and customer dashboards already use. The page header renders the real signed-in SDM identity (name + role line), not the hard-coded Marcus Chen.

  • Stand up the shared area read-service sdmAreaService in @sustentus/services/server (new db/services/sdm-area/ following the established index.ts class + instance.ts singleton pattern, exported through db/services/index.ts, mirroring csm-portfolio/). It returns one typed area envelope — the single fetch entry every other section in the epic extends rather than re-querying:

    • header: the SDM's display name + initials.
    • area: the resolved assignment (regions, platforms) so sections can label "my area".
    • leads[]: the in-scope leads (the demand side) the other sections aggregate over.
    • experts[]: the in-scope experts (the supply side) the other sections aggregate over.
    • It takes a scope: "mine" applies the area membership rule above; "tenant" returns the whole tenant's leads + experts. Both are always tenant-isolated — an SDM can never see another tenant's supply/demand.
  • Wire the "my area" vs "all" toggle to the real query. The toggle drives the scope the service resolves (mine vs tenant) so the rendered in-scope set is real and the toggle has an observable effect (my area → only the SDM's regions/platforms; all → the tenant-wide set). The "all" side is tenant-scoped, consistent with the mock showing multiple platforms/regions.

  • Retire the mock incrementally. Foundation wires the header (real SDM identity) and stands up the area envelope / scope that the rest read from, and removes sdmName from apps/web/lib/mock/sdm.ts. The demand, bid-readiness, risk, expert-strength, market-signals and service-performance section tables stay on the mock this round — each is wired to the area envelope by its own feature (3–8). Render a loading state while the area resolves and a defined empty state ("no area assigned yet") when the SDM owns no regions or platforms — no crash, no mock fallback for the header.

Acceptance criteria

  • A user with role sdm can be assigned an area (regions and/or platforms); the assignment is stored on the sdm discriminator, indexed, and queryable per tenant.
  • /sdm (and /sdm/dashboard) is server-rendered and scoped to the signed-in SDM; the header shows the real signed-in SDM, not the hard-coded Marcus Chen.
  • sdmAreaService returns a single typed area envelope (header + area + in-scope leads[] + experts[]) from @sustentus/services/server, scoped by tenant: scope: "mine" applies the area membership rule (region in area.regions and platform in area.platforms, each constraining only when set), scope: "tenant" widens to the tenant's full set — never another tenant's data.
  • The "my area" vs "all" toggle resolves to the real query: "my area" returns only the SDM's in-scope leads/experts and the toggle widens to the tenant set, with an observable difference in the set (not a client-side filter on the static mock).
  • An SDM with no assigned area sees the defined empty state, not a crash and not the mock data.
  • sdmName is removed from apps/web/lib/mock/sdm.ts; the still-mocked section tables (demand, bid readiness, risks, expert strength, market signals, service performance) continue to render unchanged pending their own features.

Out of scope

  • The demand / supply / risk / expert-strength / analytics section data (features 3–8) — this only stands up the scoping + area assignment the rest read; those section tables stay mock-driven this round.
  • The platform→module taxonomy itself and the tagging of leads/experts onto it — that is the sdm-supply-taxonomy feature (sequence 1, the dependency); this feature consumes the resolved platform key, it does not define or seed the taxonomy.
  • A UI to assign areas to SDMs — assignment is seeded / back-office this round; the dashboard reads the area, it does not manage it.
  • Backfilling / migrating areas onto existing SDM users beyond demo-tenant seeding.
  • Realtime / websocket changes — the page is server-fetched on load.
  • Cross-SDM / team-lead views beyond the "my area" vs "all" toggle.

Open questions

  • none

02_build/output/notes.md

Build notes: sdm-area-foundation

  • commits: feat: sdm-area-foundation — SDM area assignment + scoped read-service + server dashboard

What changed

  • packages/services/src/db/models/user.ts: added an area sub-document (regions: string[], platforms: ObjectId[] ref product) to the previously-empty sdm discriminator, plus two { tenantId, "area.*" } indexes so SDMs are resolvable by the supply they own. ISDMArea typed and exported.
  • packages/services/src/db/services/sdm-area/{index.ts,instance.ts}: new sdmAreaService (getAreaForSdm(tenantId, sdmUserId, scope)) returning the shared SdmAreaEnvelope (header + area + in-scope leads[] + experts[]). scope: "mine" applies the area membership rule — region in area.regions and platform in area.platforms, each constraining only when that dimension is set; scope: "tenant" widens to the tenant. Both are always tenant-scoped. Mirrors the csm-portfolio service shape.
  • packages/services/src/db/services/index.ts: export the instance + the six public types so they reach @sustentus/services/server.
  • apps/web/app/(app)/sdm/dashboard/page.tsx: converted from a "use client" mock page to an async server component — resolves the Clerk-authed SDM → tenant → app user (role === "sdm"), reads the scope from searchParams, fetches the envelope, and renders the real signed-in SDM in the header. /sdm/page.tsx re-exports it unchanged, so both routes are server-rendered.
  • apps/web/components/dashboard/sdm/area-panel.tsx: new client-free panel with the "my area" vs "all supply" toggle (links /sdm/sdm?scope=all) and the in-scope demand/expert tables, with an inline empty state.
  • apps/web/components/dashboard/sdm/empty-state.tsx: SdmDashboardEmpty for the unavailable case (no tenant / not an SDM).
  • apps/web/lib/mock/sdm.ts: removed sdmName; the six section tables still read the mock.

Acceptance criteria status

  • SDM area assignable (regions/platforms), stored on the sdm discriminator, indexed, queryable per tenant — area + two { tenantId, "area.*" } indexes on SDMSchema.
  • /sdm (and /sdm/dashboard) server-rendered and scoped to the signed-in SDM; header shows the real SDM, not Marcus Chen.
  • sdmAreaService returns the typed envelope from @sustentus/services/server, scoped by tenant: mine applies the region+platform rule, tenant widens — never another tenant's data.
  • The "my area" vs "all" toggle resolves to the real query (server re-fetch on ?scope=all), with an observable difference in the in-scope set.
  • An SDM with no assigned area sees the inline "no area assigned yet" empty state (not a crash, not mock data) while mine; the toggle still widens to the tenant set.
  • sdmName removed from apps/web/lib/mock/sdm.ts; the six section tables render unchanged.

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

  • Platform dimension depends on sdm-supply-taxonomy (sequence 1, not yet merged). That feature introduces the two-level platform→module taxonomy and the lead/expert platform tagging. Until it lands there is no platform parent, so area.platforms references the existing product taxonomy (the modules: MM, Fusion, CRM…) and the platform filter matches lead.product / expert.products. The membership rule degrades exactly as the spec intends: with no platforms assigned, the region dimension alone scopes the area. When the taxonomy feature lands, the platform key re-points without touching the envelope contract.
  • Area assignment is seeded / back-office this round (no UI), per spec.

03_release/output/changelog.md


title: "See the slice of supply you own on the SDM dashboard" date: 2026-06-23T08:00:00Z personas: [sdm] slug: sdm-area-foundation pr: https://github.com/sustentus/sustentus/pull/516

See the slice of supply you own on the SDM dashboard

Your go live readiness dashboard now knows who you are and which part of supply is yours:

  • The dashboard greets you by name and is scoped to you when you sign in — no more shared placeholder.
  • See the live leads and experts that fall inside your area — the regions and platforms you own.
  • Switch between "My area" and "All supply" to move between your own slice and the whole tenant's.
  • If no area is assigned to you yet, you get a clear empty state instead of stand-in data.

03_release/output/investor-update.md

SDMs now see the real slice of supply they own

Who it's for: Service delivery managers (SDM) What shipped: The SDM go live readiness dashboard is now scoped to the signed-in SDM and shows the real leads and experts inside their area, with a toggle between their slice and the whole tenant. Why it matters: First real, per-SDM area ownership — groundwork for Build the Bridge, Q2 2026 Objective 3 (validate technical infrastructure & payout flow).

An SDM's area can now be owned and queried per tenant, the foundation every SDM supply and demand view builds on.

Dig deeper: https://github.com/sustentus/sustentus/pull/516 · https://help.sustentus.com/changelog/2026-06-23-sdm-area-foundation

03_release/output/release.md

Release: sdm-area-foundation

  • pr: #516 · merged: yes — squashed onto main as 086dd00 on 2026-06-23
  • CI: green — Typecheck, Lint, Format, Migrate preview DB, Audit DB all passed on the final run (after a GitHub Actions runner-provisioning outage that aborted two earlier runs with no logs; re-run on the recovered infra was clean).
  • technical docs: no technical docs impact — the SDM area field and the internal sdmAreaService are not described at field/service granularity in apps/docs/app/technical/**; no new app, route, env var, or architecture change (mirrors the csm-portfolio-foundation sibling).
  • business docs: no business docs impact — the SDM "go live readiness" dashboard already exists in feature-role-matrix/service-journey; this wires real, area-scoped data behind it without changing documented persona capabilities. Area assignment is back-office (no UI) this round, so the source-of-truth pages don't lag.
  • release notes: both — changelog entry (apps/help/app/changelog/2026-06-23-sdm-area-foundation/) + investor draft in this PR
  • deploy: READY — production web (web.sustentus.com/platform.sustentus.com) and help (help.sustentus.com) both green on the merge commit 086dd00.
  • sent: investor update sent to 2 recipients on 2026-06-23, after the green deploy.

Review summary

  • Empty-area tenant leak (fixed on branch). getAreaForSdm with scope: "mine" and an SDM who has no assigned area collapsed both filters to { tenantId }, returning the whole tenant's leads/experts under "my area" — violating spec AC #5 (no-area → empty state) and contradicting the panel's "No area assigned" subtitle. Unlike the CSM sibling (whose mine is always pinned to { csm }), the SDM area has no identity pin, so an empty area must short-circuit. Added an ownsNothing = scope === "mine" && !hasArea guard that returns empty leads/experts.
  • findById not tenant-scoped (hardened on branch). Changed the SDM lookup to findOne({ _id: sdmUserId, tenantId }) so the header/area can never resolve from a foreign-tenant doc — defense in depth (the single caller already resolves the id under the tenant).
  • Inactive scope-tab hover was a no-op (fixed on branch). hover:text-muted-foreground duplicated the base colour; changed to hover:text-foreground for real hover affordance.
  • Redundant nested ternary (simplified on branch). lead.title || (lead.id ? lead.id : "—")lead.title || lead.id || "—".
  • Accepted: duplicated fullName/initialsFor helpers (matches the per-service pattern across csm-portfolio/sdm/expert-workbench); the 4-resolve/3-destructure Promise.all (registers the product model for populate(), explained by a comment).

Acceptance check (vs spec)

  • SDM can be assigned an area (regions/platforms), stored on the sdm discriminator, indexed, queryable per tenant — area sub-doc + { tenantId, "area.regions" } / { tenantId, "area.platforms" } indexes.
  • /sdm (and /sdm/dashboard) server-rendered + scoped to the signed-in SDM; header shows the real SDM, not Marcus Chen.
  • sdmAreaService returns the typed envelope (header + area + leads[] + experts[]) from @sustentus/services/server, tenant-scoped mine/tenant.
  • "My area" vs "all" toggle resolves to a real query (searchParam → service scope), observable difference.
  • SDM with no assigned area sees the defined empty state — now enforced by the ownsNothing guard (no tenant-wide leak).
  • sdmName removed from apps/web/lib/mock/sdm.ts; still-mocked section tables render unchanged.