Skip to main content

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.js only answers 410
  • web/api/_portfolio.js, _portfolio_health.js, _portfolio_scope.js, _portfolio_mutation.js, _account_creation.js, _client_mutation.js, _project_update.js
  • trigger/portfolio.ts — sable.portfolio.ingest-baseline (preview-only, manual), sable.portfolio.daily