Portfolio API
Five routes back the Portfolio surfaces: portfolio, portfolio-health, portfolio-import (retired, 410),
portfolio-options, portfolio-actions.
GET /api/portfolio
Scoped account/project payload — buildPortfolioPayload, scoped through
resolvePortfolioScope/scopeRequest/visibleProjectIds, the same scope helpers /api/chat and
/api/spine build on. publicViewer produces the narrower shape a client-role viewer sees.
GET /api/portfolio-health
Admin/lead, read-only. Board coverage and open-item verification, computed on read rather than
stored — this is deliberately the home for portfolio-level data-quality findings that used to
arrive as board_update approvals with no project attached, which meant they were approvable but
could never actually write anything. VERIFY_WINDOW_DAYS (14) matches STALE_AFTER_DAYS in
_portfolio_health.js, and the staleness count is computed server-side in SQL rather than pulling
thousands of rows into JavaScript to count client-side.
/api/portfolio-import (retired)
Every method returns 410. The Drive roster workbook (Active_Projects_MASTER_07_22.xlsx) was
imported once on 6 Aug 2026 and then retired. Project records are kept in SABLE, so importing the old
sheet again would roll them back (reopen completed projects, restore July rosters and owners). Past
runs stay in portfolio_import_runs for audit, and the portfolio page (nav "Clients") no longer shows a roster sync.
GET /api/portfolio-options
Admin/lead. Returns clients and people lookups for populating portfolio dropdowns/forms — a small convenience route so the frontend doesn't have to duplicate these queries.
POST /api/portfolio-actions
A dispatcher for several distinct mutations, each normalized and validated before it reaches the
database: status updates (appendStatus), account creation (normalizeAccountCreation), client
create/update (normalizeClientCreation/normalizeClientUpdate), operating-profile updates,
portfolio classification, reconciliation resolution, and project updates. Each mutation type has
its own normalizer specifically so a malformed request fails with a clear statusCode and message
before it ever reaches a database write — throwPortfolioMutationError turns any mapped error into
exactly that shape.
Where the code lives
web/api/portfolio.js,portfolio-health.js,portfolio-options.js,portfolio-actions.js;portfolio-import.jsonly answers 410web/api/_portfolio.js,_portfolio_health.js,_portfolio_scope.js,_portfolio_mutation.js,_account_creation.js,_client_mutation.js,_project_update.jstrigger/portfolio.ts—sable.portfolio.ingest-baseline(preview-only, manual),sable.portfolio.daily