WebUI API
Base path: /api
Request Headers
The WebUI does not require authentication. Fleet-scoped endpoints require an
X-Fleet-Id header. The header value is the integer fleet id, sent as a string
over HTTP and coerced by the backend with int(...). A missing or non-integer
value returns 400. The backend verifies the fleet exists in the fleets table.
These fleet-scoping errors apply to every fleet-scoped endpoint:
No server-side session cookies. The SPA stores the active fleet_id client-side via hash-based routing and sends it in the X-Fleet-Id header on each request.
Endpoints
GET /api/fleets — List Fleets
Returns non-soft-deleted fleets (deleted_at IS NULL) with member counts, ordered newest-first by created_at DESC, fleet_id ASC. No headers required.
Response (200 OK):
GET /api/members — List Members
Returns the selected fleet's roster via list_roster(include_message_holders=True): every active registry entry plus deregistered members that still own messages (so their message history stays inspectable). Every row carries a kind discriminator so the frontend can locate the root Director without matching on its name.
Request: X-Fleet-Id: <fleet_id> header.
Response (200 OK):
monitor field: each member carries its folded monitoring schedule —
{"interval_seconds": int, "last_ping_at": str|null, "enabled": bool} — or
null when the member is not enrolled. Folding the schedule into the list lets
the SPA render every member's schedule without an extra request per member.
Which members are enrolled — the watched set — is defined in
Monitoring.
kind values — the unified 3-value vocabulary:
The discriminator is derived at read time — the fleets join supplies "is this the root Director" and the stored member card supplies the special-kind marker; there is no dedicated column.
GET /api/monitor — Fleet Monitor Runtime
Returns the liveness of the fleet's cafleet monitor process, derived from the
monitor_runtime heartbeat (true even when the process died silently). Lets the
members page show a "monitor running / stopped" indicator so an inert schedule
does not mislead. See Monitoring.
Request: X-Fleet-Id: <fleet_id> header.
Response (200 OK):
When no monitor is running — no runtime row, or a stale or cleared heartbeat — the response fields take these values:
Launching the loop is CLI-only (cafleet monitor start, run as a
background task); there is no POST/DELETE counterpart here and no
monitor stop command — the loop terminates with the monitoring member's
pane (member delete), or self-terminates after fleet delete.
GET /api/members/{member_id}/monitor — Member Monitor Config
Returns one member's monitoring schedule.
Request: X-Fleet-Id: <fleet_id> header.
Response (200 OK):
Errors: 404 (detail: "Member not enrolled") when the member is not in the
fleet, or is not one of the
enrolled member classes — in
addition to the shared fleet-scoping errors in
Request Headers.
The SPA reads the folded monitor field on GET /api/members instead of
calling this endpoint per member — it exists for CLI/API parity.
PATCH /api/members/{member_id}/monitor — Edit Member Monitor Config
Updates a member's interval and/or enabled flag and returns the new config.
Request: X-Fleet-Id: <fleet_id> header.
Both fields are optional; a present interval_seconds must be >= 1.
Response (200 OK): the updated config, same shape as the GET above.
Errors — in addition to the shared fleet-scoping errors in Request Headers:
GET /api/members/{member_id}/inbox — Inbox Messages
Returns messages received by the member (owner_member_id = member_id), excluding broadcast_summary type messages. Consumed by the member detail view's Inbox tab in the admin WebUI.
The three message endpoints compare as follows:
Request: X-Fleet-Id: <fleet_id> header.
Response (200 OK):
All message endpoints (inbox, sent, timeline) share the same row formatter, so the field set is identical to GET /api/timeline — including status_timestamp and origin_message_id (see the timeline section below for their semantics).
The body field is the message's text column.
Status values: input_required (Pending), completed (Acknowledged).
GET /api/members/{member_id}/sent — Sent Messages
Returns messages sent by the member (single SQL query against messages filtered by from_member_id and ordered by status_timestamp DESC, served by idx_messages_from_member_status_ts), excluding broadcast_summary type messages. Consumed by the member detail view's Sent tab in the admin WebUI.
Request: X-Fleet-Id: <fleet_id> header.
Same response format as inbox.
GET /api/timeline — Unified Fleet Timeline
Returns the selected fleet's non-broadcast_summary messages. Consumed by the Discord-style admin dashboard, which groups delivery rows sharing an origin_message_id into a single broadcast entry client-side.
Request: X-Fleet-Id: <fleet_id> header.
Fleet scoping is reached through the messages.from_member_id → members.member_id → members.fleet_id join. Only messages whose sender belongs to the header fleet are returned; cross-fleet messages are invisible.
Response (200 OK):
The frontend re-orders ascending for newest-at-bottom chat rendering.
Exclusions: Rows with type == "broadcast_summary" are filtered out of the response. The summary row is not needed for the UI; the grouping convention below lets the frontend reconstruct broadcasts from their delivery rows alone.
Broadcast grouping: Every row carries an origin_message_id field:
The client groups rows by origin_message_id (non-null rows sharing a value form one broadcast entry; null rows are standalone unicast entries). Each broadcast entry's sort key is the MIN(created_at) of its rows — stable, so a broadcast never drifts when a lagging recipient ACKs.
ACK timestamps: Per-recipient ACK time is read from the status_timestamp of a completed delivery row. Delivery messages make exactly one state transition over their lifetime (input_required → completed on ACK), so for status == "completed" rows status_timestamp IS the ACK moment. If this invariant is ever broken by a future change, the timeline will silently show wrong ACK times until a dedicated acknowledged_at column is added. See Data model § ACK timestamp inference.
POST /api/messages/send — Send Message
Sends a message from a same-fleet active member. Supports both unicast (to_member_id=<int>) and broadcast (to_member_id="*").
Request:
to_member_id accepts an integer (unicast) or the string "*" (broadcast). from_member_id is always an integer.
Unicast (to_member_id is an integer): the server verifies both the sender and the destination belong to the caller's fleet and that the destination is active.
Broadcast (to_member_id == "*"): the server skips destination validation (no specific recipient to verify) and fans out to every active member in the fleet except the sender, plus a summary message. The sender is still required to be active and in the caller's fleet. The response's message_id is the summary message's id.
Sender identity: The Admin WebUI always submits from_member_id = director.member_id (the fleet's root Director). The endpoint itself is sender-agnostic — it accepts any active member in the fleet — but no UI path lets the operator pick a different sender.
Response (200 OK):
Errors — in addition to the shared fleet-scoping errors in Request Headers:
Error Format
Every WebUI API error — the 400 / 404 responses and request-validation
failures (422) alike — carries a single detail string: