# Fleet API

6 operations under /api/v1. Errors are RFC 9457 problem+json with a stable `code` field. The complete schemas are in https://nebulactrl.com/api/v1/openapi.json.

## GET /fleet

Capacity and placement of every server

Per server: resource usage, the memory promised through service limits (committed), whether that exceeds the host, and the services placed on it (limited to the caller's projects).

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Response 200: OK (Overview)
  - `servers` (FleetServer[] | null, required)
  - `totals` (FleetTotals, required)
Errors: 401 Unauthorized; 403 Forbidden; 500 Internal Server Error

## GET /servers/{serverId}/drain

Progress of a server's drain

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serverId` (path, string, required)

Response 200: OK (GetDrainOutputBody)
  - `drain` (DrainView, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /servers/{serverId}/drain

Drain a server

Marks the server as draining (nothing new is placed on it) and moves its services one at a time: each move changes the placement and deploys the service to the target; the old container is removed once the new one is live. The first failed move stops the drain and leaves the remaining services untouched. Scopes: servers:write, deployments:write, projects:write.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `Idempotency-Key` (header, string): Client-chosen key (<=255 chars). A retry with the same key replays the original response for 24 hours.
- `serverId` (path, string, required)

Request body (application/json, required): StartInputBody
  - `confirmStateful` (boolean): Required when a moved service has volumes or is a database: their data is not copied
  - `moves` (MoveBody[] | null, required): Where every service on the server goes; each service must appear exactly once

Response 202: Accepted (DrainView)
  - `actorLabel` (string, required)
  - `cancelRequested` (boolean, required)
  - `createdAt` (string (date-time), required)
  - `finishedAt` (string (date-time) | null, required)
  - `id` (string, required)
  - `message` (string, required)
  - `moves` (Move[] | null, required)
  - `serverId` (string, required)
  - `state` (enum: running | completed | failed | canceled, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## POST /servers/{serverId}/drain/cancel

Stop a drain after the move in flight

The drain ends after the current move. The server returns to `none` when nothing was moved yet, otherwise it becomes `failed` with "Canceled after N of M moves".

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serverId` (path, string, required)

Response 200: OK (DrainView)
  - `actorLabel` (string, required)
  - `cancelRequested` (boolean, required)
  - `createdAt` (string (date-time), required)
  - `finishedAt` (string (date-time) | null, required)
  - `id` (string, required)
  - `message` (string, required)
  - `moves` (Move[] | null, required)
  - `serverId` (string, required)
  - `state` (enum: running | completed | failed | canceled, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## POST /servers/{serverId}/drain/plan

Plan evacuating a server

Dry run: proposes a target for every service on the server, flags stateful services (volumes are not copied), and checks that the targets have the memory. Requires a token that is not restricted to projects.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serverId` (path, string, required)

Request body (application/json, required): PlanInputBody1
  - `targetServerId` (string): Send every service to this server; omit to pick the online server with the most free memory per service

Response 200: OK (DrainPlan)
  - `capacity` (Capacity, required)
  - `moves` (PlanMove[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /servers/{serverId}/undrain

Make a drained server schedulable again

Clears the drain state of a drained or failed-drain server. Services that were moved stay where they are.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serverId` (path, string, required)

Response 200: OK (Server)
  - `acceptWorkloads` (boolean, required): Whether services may be scheduled on this server. Turning it off keeps what already runs here and refuses new placements and moves.
  - `acmeEmail` (string | null, required)
  - `agentVersion` (string | null, required)
  - `baseDomain` (string | null, required)
  - `buildAllowEmulation` (boolean, required): Allow building for another CPU architecture through emulation (slow); needs the agent to report emulation for the platform
  - `buildAllowPreviews` (boolean, required): Allow pull request previews (untrusted code) to build here; off by default
  - `buildEnabled` (boolean, required): The server builds images for other servers and pushes them to a registry (the Builds or Both role). Independent of acceptWorkloads.
  - `buildMaxConcurrent` (integer (int64), required): Builds that run at the same time; more queue on the control plane
  - `composeVersion` (string, required): Docker Compose plugin version the agent reported, empty when it has none
  - `controlPlane` (boolean, required): The host the installer put the control plane on; its proxy serves the dashboard
  - `createdAt` (string (date-time), required)
  - `diskAlert` (boolean, required): True while the root filesystem is at or above the threshold
  - `diskAlertThresholdPercent` (integer (int64), required): Root filesystem usage that raises the disk alert (50-99)
  - `drainMessage` (string, required): Why the last drain failed or was canceled, empty otherwise
  - `drainStartedAt` (string (date-time) | null, required)
  - `drainState` (enum: none | draining | drained | failed, required): Evacuation state; draining and drained servers accept no new services
  - `facts` (Facts, required)
  - `geoEnabled` (boolean, required): Count where requests come from (country, and city at the city level) from the client IP; IPs are never stored
  - `geoLevel` (enum: country | city, required): country needs a ~8 MB database on the server, city ~130 MB
  - `geoRefreshDays` (integer (int64), required): Check for a newer database every N days (1-90)
  - `geoSource` (enum: dbip | maxmind, required): dbip = DB-IP Lite (CC BY 4.0, no key); maxmind = MaxMind GeoLite2 with the team's account
  - `id` (string, required)
  - `lastSeenAt` (string (date-time) | null, required)
  - `latest` (Latest, required)
  - `metricsRetentionDays` (integer (int64), required): How long resource metric history is kept (1-90)
  - `name` (string, required)
  - `patches` (ServerPatchSummary, required)
  - `protocolVersion` (integer (int32) | null, required)
  - `proxy` (Proxy, required)
  - `publicIp` (string | null, required)
  - `serviceCount` (integer (int64), required)
  - `status` (enum: pending | online | offline | revoked, required)
  - `trafficAnalyticsEnabled` (boolean, required): Aggregate Traefik access logs into traffic analytics
  - `trafficDailyRetentionDays` (integer (int64), required): Daily traffic history (7-365 days)
  - `trafficHourlyRetentionDays` (integer (int64), required): Hourly traffic history (2-90 days)
  - `tunnel` (ServerTunnelSummary, required)
  - `unrestricted` (boolean, required): An instance admin made this server unrestricted: stacks and services on it may use privileged mode, host networking, devices, GPUs, a runtime, the host PID namespace, unconfined seccomp or AppArmor, NET_ADMIN, SYS_ADMIN, the Docker socket and host bind mounts
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error
