# Full-sync HTTP control API The handler imports no queue or storage implementations. Inject the five contract interfaces in `Dependencies`: `RunLister`, `RunReader`, `FailurePager`, `ReplayRequester`, and `ActionReader`. The replay service owns durable idempotency and execution; the HTTP layer does not cache requests or launch work. Use `NewRunReader(live, archive, rounds)` to compose `LiveRunReader`, `ArchivedRunReader`, and `RoundStatsReader` into a `RunReader`. Live metadata wins and returns `source: "live"`; only `ErrNotFound` from live metadata permits archive fallback, returning `source: "archive"`. Live storage errors and round lookup errors propagate. Archive and round readers may be nil to disable those lookups. A failure listing always calls `FailurePager` directly, never archives. ## Routes and responses All routes require `Authorization: Bearer `. Missing, wrong, or ambiguous credentials return 401 with a `WWW-Authenticate` challenge. An empty configured token is a startup configuration error. Responses use JSON and `Cache-Control: no-store`. | Method | Route | Success body | | --- | --- | --- | | GET | `/internal/v1/full-sync-runs?syncKind=&limit=` | 200 `contract.RunList` | | GET | `/internal/v1/full-sync-runs/{runId}` | 200 `contract.RunView` | | GET | `/internal/v1/full-sync-runs/{runId}/failures?cursor=&limit=` | 200 `contract.FailurePage` | | POST | `/internal/v1/full-sync-runs/{runId}/replay` | 202 `contract.ReplayAccepted` | | GET | `/internal/v1/full-sync-actions/{actionId}` | 200 `contract.ReplayAction` | Run list results merge active Redis metadata with flat `run-.json` archives under the configured archive root. Active Redis metadata wins when an ID exists in both sources. `syncKind` accepts `traffic_usage` or `sim_status`; `limit` defaults to 50 and is capped at 100. Failure limits use the contract rules: absent, empty, or zero selects 100; positive values up to 500 are accepted; other values are rejected. Repeated cursor/limit parameters and malformed query encoding are rejected. Cursors are opaque and passed unchanged after URL decoding. The pager validates cursor contents and run binding. Empty pages serialize `items: []` and preserve `nextCursor`; only an empty next cursor signals completion. Replay requires a single `Idempotency-Key` header and a JSON body such as `{"requestedBy":"operator"}`. The body is limited to 64 KiB, must contain one JSON object with no unknown fields, and must satisfy `ReplayRequest.Validate`. If supplied, Content-Type must be `application/json` (parameters are allowed). The exact `requestedBy` value is preserved for fingerprinting. Repeated accepted requests return the service's existing action/target and current status with 202. The adapter constructs `statusUrl` and the `Location` header from actionId. All application errors use this envelope, without storage error details: ```json {"error":{"code":"invalid_argument","message":"Invalid request"}} ``` | Condition | HTTP status | Error code | | --- | --- | --- | | Invalid path, query, header, body, or `ErrInvalidArgument` | 400 | `invalid_argument` | | Missing/wrong bearer credentials | 401 | `unauthorized` | | Unknown route/resource or `ErrNotFound` | 404 | `not_found` | | Unsupported route method | 405 | `method_not_allowed` | | `ErrConflict` (including changed idempotent request), `ErrLeaseLost` | 409 | `conflict` | | `ErrFailureDetailsUnavailable` | 410 | `failure_details_unavailable` | | Oversized replay body | 413 | `body_too_large` | | `ErrBusy` | 503 | `busy` | | Other service errors or unencodable service output | 500 | `internal_error` | ## Lifecycle `NewHandler(token, deps)` can be mounted by an existing HTTP server. For an independent listener, construct `NewServer(Config{Addr: address, BearerToken: token}, deps)` and call `Start(workerCtx)`. `Start` binds synchronously and returns address-in-use and other startup errors directly. `Addr()` exposes the bound address, including an OS-assigned port when configured with port zero. Call `Wait()` after Start, normally in the worker's supervision goroutine. It returns serving or shutdown failures; it also returns startup failures if Start failed. The server is single-use and rejects a second Start. Wait can have multiple callers but must not be called before Start. Canceling `workerCtx` closes the listener and drains active HTTP requests. Active request contexts stay usable during the drain. `ShutdownTimeout` defaults to ten seconds; if it expires, active connections are closed and Wait returns an error wrapping `context.DeadlineExceeded`. Normal cancellation with a successful drain returns nil. No worker wiring or configuration-file changes are part of this package. ## Verification ```text gofmt -l internal/fullsync/httpapi go test ./internal/fullsync/... go test ./internal/fullsync/httpapi -cover go vet ./internal/fullsync/... ``` Tests use contract mocks and local HTTP listeners, requiring no Redis or archive files. `go test -race ./internal/fullsync/...` additionally requires a supported C compiler and CGO enabled.