Add /api/version capability endpoint for FE/BE version negotiation
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
This commit is contained in:
parent
8eb3ffbb6b
commit
3a2f507353
2 changed files with 24 additions and 0 deletions
|
|
@ -58,6 +58,14 @@ def test_grade_is_aliased_across_api_versions(client):
|
||||||
def test_query_validation_rejects_out_of_range(client):
|
def test_query_validation_rejects_out_of_range(client):
|
||||||
assert client.get("/thermograph/api/v2/grade",
|
assert client.get("/thermograph/api/v2/grade",
|
||||||
params={"lat": 999, "lon": 0}).status_code == 422
|
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",
|
assert client.get("/thermograph/api/v2/grade",
|
||||||
params={**Q, "days": 0}).status_code == 422
|
params={**Q, "days": 0}).status_code == 422
|
||||||
|
|
||||||
|
|
|
||||||
16
web/app.py
16
web/app.py
|
|
@ -212,6 +212,22 @@ def healthz():
|
||||||
return {"status": "ok", "role": ROLE}
|
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":
|
def _client_ip(request) -> "str | None":
|
||||||
"""The real client IP: the left-most X-Forwarded-For hop when a proxy fronts us
|
"""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)."""
|
(Caddy on prod sets it), otherwise the direct peer address (LAN dev)."""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue