Portfolio API
Six routes back the Portfolio surfaces: portfolio, portfolio-health, portfolio-import,
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.
GET / POST /api/portfolio-import
{ "mode": "dryRun" }
GET lists the last 20 import runs (portfolio_import_runs) for the org, with parser version,
counts, validation, and any error. POST starts an import — either inline or, when Trigger is
configured, enqueued via triggerTask rather than run synchronously in the request.
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
sable-agents-demo/web/api/portfolio.js,portfolio-health.js,portfolio-import.js,portfolio-options.js,portfolio-actions.jssable-agents-demo/web/api/_portfolio.js,_portfolio_health.js,_portfolio_scope.js,_portfolio_mutation.js,_account_creation.js,_client_mutation.js,_project_update.jssable-agents-demo/trigger/portfolio.ts—sable.portfolio.ingest-baseline,sable.portfolio.daily