Skip to Content

← All archived runs

Run: project-blockers

run.md

Run: project-blockers

  • issue: #481 # canonical home of the spec + state (labels, comments)
  • branch: feat/481-project-blockers
  • pr: #482

00_intake/stub.md

Stub: Customer dashboard — project blockers

  • feature-slug: project-blockers
  • epic: customer-dashboard-data
  • personas: Customer (view); delivery team (Expert, CSM, SDM) raise/clear blockers
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: customer-project-foundation
  • sequence: 3 of 9

Problem

The blockers half of the Risk/Blockers section is dummy (label, owner, impact). There is no blockers model anywhere — nothing records what is currently impeding a project, who owns the resolution, or its impact. The health engine also needs open blockers as an input to compute risk and go-live confidence, so this data has to exist before health can be real.

Proposed change

  • Define a blocker record attached to a project (lead): label, owner (Customer | Your team | Expert), impact (free text, e.g. "Delays go live by 3 days"), and an open/resolved state.
  • Build the minimal capture for the delivery team (Expert/CSM/SDM) to raise and clear a blocker on a project.
  • Wire the blockers list in the RiskBlockersFeedback section to the real open blockers for the customer's active project.

Acceptance criteria (rough)

  • Delivery team can raise a blocker on a project with owner + impact; it appears on the customer dashboard.
  • Resolving a blocker removes it from the customer's open-blockers list.
  • Open blockers are queryable by project so project-health-engine can consume them.
  • No blockers → the section renders a clean "no blockers" state, not a mock entry.

Out of scope (this feature)

  • The risk badge / risk level itself — that's computed in project-health-engine (this feature only supplies the raw blockers it reads).
  • A full blocker-management console for the delivery team beyond minimal raise/clear.

Notes for Define

  • Decide model vs. embedded sub-doc on the lead; the health engine queries "open blockers for project" frequently, so index accordingly.
  • touches: packages/services/src/db/models (new blocker model or lead sub-doc), packages/services/src/server, apps/web/components/dashboard/customer/risk-blockers-feedback.tsx.

01_define/output/spec.md

Spec: Project blockers — model, capture, and queryable source

  • slug: project-blockers
  • issue: #481
  • personas: Customer, Expert, CSM, SDM
  • touches: packages/services/src/db/models, packages/services/src/db/services, packages/services/src/server, apps/web/app/(app)/projects/[id]
  • complexity: standard

Problem

The Risk/Blockers section on the customer dashboard renders dummy blocker rows (label, owner, impact) and there is no blockers model anywhere in the platform — nothing records what is impeding a project, who owns the resolution, or its impact. This blocks two things: the customer cannot be shown the real state of their delivery, and the upcoming project-health-engine has no real signal to compute risk and go-live confidence from. This advances Build the Bridge → Q2 2026 Objective 3 (Validate technical infrastructure & payout flow) by turning a mock surface into real, queryable delivery data that the health engine can consume.

Proposed change

  • Blocker model. A new tenant-scoped Blocker collection (separate, indexed — not an embedded sub-doc — because open blockers are queried per project frequently and written by multiple personas). Each record references a project (leadId) and carries label, owner (Customer | Your team | Expert), impact (free text, e.g. "Delays go live by 3 days"), and a status of open | resolved (with resolved metadata). Indexed on { tenantId, leadId, status } so "open blockers for this project" is a covered, cheap query.
  • Blocker service. A blockerService in packages/services/src/db/services/blocker/, exported via @sustentus/services/server, with: raise(leadId, { label, owner, impact }), resolve(blockerId), and listOpenByLead(leadId). listOpenByLead is the read project-health-engine will consume.
  • Delivery-team capture. A minimal raise/clear UI on the project detail page (apps/web/app/(app)/projects/[id]), scoped to that leadId and available to the delivery team (Expert, CSM, SDM): list the project's open blockers, raise a new one (label + owner + impact), and mark one resolved. Backed by "use server" actions following the existing (app)/.../actions.ts pattern (zod-validated, resolveActionContext / runActionBody).

The customer dashboard's blockers list stays on mock data this run — wiring it to real open blockers depends on customer-project-foundation (sequence 1, not yet built), which establishes the signed-in customer's active-project read on that page. This feature supplies the real, queryable data; the foundation feature consumes it for the customer view.

Acceptance criteria

  • A Blocker model exists, tenant-scoped, attached to a project (leadId), with label, owner (Customer | Your team | Expert), impact, and status (open | resolved), indexed on { tenantId, leadId, status }.
  • blockerService exposes raise, resolve, and listOpenByLead, exported from @sustentus/services/server.
  • On the project detail page, a delivery-team member (Expert/CSM/SDM) can raise a blocker on the project with owner + impact, and it appears in that project's open-blockers list.
  • Resolving a blocker removes it from listOpenByLead for that project (its status becomes resolved).
  • Open blockers are queryable by project via listOpenByLead(leadId) so project-health-engine can consume them.
  • When a project has no open blockers, the capture surface renders a clean "no blockers" state, not a mock entry.

Out of scope

  • The risk badge / risk level itself — computed in project-health-engine; this feature only supplies the raw blockers it reads.
  • Wiring the customer dashboard blockers list to real data — deferred to customer-project-foundation (which provides the customer's active-project read); the customer-facing list stays on mock this run.
  • A full blocker-management console for the delivery team beyond minimal raise/clear (filtering, history, assignment, comments, notifications).

Open questions

  • none

02_build/output/notes.md

Build notes: project-blockers

  • branch: feat/481-project-blockers
  • commits:
    • feat: project-blockers — Blocker model + blockerService
    • feat: project-blockers — delivery-team capture on project detail page

What changed

  • packages/services/src/db/models/blocker.ts (new): tenant-scoped Blocker model referencing a project (lead), with label, owner (Customer | Your team | Expert), impact, status (open | resolved) plus resolvedAt / resolvedBy / createdBy. Uses the standard schemaPlugin / softDeletePlugin / tenantPlugin. Indexed on { tenantId, lead, status } — the hot "open blockers for this project" read.
  • packages/services/src/db/services/blocker/{index,instance}.ts (new): BlockerService with raise(tenantId, leadId, {label, owner, impact, createdBy}), resolve(tenantId, blockerId, resolvedBy) (open → resolved), and listOpenByLead(tenantId, leadId) (open only, newest first). Exported as blockerService (+ BlockerService, RaiseBlockerInput) from @sustentus/services/server via the db barrels.
  • apps/web/app/(app)/projects/[id]/actions.ts (new): raiseBlockerAction / resolveBlockerAction"use server", zod-validated, gated to delivery roles (admin/csm/sdm/expert) via resolveActionContext, wrapped in runActionBody.
  • apps/web/components/service-leads/lead-blockers/ (new): lead-blockers.tsx (server: fetches
    • serialises open blockers), blockers-panel.tsx (client: list, raise form, resolve buttons, clean "no blockers" empty state), loading.tsx (skeleton).
  • apps/web/app/(app)/projects/[id]/page.tsx: renders <LeadBlockers> in the right column, only for delivery roles (admin/csm/sdm/expert).

Acceptance criteria status

  • Blocker model exists, tenant-scoped, attached to lead, with label/owner/impact/status, indexed { tenantId, lead, status }models/blocker.ts.
  • blockerService exposes raise, resolve, listOpenByLead, exported from @sustentus/services/server.
  • On the project detail page a delivery-team member can raise a blocker (owner + impact) and it appears in that project's open list — blockers-panel + raiseBlockerAction, router.refresh().
  • Resolving a blocker drops it from listOpenByLead (status → resolved) — resolveBlockerAction.
  • Open blockers are queryable by project via listOpenByLead(leadId) for project-health-engine.
  • No open blockers → clean "no blockers — project on track" state, not a mock entry.

Verify result

  • mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. Ship reads them back from gh pr checks. No check is expected to fail.

Notes for review

  • Owner field is the named role label (Customer | Your team | Expert) exactly as the spec and the existing customer-dashboard mock express it — it is a free label, not a user reference.
  • Admin included alongside Expert/CSM/SDM in the capture's allowed roles (visibility + write) for oversight; the spec named the delivery team (Expert/CSM/SDM) and admin is the platform superuser. If you'd rather lock it to exactly the three, say so and I'll drop admin from DELIVERY_ROLES.
  • Customer dashboard stays on mock this run (per the approved spec) — the customer-facing read is deferred to customer-project-foundation. This feature only supplies the queryable data + capture.

03_ship/output/changelog.md

Changelog: project-blockers

Persona: expert, csm, sdm

Track what's blocking a project

Delivery teams can now record and clear blockers on a project, so everyone can see what's holding up go-live:

  • Raise a blocker from the project page with its owner — the customer, your team, or the expert — and a short note on its impact, such as "delays go live by 3 days".
  • Mark a blocker resolved once it's cleared and it drops off the project's open list.
  • A project with nothing outstanding shows a clean "no blockers" state, so an empty list means the project really is clear.

03_ship/output/investor-update.md

Delivery teams can now track what's blocking each project

We've given the platform its first real record of what is impeding active projects. Delivery teams — experts, customer success, and service delivery managers — can now raise a blocker on any project, naming who owns it and the impact on go-live, and clear it once resolved. Until now this was placeholder content with nothing behind it; it is now real, per-project data the team works from.

  • Experts, CSMs, and SDMs can raise and resolve blockers directly on the project they're delivering, each with an owner and a plain statement of impact.
  • Open blockers are now queryable per project — the foundation the upcoming project health scoring reads to flag at-risk go-lives before they slip.
  • The data is structured and tenant-scoped from day one, so it feeds delivery health and reporting later without new infrastructure.

This advances the Build the Bridge initiative and our Q2 2026 objective to validate technical infrastructure & payout flow — turning project delivery risk into real, trackable data that protects the lead-to-payout cycle.

03_ship/output/pr.md

Ship: project-blockers

  • PR: #482
  • branch: feat/481-project-blockers
  • CI: format/lint/typecheck <not reporting as checks on this PR> · preview build <pass — web + all app previews deploying green; web Deployed>
  • technical docs: no technical docs impact (the technical/packages/services page is structural and does not enumerate individual models/services)
  • business docs: apps/docs/app/business/feature-role-matrix/projects/page.mdx — added "Raise blocker on project" and "Resolve blocker" rows
  • release notes: both — investor draft (03_ship/output/investor-update.md) + changelog entry published in apps/help/app/changelog/page.mdx

Review summary

  • Expert "own only" not enforced server-side (judgement call, low severity). The page only shows the Blockers panel to an expert for projects they can access, and the feature-role matrix says expert = "own only". But raiseBlockerAction / resolveBlockerAction gate on role only — any expert/CSM/SDM/admin in the tenant could raise/resolve a blocker on any project in that tenant by calling the action directly. Blast radius is within-tenant and the actors are trusted internal delivery staff, so it's not a data-leak; it's an authZ/UX-consistency gap. Logged as a follow-up rather than expanded here (spec scoped this to "minimal raise/clear"). resolve would need a blocker→lead lookup to enforce expert assignment.
  • The rest of the diff traced clean across correctness (conditions, null/await, soft-delete + tenant scoping on the new queries), cross-file (barrel exports, @sustentus/services/server consumption, action-import path matches the brd/[id]/actions precedent), and reuse (model/service/action all follow the csat/quote patterns). No other findings.

Acceptance check (vs spec)

  • Blocker model — tenant-scoped, attached to lead, label/owner/impact/status, indexed { tenantId, lead, status } (packages/services/src/db/models/blocker.ts).
  • blockerService exposes raise / resolve / listOpenByLead, exported from @sustentus/services/server.
  • Delivery-team member can raise a blocker (owner + impact) on the project detail page and it appears in the open list.
  • Resolving a blocker drops it from listOpenByLead (status → resolved).
  • Open blockers queryable by project via listOpenByLead(leadId) for project-health-engine.
  • No open blockers → clean "no blockers" state, not a mock entry.

Merge & deploy

  • merged: no — awaiting explicit human merge approval (gate:merge-approved).
  • deploy: pending merge.