Skip to Content

← All archived runs

Run: customer-project-foundation

run.md

Run: customer-project-foundation

  • issue: #475 # canonical home of the spec + state (labels, comments)
  • branch: claude/clever-thompson-1c82lv
  • pr: #476

00_intake/stub.md

Stub: Customer dashboard — server-fetched project foundation

  • feature-slug: customer-project-foundation
  • epic: customer-dashboard-data
  • personas: Customer
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: none
  • sequence: 1 of 9

Problem

The web customer dashboard is a "use client" page that imports a single mock object from apps/web/lib/mock/customer.ts. There is no server-side resolution of "the signed-in customer's active project", so nothing on the page can be real. A "project" is a lead with isActiveProject = true; the dashboard header (customerName, company, activeProject.id/title/startDate) and the team strip (team[]) are already backed by the lead + user models and only need wiring once that foundation exists.

Proposed change

  • Convert /customer/dashboard to a server component that resolves the Clerk-authenticated customer → their tenant → their active project (lead.isActiveProject = true, scoped by lead.customer + tenant).
  • Build the read service in @sustentus/services/server that returns the project envelope every other feature extends: header (customer name + companyName), project identity (id = requestId, title, startDate), and team derived from lead.expert + lead.manager (name, role, initials, type expert|team).
  • Wire the dashboard header and the team part of the ProjectStatusStrip to this real data, replacing those fields of the mock. Add loading + empty (no active project) states.
  • Decide the project title source (lead has no explicit title field today — derive from product.name / category, or add a title field) and the progress % placeholder until milestone-delivery-tracking computes it.

Acceptance criteria (rough)

  • A signed-in customer sees their own active project's header + team from the database, not mock.
  • A customer with no active project sees a defined empty state, not a crash or mock project.
  • Data is tenant- and customer-scoped — a customer cannot see another customer's project.
  • The read service exposes a typed envelope the other eight features extend (single fetch entry).

Out of scope (this feature)

  • Milestones, health, finance, activity, blockers, actions, change control, messaging — each is its own feature that extends this envelope.
  • Multi-project switching / project picker.

Notes for Define

  • Settle the project title and progress % questions here — both ripple into every section.
  • This is the foundation: name the read service + envelope type so sections 2–9 extend it rather than re-querying the lead.
  • touches: apps/web/app/(app)/customer/dashboard/page.tsx (client→server), apps/web/lib/mock/customer.ts (retire incrementally), packages/services/src/db/models/lead.ts, packages/services/src/server (new read service), apps/web/components/dashboard/customer/project-status-strip.tsx.

01_define/output/spec.md

Spec: Customer dashboard — server-fetched project foundation

  • slug: customer-project-foundation
  • issue: #475
  • personas: Customer
  • touches: apps/web/app/(app)/customer/dashboard/page.tsx, apps/web/components/dashboard/customer/project-status-strip.tsx, apps/web/lib/mock/customer.ts, packages/services/src/db/models/lead.ts, packages/services/src/db/services/customer-project/, packages/services/src/db/services/index.ts
  • complexity: standard

Problem

The web customer dashboard at /customer/dashboard is a "use client" page that renders a single mock object from apps/web/lib/mock/customer.ts — nothing a signed-in customer sees is their own data. A "project" already exists in the data model (a lead with isActiveProject = true, scoped by lead.customer + tenant), and the header and team strip are backed by the lead + user models; they just need wiring once a server-side "resolve the signed-in customer's active project" foundation exists. This is sequence 1 of 9 in the customer-dashboard-data epic and the prerequisite for the other eight sections — it advances Build the Bridge / Q2 2026 Objective 3 (Validate Technical Infrastructure & Payout Flow) by proving the customer dashboard can render real, tenant-scoped project data end to end.

Proposed change

  • Convert /customer/dashboard to a server component that resolves the Clerk-authenticated customer → their tenant (getTenant) → their active project (the lead with isActiveProject = true, scoped by lead.customer = the resolved app user + tenantId), reusing the existing getTenant / resolveAppUserForTenant helpers in apps/web/lib/.
  • Build a read service customerProjectService in @sustentus/services/server (new db/services/customer-project/ following the index.ts class + instance.ts singleton pattern, exported through db/services/index.ts). It returns one typed project envelope — the single fetch entry the other eight features extend rather than re-querying the lead:
    • header: customer display name (firstname/lastname) + companyName.
    • project: id (the lead's requestId), title, startDate, progress.
    • team[]: derived from lead.expert + lead.manager{ name, role, initials, type: "expert" | "team" }.
  • Project title — add an optional title: string field to the lead schema. The envelope resolves the title as lead.title ?? lead.product?.name ?? lead.category, so it is meaningful today (no lead has a title yet → falls back to product/category) and becomes editable later without an envelope change. No write path / editor UI for title is built this run (see Out of scope).
  • Progress % — the envelope exposes progress: number and the foundation returns a fixed 0 placeholder. The milestone-delivery-tracking feature (sequence 4) computes the real value later with no envelope-shape change.
  • Wire the header and the team strip to the envelope. The page header (project title, customer name, company, project id badge) and the team prop passed to ProjectStatusStrip read from the envelope. Those fields are removed from the mock; the remaining mock sections (go-live, actions, milestones, finance, activity, blockers, change control) stay on customer.ts until their own features land — the mock is retired incrementally.
  • States — render a loading state (Suspense fallback) while the envelope resolves, and a defined empty state ("no active project yet") when the customer has no isActiveProject lead. No crash, no mock fallback.

Acceptance criteria

  • /customer/dashboard is a server component; a signed-in customer sees their own active project's header (title, name, company, project id) and team from the database, not from customer.ts.
  • The read service is tenant- and customer-scoped: it only returns the active project whose lead.customer matches the resolved app user within the session tenant — a customer cannot see another customer's (or another tenant's) project.
  • A customer with no isActiveProject lead sees the defined empty state, not a crash and not a mock project.
  • customerProjectService returns a single typed envelope (header, project, team) exported from @sustentus/services/server; the project title resolves via lead.title ?? product.name ?? category and progress is a fixed 0 placeholder.
  • lead gains an optional title field; existing leads (no title) render the product/category fallback with no migration required.
  • The header + team fields are removed from apps/web/lib/mock/customer.ts; the remaining mock sections still render unchanged.

Out of scope

  • The other eight sections — milestones/delivery, project health, finance, activity, blockers, action items, change control, messaging — each extends this envelope in its own feature.
  • Real progress computation (owned by milestone-delivery-tracking, sequence 4).
  • A write path / editor UI for the new lead.title field (CSM/SDM editing the title) — read + fallback only this run.
  • Multi-project switching / a project picker — a customer resolves to a single active project.
  • Migrating or backfilling title onto existing leads.

Open questions

  • none

02_build/output/notes.md

Build notes: customer-project-foundation

  • branch: claude/clever-thompson-1c82lv
  • commits:
    • feat: customer-project-foundation — server-fetched project envelope + dashboard wiring

What changed

  • packages/services/src/db/models/lead.ts: added optional title field to ILeadPopulated (carries to ILead) and the schema ({ type: String, trim: true }). No migration — existing leads simply have no title and fall back.
  • packages/services/src/db/services/customer-project/: new read service.
    • index.ts: CustomerProjectService.getActiveProjectForCustomer(tenantId, customerUserId) resolves the single lead with isActiveProject = true scoped by tenantId + customer, populates manager/expert/customer/product (registering those models first so populate resolves), and maps it to the typed CustomerProjectEnvelope (header, project, team). Title resolves title ?? product.name ?? category; progress is the fixed 0 placeholder; team derives from expert (type expert)
      • manager (type team). Returns null when there is no active project.
    • instance.ts: customerProjectService singleton.
  • packages/services/src/db/services/index.ts: export the service + the CustomerProjectEnvelope / CustomerProjectTeamMember / CustomerProjectService types (reaches apps via @sustentus/services/server).
  • apps/web/app/(app)/customer/dashboard/page.tsx: converted from a "use client" page to an async server component. Resolves tenant (getTenant) → app user (resolveAppUserForTenant, customer role) → envelope. Header (title, customer name, company, project id badge) and the ProjectStatusStrip team now read from the envelope; the remaining sections stay on mock until their own features land. Renders the empty state when there is no active project.
  • apps/web/components/dashboard/customer/empty-state.tsx: new "no active project yet" empty state.
  • apps/web/app/(app)/customer/dashboard/loading.tsx: route-level Suspense loading skeleton shown while the server component resolves the envelope.
  • apps/web/lib/mock/customer.ts: removed the header/team fields now sourced from the envelope (customerName, company, team, and activeProject.{id,title,startDate}), plus the now-unused formatShort helper. activeProject keeps progress (still consumed by ProgressSection, which stays on mock this run).

Acceptance criteria status

  • /customer/dashboard is a server component; signed-in customer sees their own active project's header + team from the DB — page resolves the envelope server-side.
  • Read service is tenant- and customer-scoped — query filters on tenantId + customer = resolved app user + isActiveProject: true.
  • No active project → defined empty state (CustomerDashboardEmpty), no crash / no mock.
  • customerProjectService returns a single typed envelope exported from @sustentus/services/server; title resolves title ?? product.name ?? category, progress is fixed 0.
  • lead gains an optional title field; existing leads fall back with no migration.
  • Header + team fields removed from mock/customer.ts; remaining sections render unchanged (ProgressSection still reads mock activeProject.progress).

Verify result

  • Mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. No check is expected to fail.

Notes for review

  • The envelope is intentionally the single fetch entry for sections 2–9 — they should extend CustomerProjectEnvelope / add methods to CustomerProjectService rather than re-querying the lead.
  • project.id is the display string #<requestId> (matches the existing header badge); startDate is exposed as an ISO string for downstream sections (header doesn't render it this run). progress stays 0 until milestone-delivery-tracking (sequence 4).
  • ProgressSection still shows the mock 64% by design — wiring it to real progress is out of scope (owned by milestone-delivery-tracking).

03_ship/output/changelog.md

Changelog: customer-project-foundation

Persona: Customer

See your live project on your dashboard

Your customer dashboard now shows your real project instead of sample content:

  • The header shows your actual project — its name, your company, and the project reference — from your live project.
  • The "talk to your team" panel lists the real people on your project: your assigned expert and delivery manager.
  • If you don't have an active project yet, your dashboard now shows a clear "no active project yet" message instead of sample data.

This begins making your customer dashboard reflect your real project.

03_ship/output/investor-update.md

Customers now see their live project on their dashboard

We've connected the customer dashboard to live project data — the first of nine steps that turn the customer's home screen from a static demo into a real, secure view of their engagement. This advances Build the Bridge and our Q2 objective to Validate Technical Infrastructure & Payout Flow: it proves the customer-facing surface can render real, tenant-isolated project data end to end.

  • A signed-in customer now sees their own project — its name, their company, and their delivery team (expert and manager) — pulled live from the platform, not sample content.
  • Access is strictly scoped: a customer can only ever see their own project, never another customer's or another organisation's.
  • It's built as a reusable foundation the next eight dashboard sections extend, so the remaining customer-facing data lands faster.

03_ship/output/pr.md

Ship: customer-project-foundation

  • PR: #476 — https://github.com/sustentus/sustentus/pull/476
  • branch: claude/clever-thompson-1c82lv
  • CI: format pass · lint pass · typecheck pass · preview build pass (last build commit; re-running on ship commit)
  • technical docs: no technical docs impact — the services page documents structure, not individual db services; the new read service + optional lead.title field change nothing documented.
  • business docs: no business docs impact — feature-role-matrix/projects already grants Customer "view project (own only)"; wiring the dashboard to real data implements that documented capability rather than changing the matrix.
  • release notes: both — investor draft (03_ship/output/investor-update.md) + changelog entry published to apps/help/app/changelog/page.mdx in this PR.

Review summary

  • Fixed on branchpage.tsx resolveActiveProject returned the service promise without await, so the try/catch only covered tenant/user resolution; a rejection from the project query would escape uncaught to the error boundary instead of degrading to the empty state (inconsistent with the sibling dashboard-stats). Added return await so all resolution failures degrade gracefully.
  • Accepted (non-blocking) — project.id = #${requestId} could read #undefined only if an active-project lead lacked a requestId; requestId is allocated at lead creation and uniquely indexed, so not reachable for an active project.
  • Accepted (pre-existing) — ILeadPopulated.customer is optional in the type but required in the schema; the service already guards with ?. / fallback, and this PR doesn't change it.

Acceptance check (vs spec)

  • /customer/dashboard is a server component sourcing the customer's own active project header + team from the DB — page resolves the envelope server-side.
  • Read service is tenant- and customer-scoped — query filters tenantId + customer + isActiveProject: true.
  • No active project → defined empty state, no crash, no mock fallback.
  • customerProjectService returns a single typed envelope from @sustentus/services/server; title resolves title ?? product.name ?? category, progress fixed 0.
  • lead gains an optional title field; existing leads fall back with no migration.
  • Header + team fields removed from mock/customer.ts; remaining sections unchanged.

Merge & deploy

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