customer-project-foundationrun.md00_intake/stub.mdThe 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.
/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).@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).product.name / category, or add a title field) and the progress % placeholder until
milestone-delivery-tracking computes it.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.mdThe 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.
/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/.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" }.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: 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.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.isActiveProject lead. No crash,
no mock fallback./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.lead.customer matches the resolved app user within the session tenant — a customer cannot see
another customer's (or another tenant's) project.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.apps/web/lib/mock/customer.ts; the remaining mock
sections still render unchanged.progress computation (owned by milestone-delivery-tracking, sequence 4).lead.title field (CSM/SDM editing the title) — read + fallback
only this run.title onto existing leads.02_build/output/notes.mdfeat: customer-project-foundation — server-fetched project envelope + dashboard wiringpackages/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)./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.tenantId +
customer = resolved app user + isActiveProject: true.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.mock/customer.ts; remaining sections render
unchanged (ProgressSection still reads mock activeProject.progress).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.mdPersona: Customer
Your customer dashboard now shows your real project instead of sample content:
This begins making your customer dashboard reflect your real project.
03_ship/output/investor-update.mdWe'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.
03_ship/output/pr.mdlead.title field change nothing documented.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.03_ship/output/investor-update.md) + changelog entry published to apps/help/app/changelog/page.mdx in this PR.page.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.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.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./customer/dashboard is a server component sourcing the customer's own active project header + team from the DB — page resolves the envelope server-side.tenantId + customer + isActiveProject: true.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.mock/customer.ts; remaining sections unchanged.gate:merge-approved).