Skip to Content

← All archived runs

Run: workspace-shell

run.md

Run: workspace-shell

  • branch: claude/pipeline-workspace-shell-b2lkj9
  • pr: #567

00_intake/stub.md

Stub: Workspace shell — the brand-new route and design spine

  • feature-slug: workspace-shell
  • epic: lead-workspace
  • personas: Customer, Expert, CSM, SDM, Admin, Vendor
  • initiative: Refine the Bridge / objective: Validate Technical Infrastructure & Payout Flow (Q2 2026 · Objective 3)
  • depends-on: none
  • sequence: 1 of 6

Problem

There is no single page that answers "where is this lead and what can I do next?" — the lead is scattered across service-leads/[id], projects/[id], expert/bids/[id] and several satellite pages, each re-rendering the same header/journey/activity. Before any data or actions can be unified, the platform needs the shell: one new page whose layout is stable and persona-aware. This first feature lands the design (ported from apps/demo/workspace) so the team can see and agree the surface early.

Proposed change

Add a brand-new, additive route in apps/web (e.g. app/(app)/workspace/[id]) — leaving service-leads, projects and every existing detail page untouched and functional. Port the design spine from apps/demo/workspace as real apps/web components (server-by-default, @sustentus/ui primitives):

  • Workspace header — request ref, service, customer, a per-persona status chip, and a next-best-action CTA slot (empty for now).
  • Journey strip — the lead workflow spine (workflows.json order), with the lead's current stage highlighted. Read the real status via the workflow engine accessors; not the demo's click-to-preview.
  • Body + levers rail — the two-column layout: a section column rendering section-card frames via a section registry, and a levers-rail frame (empty placeholder region — no levers yet).
  • Persona resolved from auth (apps/web/lib/auth.ts), never from the URL.

Sections render as scaffolding (titles + empty/placeholder states); real data is stub 2, actions are stubs 4–6. The page should render for a single real lead id at its true status.

Acceptance criteria (rough)

  • A new route renders at …/workspace/[id] for a valid lead id; existing routes are unchanged and still work.
  • The header, journey strip, section column and levers-rail frame all render, matching the demo's layout/structure.
  • The journey strip highlights the lead's real current stage, resolved through the workflow engine (not hardcoded).
  • Persona comes from auth; an unknown/invalid lead id renders not-found.
  • No mutations and no real satellite data yet — sections show frames/empty states; the levers rail is an empty placeholder.

Out of scope (this feature)

  • Loading/displaying real satellite data (BRD, proposals, quote, milestones, invoices, activity) — stub 2.
  • Per-persona section lists, jargon-free labels, capability gating — stub 3.
  • Any lever, CTA wiring, or mutation — stubs 4–6.
  • Touching or redirecting service-leads/[id], projects/[id], or any existing page.

Notes for Define

  • Design source: apps/demo/components/workspace/* + apps/demo/lib/workspace/{journey,views}.ts. Port the structure/look; do not import from apps/demo.
  • Resolve status/labels only through the @sustentus/services workflow accessors (packages/services/src/db/workflows/) — never a local status map (avoids the status-name drift the brief warns of).
  • touches: apps/web/app/(app)/workspace/[id]/**, apps/web/components/workspace/**, @sustentus/ui.
  • Finalise the route segment and whether the page is gated by the existing route-policies.ts.

01_define/output/spec.md

Spec: Workspace shell — the lead-workspace route and design spine

  • slug: workspace-shell
  • personas: Customer, Expert, CSM, SDM, Admin, Vendor
  • touches: apps/web/app/(app)/workspace/[id], apps/web/components/workspace, @sustentus/ui, @sustentus/services (workflow accessors)
  • complexity: standard

Problem

A lead has no single home. Today it is scattered across service-leads/[id], projects/[id], expert/bids/[id] and several satellite pages, each re-rendering its own header, journey and activity — so no one page answers "where is this lead and what can I do next?". This is the first feature of the lead-workspace epic (Refine the Bridge / Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow: leads completing end-to-end and lead-to-payout cycle time). Before any data or actions can be unified onto one page, the platform needs the shell: one stable, persona-aware layout. This feature lands the design — ported from apps/demo/workspace as real apps/web components — read-only, so the team can see and agree the surface early before data (stub 2) and action levers (stubs 4–6) are wired on.

Proposed change

Add a brand-new, additive route at apps/web/app/(app)/workspace/[id] — leaving service-leads/[id], projects/[id], expert/bids/[id] and every other detail page untouched and fully functional. Port the design spine from apps/demo/workspace as real, server-by-default apps/web components built on @sustentus/ui primitives (structure/look ported; nothing imported from apps/demo):

  • Workspace header — request ref, service, customer, a per-persona status chip, and an (empty for now) next-best-action CTA slot. The status chip label comes from the workflow engine's per-persona label (resolveStatusLabel(status, persona)), never a local map.
  • Journey strip — the lead workflow spine rendered from @sustentus/services (getStatusesByWorkflow("lead"), declared order), with the lead's real current stage highlighted via getStatus(lead.status). Read-only — not the demo's client-side click-to-preview.
  • Body + levers rail — the two-column layout: a section column rendering section-card frames via a section registry, and a levers-rail frame — an empty placeholder region (no levers yet).
  • Persona resolved from auth (apps/web/lib/auth.ts getRole()), never from the URL.
  • Minimal lead read — the page reads one real lead by id (leadService.findById) for the header identity fields and status only; satellite data is stub 2. An unknown/invalid id renders notFound().
  • Entry points — the existing service-leads/[id] and projects/[id] detail pages each gain a top-right "Go to workspace" link to /workspace/<id> (both pages already load the same lead by the same id). This is the only change to those pages.

The page renders for a single real lead id at its true status. Sections render as scaffolding (titles + empty/placeholder states); the levers rail and the header CTA slot are empty placeholders.

Decisions settled this Define: route segment is workspace/[id] (matches the demo source and the epic breakdown); access is open to all six personas via a route-policies.ts entry for /workspace. (The apps/web proxy middleware is deny-by-default — an unlisted (app) route redirects to the role home — so a policy entry is required for the route to be reachable at all; persona is still resolved from auth in the page via getRole().)

Acceptance criteria

  • A new route renders at /workspace/<id> for a valid lead id; expert/bids/[id] is unchanged, and service-leads/[id] / projects/[id] keep working with only a "Go to workspace" link added.
  • All six personas can open /workspace/<id> (the route is allowed for every role in route-policies.ts).
  • The service-leads/[id] and projects/[id] detail pages each show a top-right "Go to workspace" link to /workspace/<id>.
  • The header, journey strip, section column and levers-rail frame all render, matching the demo's layout/structure, as server components built on @sustentus/ui primitives.
  • The journey strip renders the lead spine in workflows.json order via getStatusesByWorkflow("lead") and highlights the lead's real current stage resolved through getStatus(lead.status) — not a hardcoded or locally-duplicated status map.
  • Persona is resolved from auth (getRole()); the header status chip uses resolveStatusLabel(status, persona).
  • An unknown or invalid lead id renders not-found (notFound()).
  • No mutations and no satellite data: sections show titles + empty/placeholder states; the levers rail and the header next-best-action CTA slot are empty placeholders.
  • No status names or transition rules are duplicated locally — all status/label resolution goes through the @sustentus/services workflow accessors.

Out of scope

  • Loading/displaying real satellite data — BRD, proposals, quote, milestones, invoices, activity (stub 2, lead-data-display).
  • Per-persona section lists, broader jargon-free relabeling, and capability gating (stub 3, persona-capability-matrix).
  • Any lever, next-best-action logic, CTA wiring, or mutation (stubs 4–6).
  • Consolidating or redirecting service-leads/[id], projects/[id], expert/bids/[id], proposals/[id], the finance pages or the BRD page. The only change to a detail page is the "Go to workspace" link added to service-leads/[id] and projects/[id].
  • Refactoring any existing inline role === … checks or pages; importing anything from apps/demo.

Open questions

  • none — the route segment (workspace/[id]) and the gating approach (page-level auth only) were the two items the stub deferred to Define, and both are now decided above.

Context budget

Per the epic breakdown's recorded note, this Define read apps/demo/components/workspace/** and apps/demo/lib/workspace/** as the design source (code, not docs) — beyond the docs-only intake band but necessary because the design to port is code, not prose.

02_build/output/notes.md

Build notes: workspace-shell

  • commits: feat: workspace-shell — lead workspace route + design spine

What changed

  • apps/web/app/(app)/workspace/[id]/page.tsx: new additive route. Resolves persona via getRole() and tenant via getTenantOrNull() (redirect to /sign-in if missing), loads the lead with leadService.findById, and renders notFound() for a missing or invalid id (CastError / BSONError caught → not-found). Server component.
  • apps/web/components/workspace/workspace-view.tsx: composes the shell — header, journey strip, section column and levers-rail frame — in the demo's two-column layout. Derives the status name from lead.status and the per-persona chip label via statusService.resolveLabel(name, persona).
  • apps/web/components/workspace/workspace-header.tsx: request ref (#<requestId>), service, customer and the per-persona status chip, plus the next-best-action CTA slot — an empty placeholder.
  • apps/web/components/workspace/journey-strip.tsx: renders the lead spine from statusService.getDisplayOrder("lead") (= getStatusesByWorkflow("lead")) in workflows.json order, highlighting the lead's real current status. The spine is built by walking positive transitions from the first status; off-spine statuses (e.g. qualified_out) render as a drop-off chip. No local status map; read-only (nothing clickable).
  • apps/web/components/workspace/section-registry.ts + section-frame.tsx: the section list and the card frame each renders — title + empty placeholder. Per-persona section lists and real data are later features, so the shell shows the full set as scaffolding.
  • apps/web/components/workspace/levers-rail.tsx: an empty placeholder rail (no levers yet).
  • apps/web/lib/route-policies.ts: added a /workspace policy allowing all six roles. The apps/web proxy middleware (proxy.ts) is deny-by-default — an unlisted (app) route redirects to the role home — so this entry is what makes the route reachable for every persona. (The earlier "page-level auth only" note was a misread of the middleware and is corrected here.)
  • apps/web/components/workspace/workspace-link-button.tsx: a shared "Go to workspace" link button.
  • apps/web/app/(app)/service-leads/[id]/page.tsx and apps/web/app/(app)/projects/[id]/page.tsx: wrapped the existing grid in a flex column and added a top-right "Go to workspace" link. Both pages already load the same lead by the same id, so the link target is /workspace/<id>; no other behaviour on those pages changed.

Acceptance criteria status

  • A new route renders at /workspace/<id> for a valid lead id; expert/bids/[id] is unchanged, and service-leads/[id] / projects/[id] keep working with only a "Go to workspace" link added.
  • All six personas can open /workspace/<id>route-policies.ts allows every role (the proxy middleware is deny-by-default, so the entry is required).
  • The service-leads/[id] and projects/[id] detail pages each show a top-right "Go to workspace" link to /workspace/<id>.
  • The header, journey strip, section column and levers-rail frame all render, matching the demo's layout/structure, as server components built on @sustentus/ui primitives.
  • The journey strip renders the lead spine in workflows.json order via getStatusesByWorkflow("lead") and highlights the lead's real current stage — not a hardcoded or locally-duplicated status map.
  • Persona is resolved from auth (getRole()); the header status chip uses resolveStatusLabel(status, persona) (via statusService.resolveLabel).
  • An unknown or invalid lead id renders not-found (notFound()).
  • No mutations and no satellite data: sections show titles + empty/placeholder states; the levers rail and the header next-best-action CTA slot are empty placeholders.
  • No status names or transition rules are duplicated locally — all status/label resolution goes through the @sustentus/services workflow accessors (statusService).

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. No check is known to fail.

Notes for review

  • The journey spine is derived from the engine's positive-transition chain rather than a hardcoded stage list, so it stays correct if workflows.json order changes and keeps qualified_out (and any future drop-off) off the linear spine without naming it.
  • Status name is read from the populated lead.status (a StatusView with .name); the per-persona chip label comes only from statusService.resolveLabel.
  • Header identity uses requestId, service.name (falling back to title), and a customer display-name helper (company name → full name → email).

03_release/output/investor-update.md

The lead workspace is taking shape — its design shell is now live

Who it's for: All six roles — customer, expert, CSM, SDM, admin, vendor. What shipped: A new per-lead workspace page, reachable from the lead and project views, that shows a lead's real journey stage on one persona-aware screen. Why it matters: It's the design foundation for bringing a lead's whole journey onto a single view — groundwork toward a shorter lead-to-payout cycle (Refine the Bridge · Q2 Objective 3).

Dig deeper: <merged-PR URL> · <no changelog — early-preview shell>

03_release/output/release.md

Release: workspace-shell

  • pr: #567 (https://github.com/sustentus/sustentus/pull/567) · merged: no — Ready to merge gate is unticked; STOPPED before merge
  • CI: green — Quality Project (lint · format · typecheck) success, Migrate preview database success, Migrate production database skipped (expected on PR), Vercel Preview Comments success
  • technical docs: no technical docs impact — the change adds a route + components within apps/web; no new app, package, route convention, env var, build/CI step, or architecture documented in apps/docs/technical changed (technical docs don't enumerate web routes or route-policies)
  • business docs: no business docs impact — this is a read-only design shell (placeholder sections, no real data or actions), so no documented product capability changed; the unified lead-workspace capability is documented when data + actions land in later lead-workspace features
  • release notes: investor-only — early-preview shell adds a navigable surface but no new user capability, so no end-user changelog (a "you can now…" entry would overstate placeholders); investor note framed as foundation/velocity in 03_release/output/investor-update.md
  • deploy: pending — gated behind the merge (step 7 runs after the squash-merge)
  • sent: pending — investor email is gated behind merge + green production deploy (step 8); audience cut is investor-only

Review summary

/code-review medium (standard complexity), two finder angles + manual verification on the git diff 771d676..HEAD apps/web scope:

  • Correctness — no actionable findings. Verified the two real risk areas: the journey-strip spine walk is loop-guarded (!onSpine.has(cursor) + if (!status) break) and degrades gracefully when the lead's status is off-spine/unknown (no highlight, no crash); the service-leads/[id] and projects/[id] JSX restructure keeps div nesting balanced (confirmed by the green Vercel build) with every prior child in its original column. Null-safety on lead.status?.name and the customer/service fallbacks is sound.
  • Conventions — no real violations. The finder's "Title Case" flags on Requirements (BRD) / Proposals were false positives (those are sentence case — first word capitalised, BRD an acronym); the customerName/loadLead helper notes were "no rule broken" and loadLead deliberately mirrors the existing loadLeadForDetail pattern in the service-leads page.
  • Cleanup — none required; the diff is additive (new route + components) plus a minimal, behaviour-preserving wrapper on the two detail pages.

Acceptance check (vs spec)

  • New route renders at /workspace/<id> for a valid lead id; existing detail pages keep working — additive route; only a link added to service-leads/projects.
  • All six personas can open /workspace/<id>route-policies.ts /workspace policy lists every role (middleware is deny-by-default).
  • service-leads/[id] and projects/[id] show a top-right "Go to workspace" link — shared WorkspaceLinkButton.
  • Header, journey strip, section column, levers-rail frame render as server components on @sustentus/ui.
  • Journey strip uses getStatusesByWorkflow("lead") order and highlights the lead's real current stage — no local status map.
  • Persona from getRole(); status chip via statusService.resolveLabel(status, persona).
  • Unknown/invalid id renders notFound().
  • No mutations / no satellite data — placeholder sections; empty levers rail and header CTA slot.
  • No status names or transitions duplicated locally — all via @sustentus/services accessors.

Notes

  • Page padding fix (p-6 on the workspace container) landed in this PR so the shell isn't flush to the screen edges.
  • Audience-cut judgement (investor-only, no end-user changelog) is the agent's call per the Release contract; raise it with the user if they'd prefer a changelog or no investor email at all, since both are gated behind the unticked merge box.