Add JSON state API for daemon
Executive Summary
Once daemon mode (#70) and headless EC2 (#60) ship, ShipCode runs without a desktop window. There is no way to ask "what's running, what failed, what's queued" from outside the main process. Symphony solves this with a small HTTP server (SPEC §13.7) that exposes a JSON state endpoint plus a refresh trigger. This issue adds the equivalent: a loopback-only API and a minimal HTML dashboard, suitable for curl, scripts, and browser inspection on a headless host.
Implementation Checklist
Problem Statement
The pipeline state today lives in the Electron main process and is only observable through the renderer. Daemon and headless modes need a programmatic surface that does not require IPC, does not require the desktop app to be running, and does not push state to a third-party. Operators need to confirm "is anything stuck?", trigger an immediate reconcile after editing config, and look up the log path for a single issue's run.
Goals
GET /api/v1/state returns running pipelines, retry queue, totals, and current rate-limit headroom.
GET /api/v1/<issue_number> returns runtime detail for one issue, including log paths.
POST /api/v1/refresh enqueues an immediate reconcile tick and returns 202.
GET / returns a minimal server-rendered HTML dashboard.
- Bind to
127.0.0.1 by default; require explicit config to bind elsewhere.
Non-Goals
- Not adding write endpoints beyond
/refresh.
- Not adding authentication in this issue (loopback-only is the security boundary).
- Not building a rich SPA dashboard — server-rendered HTML is enough.
- Not exposing the desktop renderer over the network.
User Stories with Acceptance Criteria
Story 1: Operator inspects daemon state
- As an operator on a headless box, I run
curl localhost:PORT/api/v1/state and get a JSON snapshot.
- Acceptance: response includes
running[], retrying[], totals, rate_limits; HTTP 200; valid JSON.
Story 2: Operator looks up one issue
- As an operator, I run
curl localhost:PORT/api/v1/123 for a known active issue.
- Acceptance: response includes phase, attempt, log file paths, last error if any; HTTP 200.
Story 3: Refresh on demand
- As an operator who just edited WORKFLOW.md, I
POST /api/v1/refresh and the dispatcher runs immediately.
- Acceptance: HTTP 202; reconcile tick observed in logs within 1 second.
Story 4: Closed issue returns 404
- As a script, I query an issue that is not currently tracked.
- Acceptance: HTTP 404 with
{error: {code:'not_found', message: ...}}.
Functional Requirements
- New process owns the HTTP server, sourcing pipeline state from the main process via existing in-process channels (or directly when daemon is the main process).
- Default bind:
127.0.0.1:<port>; port configurable; bind address configurable.
- Read-only endpoints except
POST /refresh.
- All non-GET methods on read-only endpoints return HTTP 405.
- Error envelope:
{error: {code, message}} for all 4xx/5xx.
- HTML dashboard at
/ lists running and retrying pipelines with links to per-issue detail.
Non-Functional Requirements
- Server start failure (port in use, etc.) does not crash the pipeline; logs and continues without API.
/api/v1/state returns within 100ms for up to 100 concurrent issues.
- No persistent connections required; plain HTTP/1.1.
Success Criteria
curl http://127.0.0.1:PORT/api/v1/state returns the documented shape with the daemon idle.
- Querying an issue not in the registry returns HTTP 404 with the documented error envelope.
POST /api/v1/refresh triggers a dispatcher tick observable in logs within 1 second.
PUT /api/v1/state returns HTTP 405.
- Killing the API server while pipelines run does not interrupt any in-flight phase.
Out of Scope
- Auth, TLS, multi-tenant scoping.
- WebSocket / SSE streams.
- Mutation endpoints beyond
/refresh.
- Cross-host federation of multiple daemons.
Dependencies
Verification Plan
- Integration test: start the server with a fake pipeline registry, hit each documented endpoint, assert shapes and status codes.
- Bind-address test: confirm default refuses connections from non-loopback.
- Failure-mode test: simulate port-in-use; assert daemon continues without API.
- Manual: run on a headless box, confirm dashboard renders in a browser via SSH port-forward.
Risks & Open Questions
- Risk: Endpoint shape drift across versions; document the schema and version under
/api/v1/.
- Risk: Loopback-only assumption broken when users tunnel; document recommended SSH-tunnel usage.
- Open Q: Should
/api/v1/state include recent log tail snippets, or only paths?
- Open Q: Where does this live —
apps/cli, packages/server, or inside the existing daemon?
Add JSON state API for daemon
Executive Summary
Once daemon mode (#70) and headless EC2 (#60) ship, ShipCode runs without a desktop window. There is no way to ask "what's running, what failed, what's queued" from outside the main process. Symphony solves this with a small HTTP server (SPEC §13.7) that exposes a JSON state endpoint plus a refresh trigger. This issue adds the equivalent: a loopback-only API and a minimal HTML dashboard, suitable for
curl, scripts, and browser inspection on a headless host.Implementation Checklist
Problem Statement
The pipeline state today lives in the Electron main process and is only observable through the renderer. Daemon and headless modes need a programmatic surface that does not require IPC, does not require the desktop app to be running, and does not push state to a third-party. Operators need to confirm "is anything stuck?", trigger an immediate reconcile after editing config, and look up the log path for a single issue's run.
Goals
GET /api/v1/statereturns running pipelines, retry queue, totals, and current rate-limit headroom.GET /api/v1/<issue_number>returns runtime detail for one issue, including log paths.POST /api/v1/refreshenqueues an immediate reconcile tick and returns 202.GET /returns a minimal server-rendered HTML dashboard.127.0.0.1by default; require explicit config to bind elsewhere.Non-Goals
/refresh.User Stories with Acceptance Criteria
Story 1: Operator inspects daemon state
curl localhost:PORT/api/v1/stateand get a JSON snapshot.running[],retrying[],totals,rate_limits; HTTP 200; valid JSON.Story 2: Operator looks up one issue
curl localhost:PORT/api/v1/123for a known active issue.Story 3: Refresh on demand
POST /api/v1/refreshand the dispatcher runs immediately.Story 4: Closed issue returns 404
{error: {code:'not_found', message: ...}}.Functional Requirements
127.0.0.1:<port>; port configurable; bind address configurable.POST /refresh.{error: {code, message}}for all 4xx/5xx./lists running and retrying pipelines with links to per-issue detail.Non-Functional Requirements
/api/v1/statereturns within 100ms for up to 100 concurrent issues.Success Criteria
curl http://127.0.0.1:PORT/api/v1/statereturns the documented shape with the daemon idle.POST /api/v1/refreshtriggers a dispatcher tick observable in logs within 1 second.PUT /api/v1/statereturns HTTP 405.Out of Scope
/refresh.Dependencies
Verification Plan
Risks & Open Questions
/api/v1/./api/v1/stateinclude recent log tail snippets, or only paths?apps/cli,packages/server, or inside the existing daemon?