7.2 KiB
HTTP contract
JSON API, UTF-8. Authenticated sessions use an HttpOnly SameSite=Strict cookie. Mutating authenticated requests require X-CSRF-Token from login or session response. Browser requests must be same-origin. No secrets returned in snapshots. Application-handled failures return { "error": "human readable explanation" } with non-2xx status. Framework routing errors (for example 405) may have a plain-text body; clients must tolerate this. A snapshot is the source of truth; a 2xx response confirms completion. After admission under the serialized operation lock, topology mutations run independently of the HTTP connection through commit/rollback and cleanup. A lost response does not imply an uncommitted change: refresh the snapshot before retrying. Requests cancelled while waiting for admission make no change. Shutdown rejects new admissions and waits for admitted work within the documented process deadline.
Routes
GET /— embedded UI shell (login shown until session validated).GET /licenses.txt— licenses and attribution for the bundled Bootstrap/Font Awesome assets.GET /healthz— process readiness{status:"ok"}(not upstream checker health).POST /api/loginJSON{password:string}→{authenticated:true,csrf:string}, Set-Cookie. 401 invalid, 429 limited.GET /api/session→{authenticated:true,csrf:string}, 401 expired/missing.POST /api/logout→{ok:true}, clears cookie.GET /api/snapshot→ full snapshot below.GET /api/events— SSE, event namesnapshot, full snapshot every second, heartbeat 15s. Session expiry closes stream. EventSource reconnects automatically; recheck session after errors.PUT /api/services/{id}— full ServiceConfig JSON; id must match path. Atomically save and hot-apply. →{ok:true}. Validation/bind error: 400; persistence failure: 400 with explanation, runtime rollback attempted. Stopped listener drains established sessions. Changing primary, replicas, policy or auto mode does not restart a running listener. Existing sessions keep their previous route.DELETE /api/services/{id}— requiresenabled:falsefirst; drains/removes then →{ok:true}.PUT /api/teams/{id}— full Team JSON; new team or rename. Addresses of existing ids are immutable; add another id instead. →{ok:true}.
ServiceConfig:
{"id":"notes","name":"Notes","listen":"0.0.0.0:18080","upstream_port":8080,"primary":"a","replicas":["b"],"enabled":false,"auto_switch":false,"replica_policy":"isolate"}
Team:
{"id":"a","name":"Team A","address":"192.0.2.10"}
Backward-compatible optional fields: ServiceConfig has custom:false and custom_ip:null; Team has hidden:false. The legacy add dialog sends custom:true, equal listen/upstream ports and an optional custom_ip. A non-null IP resolves/reuses its team, sets primary, and atomically saves team plus service. New anonymous IP targets have hidden:true; the UI shows them only in services that reference them. No separate operator team-creation step is required. Clearing custom_ip leaves the submitted primary/replicas authoritative. Manual quick promotion retains the previous primary as replica; automatic promotion does the same and clears the direct-IP override.
Configured/saved topology containing a non-null custom_ip must also contain its matching primary team. The PUT adapter creates that pair; raw TOML/state files are validated rather than silently repaired.
Snapshot
{
"now_ms": 1792700000000,
"main_team_ip": "192.0.2.10",
"health_max_age_ms": 12000,
"teams": [{"id":"a","name":"Team A","address":"192.0.2.10"}],
"services": [{
"id":"notes","name":"Notes","listen":"0.0.0.0:18080","upstream_port":8080,
"primary":"a","replicas":[],"enabled":true,"auto_switch":false,"replica_policy":"isolate",
"engine":{"running":true,"last_error":null,"stats":{"accepted":5,"active":1,"completed":4,"connect_errors":0,"io_errors":0,"rejected":0,"accept_errors":0,"bytes_up":1234,"bytes_down":5678,"replica_disconnects":0,"replica_errors":0,"replica_bytes":0}},
"health":{"a":{"reachable":true,"healthy":true,"consecutive_failures":0,"consecutive_successes":2,"checked_at_ms":1792700000000,"latency_ms":2}},
"scoreboard":{"a":{"status":101,"stolen":3,"lost":0}}
}],
"scoreboard":{"enabled":true,"connected":true,"kind":"forcad","round":5,"updated_at_ms":1792700000000,"stale":false,"last_error":null,"teams":[],"tasks":[],"entries":{}},
"events":[{"at_ms":1792700000000,"service":"notes","message":"Настройки сохранены"}]
}
Compute displayed bandwidth from positive byte-counter deltas / now_ms deltas, reset baseline when counters decrease. Show unknown health until checked. Scoreboard badges must indicate stale data; never imply stale checker state is current. Replica counters mean explicit disconnections/errors, not dropped arbitrary packets. replica_policy: strict uses backpressure, isolate disconnects a lagging replica per session.
The restored panel retains the legacy four connection counters; byte totals and replica counters are available in their tooltip and the snapshot API. TCP freshness uses health_max_age_ms; the UI additionally marks the whole snapshot stale and disables mutations after five seconds without updates. The top service strip shows the configured main_team_ip checker status, independently of the selected upstream, as in the legacy panel. Unset main IP gives an uncolored strip. Checker names and lost-flag chips retain the original color/sort behavior.
UI follow-up: the Teams dialog exposes existing team PUT (create/rename, preserving hidden status; existing address read-only). Configuration help displays existing scoreboard snapshot fields without adding endpoints. Invalid custom-IP drafts stay local across snapshots; Enter retries save and Escape restores the saved address. Changing a direct IP preserves other replicas; the backend removes any new-primary overlap. Add-proxy drafts persist across dialog cancel/reopen, until successful save or page reload. Process configuration remains TOML-only and requires a restart.
Optional flag tokenization
ServiceConfig adds flag_protection: boolean (default false when absent). Full PUT
must preserve this field; switching it affects new connections only. Enabling
requires global [flags] configuration and a successfully initialized keyed vault.
Saved topology contains only this boolean, never keys or flag mappings.
Snapshot adds flag_protection: null | {ready,entries,cache_hits,encoded,decoded,errors,capacity,ttl_seconds}.
Counters are global across protected services and count occurrences, not distinct
flags (except entries). They reset on restart; entries reload from the encrypted
vault. Entries expire lazily on vault access/startup. ready means the worker is
available, not proof that every future disk write will succeed. Flags, tokens,
keys, and vault paths are never returned. No endpoint dumps or clears the vault.
Known tokens are decoded only in the matching service-ID namespace. Unknown/expired matches pass unchanged. Full-vault or persistence failure closes the protected TCP session and increments errors; there is no plaintext fallback. See flag-protection.md for format, TTL, cryptography and TCP limits.