fix(dashboard): add lightweight /api/health liveness endpoint

/api/status is the only public liveness route, and its handler loads the
gateway config, probes gateway health, and counts sessions before it can
answer. That work is wrong for a readiness probe: a caller that only needs
to know the process is up pays for a cold plugin import tree.

Add /api/health, which returns process liveness, version, and the auth-gate
shape and touches nothing else.
This commit is contained in:
izumi0uu 2026-07-24 19:43:44 -05:00 committed by Booker
parent bb4765d21c
commit ccab46ca43
4 changed files with 32 additions and 0 deletions

View file

@ -31,6 +31,11 @@ the SPA should bootstrap it after login instead.
from __future__ import annotations
PUBLIC_API_PATHS: frozenset[str] = frozenset({
# Minimal process liveness probe for desktop/backend boot handshakes. It
# intentionally avoids gateway config, platform discovery, MCP setup, and
# host-local detail so readiness checks cannot spend their budget inside
# cold plugin imports.
"/api/health",
# Liveness probe target. Returns version, gateway state, active
# session count, and the dashboard auth-gate shape. No bodies, no
# session content, no secrets. Documented as the portal's wildcard

View file

@ -3036,6 +3036,16 @@ async def get_ssh_ownership(request: Request):
return {"ok": True, "sshOwnerNonce": _SSH_OWNER_NONCE, "protocolVersion": 1}
@app.get("/api/health")
async def get_health():
"""Lightweight process liveness for desktop/backend readiness probes."""
return {
"ok": True,
"version": __version__,
"auth_required": bool(getattr(app.state, "auth_required", False)),
}
@app.get("/api/status")
async def get_status(profile: Optional[str] = None):
status_scope = None

View file

@ -78,6 +78,7 @@ def test_gated_status_is_public(gated_app):
@pytest.mark.parametrize("path", [
"/api/health",
"/api/config/defaults",
"/api/config/schema",
"/api/model/info",

View file

@ -67,6 +67,22 @@ def test_status_reports_auth_required_in_gated_mode(gated_client):
assert body["auth_providers"] == ["stub"]
def test_health_reports_liveness_without_loading_gateway_config(gated_client, monkeypatch):
def _boom():
raise AssertionError("health must not load gateway config")
monkeypatch.setattr("gateway.config.load_gateway_config", _boom)
r = gated_client.get("/api/health")
assert r.status_code == 200
body = r.json()
assert body == {
"ok": True,
"version": web_server.__version__,
"auth_required": True,
}
def test_status_reports_auth_disabled_in_loopback_mode(loopback_client):
r = loopback_client.get("/api/status")
assert r.status_code == 200