From 3a2f50735311927033a01e32d92065091bc9fc38 Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Wed, 22 Jul 2026 11:41:46 -0700 Subject: [PATCH] Add /api/version capability endpoint for FE/BE version negotiation Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z --- tests/web/test_api.py | 8 ++++++++ web/app.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/tests/web/test_api.py b/tests/web/test_api.py index dbea466..762a33c 100644 --- a/tests/web/test_api.py +++ b/tests/web/test_api.py @@ -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 diff --git a/web/app.py b/web/app.py index b20d449..35f664e 100644 --- a/web/app.py +++ b/web/app.py @@ -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)."""