Skip to content

Monitor HTTP API v1

Moraine exposes a read-only HTTP API for the monitor dashboard and other local monitor clients. The canonical base path is /api/v1. API versioning applies to the HTTP surface only; it does not change the MCP transport or MCP tool schemas.

The examples below use relative paths because the bundled dashboard calls the API on the same origin that served the dashboard.

Route Matrix

All canonical routes are GET routes and successful responses use JSON.

Route Query Purpose
/api/v1/capabilities none Describe this server build, its observed schema migration level, and available HTTP feature groups.
/api/v1/health none Probe ClickHouse health and report a compact ingest heartbeat summary.
/api/v1/status history Return ClickHouse/database diagnostics plus typed ingestion health, finite historical coverage, optional durable progress history, conservative ETA, and active alerts.
/api/v1/analytics range Return token, turn, and concurrent-session time series for a supported window.
/api/v1/tables none List tables with engine, temporary-table marker, and estimated row count.
/api/v1/tables/:table limit Return the named table's schema and a bounded row preview. :table is a path parameter.
/api/v1/web-searches limit Return a bounded list of normalized web-search activity.
/api/v1/sessions since, limit Return bounded session analytics, including the turns and steps used by the dashboard.

There is no session-detail backend route. In particular, /api/v1/sessions/:id is not part of the API. A client that needs a session must use the records returned by /api/v1/sessions; it must not infer a detail route from the collection URL.

Session title uses the shared display-label precedence: explicit metadata title/name, then the first genuine Codex event_msg/user_message, followed by existing metadata and a privacy-safe descriptor. Injected Codex response_item/message role=user content is not eligible for the prompt tier. Prompt-derived labels are trimmed to the first line and capped at 120 Unicode scalar values plus an ellipsis.

Capabilities Contract

GET /api/v1/capabilities returns HTTP 200 with this contract:

{
  "ok": true,
  "server_version": "0.0.0",
  "schema_migration_level": "017",
  "features": {
    "analytics": true,
    "sessions": true,
    "table_inspection": true,
    "web_searches": true
  }
}

The example version values are illustrative. Field semantics are:

  • ok is always true for a successfully generated capabilities document.
  • server_version is the version of the running Moraine server build. It is not the ClickHouse version.
  • schema_migration_level is the latest applied Moraine migration identifier the server can observe. Migration identifiers are opaque strings; clients must not parse them as integers. It is null when no applied migration level is available, including when the database or migration ledger is absent or cannot be read. That null is capability metadata, not by itself an HTTP error.
  • features.analytics means the analytics collection route is implemented.
  • features.sessions means session collection is implemented. It does not advertise a session-detail route.
  • features.table_inspection means table listing and bounded table preview are implemented.
  • features.web_searches means the normalized web-search collection is implemented.

Feature values advertise route support, not backend health or the presence of rows. A feature can be true while a particular read returns an empty result or a backend error.

The features object is additive. Clients must test the keys they understand and ignore unrecognized feature keys. Adding another feature key does not, on its own, require a new API version. The top-level fields and the four feature keys shown above are the required v1 contract.

Query Parameters and Bounds

Limits are parsed as unsigned decimal integers and then clamped. A value below the lower bound, including 0, becomes the lower bound; a value above the upper bound becomes the upper bound. A value that cannot be parsed as an unsigned integer is a malformed query and returns HTTP 400.

Route Parameter Default Accepted or effective values
/api/v1/status history 0 clamped to 0..=120; omitted or 0 performs no history read
/api/v1/analytics range 24h 15m, 1h, 6h, 24h, 7d, or 30d
/api/v1/sessions since 30d 1h, 6h, 24h, 7d, 30d, 90d, or all
/api/v1/sessions limit 50 clamped to 1..=200
/api/v1/web-searches limit 100 clamped to 1..=1000
/api/v1/tables/:table limit 25 clamped to 1..=500

An unknown range does not produce an error; it resolves to 24h. An unknown since similarly resolves to 30d. Responses report or embody the resolved window, so clients should treat the supported values above as the request contract rather than relying on fallback behavior.

Status history is the exception to positive collection limits: zero is meaningful and preserves the cheap latest-only status poll. When requested, history contains at most 120 oldest-to-newest narrow checkpoint points for the selected ingestor run; it does not repeat host data, source paths, errors, or backend sink payloads.

The table path parameter must match the strict ASCII identifier pattern [A-Za-z_][A-Za-z0-9_]*. A rejected identifier returns HTTP 400 with code: "invalid_request". A syntactically valid name that cannot be read from the configured database is a backend read failure, not a 404 resource response.

Successful Empty and Nullable Values

Collection routes use empty arrays when a successful query has no matching rows. They do not use null to mean an empty collection.

null marks unavailable or unobserved optional diagnostics:

  • capabilities.schema_migration_level is null under the conditions described in the capabilities contract.
  • /api/v1/status can return HTTP 200 and ok: true while clickhouse.healthy is false. In that diagnostic response, unavailable clickhouse.version or clickhouse.ping_ms values are null and clickhouse.error explains the probe result. If the database does not exist, its table list is empty.
  • Health and status connection diagnostics use connections.total: null with a sibling connections.error when connection metrics are unavailable. A connection-metrics error alone does not make an otherwise successful health probe fail.
  • When no ingest heartbeat is available, ingestor.present and ingestor.alive are false, while ingestor.latest and ingestor.age_seconds are null.
  • ingest_status is null when no database/status is available in an otherwise successful diagnostic response. A typed repository read failure is instead a non-2xx response as described below. When present, its independent health, coverage, freshness, and readiness conditions use true, false, or unknown states with stable reason codes.
  • query_pressure is always present in a successful status response. Its scope is process; interactive, background, migration, and administrative each report current running/queued counts and a cumulative rejected count. resource_limit_events is cumulative for that backend process. No counter or label contains SQL, query text, credentials, or user content.
  • ingest_status.heartbeat.latest.progress is absent on pre-034 heartbeat rows. A present progress snapshot freezes the startup file/byte denominator for one ingestor instance and advances only after checkpoint rows are durably committed. SQLite sources without a byte watermark report completed-file coverage; coverage_basis: "files" identifies that measurement without degrading an otherwise complete, healthy ingest run.
  • ingest_status.history is omitted when history is omitted or zero. Requested history points contain timestamp, queue/active pressure, queue capacity, sink row/retry pressure, discovery state, and durable file/byte completion counters.
  • ingest_status.rate and ingest_status.eta are null until at least six same-instance observations cover 30 seconds with stable throughput. ETA is also suppressed during discovery, sink retries, zero progress, and excessive short/long-window variance.

Clients must distinguish a diagnostic value inside an HTTP 200 response from an endpoint failure represented by a non-2xx status.

Errors and Status Codes

API handlers use JSON errors with at least this envelope:

{
  "ok": false,
  "code": "backend_failure",
  "error": "human-readable message"
}

An endpoint can add diagnostic fields to that minimum envelope. code is a stable machine-readable category; error message text is narrative operator context and can include backend details. For example, a health failure also reports its configured database information and connection diagnostics. Clients should branch on the HTTP status and code, not match arbitrary message text.

Status Meaning
200 OK The request completed. Inspect diagnostic fields such as clickhouse.healthy; 200 does not mean every component is healthy.
400 Bad Request Request/repository validation failed (invalid_request), including a rejected table identifier. Framework-generated malformed-query responses are not guaranteed to use the application JSON envelope.
403 Forbidden Static-file path traversal or a path that resolves outside the configured static root. The JSON error is {"ok":false,"error":"forbidden"}.
404 Not Found No requested static file exists. The JSON error is {"ok":false,"error":"not found"}.
405 Method Not Allowed The path exists but does not support the requested HTTP method. A JSON error envelope is not guaranteed.
429 Too Many Requests Admission is full (busy), or a ClickHouse memory, disk, row, or byte limit was reached (resource_exhausted).
499 Client Closed Request Admitted repository work was cancelled (cancelled) while an HTTP response was still possible. A fully disconnected client cannot receive this response.
500 Internal Server Error A Moraine invariant/implementation failed (internal_error), or the static root/file cannot be resolved/read.
503 Service Unavailable A required repository, ClickHouse, or transport operation failed (backend_failure).
504 Gateway Timeout An explicitly supplied absolute caller deadline expired (deadline_exceeded). Current Monitor requests do not supply one, and Moraine adds no default query deadline.

/api/v1/health returns 503 when the required store-health read, ping, or version probe fails. Most collection read failures also return 503. /api/v1/status is deliberately diagnostic: it can describe a missing or unhealthy database with 200; it returns 503 when a required table-summary read fails after the database was observed to exist.

One-Release Legacy Aliases

The previous /api/* dashboard routes remain as direct aliases for one release:

Canonical route Temporary legacy alias
/api/v1/health /api/health
/api/v1/status /api/status
/api/v1/analytics /api/analytics
/api/v1/tables /api/tables
/api/v1/tables/:table /api/tables/:table
/api/v1/web-searches /api/web-searches
/api/v1/sessions /api/sessions

A direct alias invokes the same handler, with the same query handling, response payload, and status code. It is not an HTTP redirect and does not return a Location header. New clients must use /api/v1; the aliases are temporary migration support and are removed after their one-release compatibility window.

/api/v1/capabilities is new and has no /api/capabilities alias.

Static Assets

API routes take precedence over static-file handling. Every other GET request is resolved under the configured monitor static directory:

  • / serves index.html.
  • A path naming a directory serves that directory's index.html.
  • A path naming a file serves that file with a MIME type inferred from its extension.
  • Missing paths return the JSON 404 described above. There is no implicit single-page-app rewrite of an unknown path back to the root index.html.
  • Paths containing .., and paths whose canonical location escapes the static root, return the JSON 403 described above.

The server validates at startup that the selected static path is a directory and contains index.html. --static-dir selects custom built assets; without an explicit override, packaged assets or the source-tree web/monitor/dist build are used according to the server's static-directory resolution rules.

Because unmatched paths enter static-file handling, an unknown path under /api/v1 is not evidence that an API resource exists. In particular, clients must not probe an inferred /api/v1/sessions/:id path.

Shared MCP Endpoint

When backend.bind is an explicit loopback IP, the unified backend also serves MCP at POST /mcp on this listener. The endpoint accepts one JSON-RPC message per request using Content-Type: application/json. Clients should send Accept: application/json, text/event-stream; responses are JSON. Calls after initialize must include the negotiated MCP-Protocol-Version header. Notifications return 202 Accepted with an empty body.

GET /mcp returns 405 Method Not Allowed. This endpoint does not allocate MCP session IDs or expose batch and SSE response modes. It shares the same repository, caches, request admission, and cancellation behavior as the backend's private Unix-socket MCP service.

MCP routes do not live under /api/v1, and HTTP clients cannot select a named backend or launch-directory project scope through this endpoint. Use moraine run mcp -- --project-only for scoped retrieval.

The endpoint is mounted only for an explicit loopback backend.bind. A configured auth token may permit non-loopback monitor startup, but /mcp remains disabled on that listener because HTTP request authentication is not implemented.