Add /api/version capability endpoint for FE/BE version negotiation

Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
This commit is contained in:
Emi Griffith 2026-07-22 11:41:46 -07:00
parent 8eb3ffbb6b
commit 3a2f507353
2 changed files with 24 additions and 0 deletions

View file

@ -58,6 +58,14 @@ def test_grade_is_aliased_across_api_versions(client):
def test_query_validation_rejects_out_of_range(client):
assert client.get("/thermograph/api/v2/grade",
params={"lat": 999, "lon": 0}).status_code == 422
def test_api_version_reports_backend_contract(client):
r = client.get("/thermograph/api/version")
assert r.status_code == 200
body = r.json()
assert {"backend_version", "min_frontend", "payload_ver"} <= body.keys()
assert body["backend_version"] == "2"
assert client.get("/thermograph/api/v2/grade",
params={**Q, "days": 0}).status_code == 422

View file

@ -212,6 +212,22 @@ def healthz():
return {"status": "ok", "role": ROLE}
API_CONTRACT_VERSION = "2" # bump only on a breaking change to any /api/v{N} route or payload shape
MIN_SUPPORTED_FRONTEND = "1" # oldest frontend contract version this backend still serves correctly
@app.get(f"{BASE}/api/version")
def api_version():
"""Capability/version-negotiation endpoint: lets a split-out frontend detect at
boot whether the backend it's talking to still supports its contract.
Deliberately does no I/O, same posture as /healthz."""
return {
"backend_version": API_CONTRACT_VERSION,
"min_frontend": MIN_SUPPORTED_FRONTEND,
"payload_ver": payloads.PAYLOAD_VER,
}
def _client_ip(request) -> "str | None":
"""The real client IP: the left-most X-Forwarded-For hop when a proxy fronts us
(Caddy on prod sets it), otherwise the direct peer address (LAN dev)."""