## Summary
Turns /dashboards/portfolio/[siteSlug] into a human-operated Rhodes CRM for a
site, so authorized users can read and edit the full Rhodes work hierarchy
directly — without mutating Rhodes through chat or MCP tools. The page gains a
domain-oriented tab structure (Overview, Work Plan, Documents) and a stage-aware
Work Plan workbench (hierarchy tree → selected-node detail → node-context rail).
Human edits go through the existing confirm-diff dialog and save immediately
after confirmation, except guarded changes — dueDiligence edits and
milestone completion-state changes (flipping whether a milestone is
completed) submit a pending fieldChangeRequests row (source: "ui") and only
apply after EduOps approval. Ordinary milestone date/status upkeep saves
immediately, matching the Rhodes MCP milestone writer. Agent/MCP writes keep
their separate approval-card flow. Permissions and audit logging are enforced
server-side, derived/immutable fields stay read-only, and reads are
indexed/paginated rather than relying on unbounded table scans. Implements the
contract in features/portfolio/rhodes-ui/GOAL.md.
### Screenshots
<img width="2257" height="1225" alt="Screenshot 2026-06-29 at 9 30 50 PM" src="https://github.com/user-attachments/assets/de724b46-b994-47fa-84cc-45080dd16728" />
<img width="1964" height="949" alt="Screenshot 2026-06-29 at 9 32 28 PM" src="https://github.com/user-attachments/assets/5dd80758-e8c5-4745-9830-43bde1e6f582" />
<img width="1253" height="521" alt="Screenshot 2026-06-29 at 9 32 59 PM" src="https://github.com/user-attachments/assets/ee337f7f-5763-4b63-88dc-abe5571bc61c" />
<img width="1388" height="1245" alt="Screenshot 2026-06-29 at 9 33 34 PM" src="https://github.com/user-attachments/assets/d06d06a2-497b-431a-850c-fd47daa6c0af" />
<img width="1627" height="1106" alt="Screenshot 2026-06-29 at 9 35 06 PM" src="https://github.com/user-attachments/assets/defa7053-cda7-4675-b489-2f16a492cf8a" />
### Changes
Backend
- chat/convex/rhodes/portfolioWorkbench.ts *(new)* — UI-oriented read/write
surface. getSiteWorkbench builds the stage-aware tree, selected-node
summaries, and per-tab scoped/rollup context (notes, documents + gaps), scoped
per tab and with truncation flags so the UI never depends on unbounded reads.
Typed save mutations (saveNote, deleteNote, saveDocument,
registerVerifiedDriveDocument, saveTask, deleteTask, saveWorkUnitGroup,
saveWorkUnit, saveMilestoneDates) route through the shared Rhodes write
helpers and authorization. saveMilestoneDates writes directly for ordinary
date/status upkeep and produces a pending field-change request only when the
edit changes milestone completion state.
- chat/convex/rhodes/runtime/writes/{documentWrites,taskWrites,workUnitGroupWrites,siteWrites}.ts
— tighten optional-field clear semantics, export applyMilestonePatches for
the direct-save path, and harden the Drive-identity guard so a public document
update cannot swap a verified Drive URL while keeping the old file ID.
- chat/convex/_generated/api.d.ts — generated registration for the new module.
Google Drive integration
- chat/app/(main)/api/rhodes/drive/{files,folders,register-document,upload-complete,upload-session}/route.ts
*(new)* — five Next route handlers that proxy the workbench Drive flows
(browse files/folders, register an existing Drive file as a document, and the
resumable upload session/completion handshake) to the Rhodes worker with the
current user as the delegated actor.
- chat/rhodes-worker/src/index.ts — handles the delegated portfolio-drive
requests: verifies the shared secret, impersonates the Clerk identity so Convex
runs with the user's capabilities, browses Drive, and cross-references
registered documents. Returns precise HTTP status codes per error class.
- chat/rhodes-worker/src/drive-root.ts *(new)* — assertDriveIdInsideRoot
BFS containment guard ensuring every browsed/registered Drive id resolves under
the authorized site's Drive folder, extracted from the handler for direct
testing.
Frontend
- chat/components/dashboards/portfolio/portfolio-rhodes-workbench.tsx *(new)*
— the workbench: stage-aware hierarchy tree, selected-node read/edit surface
(fields, milestone dates, WUGs, work units, tasks, notes), node-context rail,
and the site-wide Documents rollup. Notes are scoped to the Work Plan
selected-node rail. Full-bleed three-pane Work Plan layout with
independently-scrolling flush panes; the Documents tab renders a centered
reading column with explicit scope labelling. Edit affordances are hidden for
read-only users.
- chat/components/dashboards/portfolio/site-detail-page.tsx — adds the
three-tab structure (Overview, Work Plan, Documents) over the existing Overview
cards (URL ?tab= state), and hands the workbench its own layout shell (hidden
tab-bar scroll track).
Docs & tests
- features/portfolio/rhodes-ui/GOAL.md *(new)* — the goal/contract.
- chat/convex/rhodesPortfolioWorkbench.test.ts *(new)* — backend read model,
save mutations, guarded completion-state routing, Drive-identity guard,
authorization.
- chat/components/dashboards/portfolio/__tests__/ *(new)* —
portfolio-rhodes-workbench.test.tsx (tree derivation, node center/rail
rendering, confirm-diff immediate save vs guarded completion-state pending
request, Drive upload + registration retry, read-only gating, truncation
lockout) and site-detail-page-rhodes-tabs.test.tsx (tab routing).
- chat/app/(main)/api/rhodes/drive/__tests__/routes.test.ts *(new)* — the
five Drive route handlers forward the delegated actor correctly.
- chat/rhodes-worker/src/drive-root.test.ts *(new)* — assertDriveIdInsideRoot
accepts files under the authorized root and rejects everything else.
### Design Decisions
- Domain model, not an MCP wrapper — the UI is built on typed Convex read
models and mutations organized around the Rhodes domain, not the MCP function
list. MCP stays an agent/tool pathway; ordinary human edits do not route
through MCP pending approvals.
- Guarded changes are the only deferred writes — dueDiligence edits and
milestone completion-state changes create a pending fieldChangeRequests
row and leave the official value unchanged until EduOps approval. Ordinary
milestone date/status upkeep — and everything else — saves immediately after
confirm-diff. The Work Plan milestone editor flags an edit as an approval
request only when it changes completion state.
- Drive writes go through verified registration — documents are registered
via the worker's delegated, containment-checked Drive path; the public
saveDocument mutation refuses to introduce or alter a Drive identity, so a
verified Drive URL can never be swapped while keeping the old file ID.
- Derived/immutable fields stay protected — sites.stage, Quality Bar
rollup status, and milestone/WUG relationship arrays are shown as
derived/non-editable, never raw-editable.
- Bounded reads — getSiteWorkbench is scoped per tab and flags truncation;
the workbench disables hierarchy editing when rows are partial rather than
acting on incomplete data.
## Test Plan
- [x] pnpm --filter @bran/chat typecheck (also runs as the typecheck-chat pre-commit hook)
- [x] pnpm biome check on changed files (pre-commit hook)
- [x] pnpm --filter @bran/chat test — workbench component tests (33) + tab routing (4) + backend rhodesPortfolioWorkbench.test.ts (39) + Drive route handlers (5) green
- [x] Worker pnpm --filter @bran/location-os-mcp-worker test — drive-root.test.ts containment guard green
- [x] Backend rhodesPortfolioWorkbench.test.ts covers read model, save flows, guarded completion-state routing, Drive-identity guard, and authorization
- [ ] Manual: load a real Buildout (P1) site — confirm Milestones default-expanded; edit a milestone due/start date or non-completion status and verify it saves immediately after confirm-diff (no pending request)
- [ ] Manual: on the same site, flip a milestone's completion state and verify it creates a pending request (no immediate write) with a "Pending EduOps approval" treatment
- [ ] Manual: load an Operating (P2) site — confirm Quality Bars default-expanded with Milestones still accessible
- [ ] Manual: upload a document to Drive from the workbench, then confirm an existing Drive file can be registered without re-uploading; verify edit cannot replace the Drive file on an existing document
- [ ] Manual: confirm a read-only user sees no edit affordances, and that node-scoped (Work Plan rail) vs site-wide (Documents tab) views are clearly distinguished
- [ ] Manual: verify mobile/tablet collapses the three panes intentionally (stacked) rather than squeezing them