From 820cb9b8076b69d2e691e801077082fab388bf16 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 07:05:48 +0000 Subject: [PATCH 1/2] Dashboard: revalidate JS/CSS on every load instead of caching for an hour The asset URLs are not versioned, so Cache-Control: public, max-age=3600 kept browsers (and CDNs in front) on the previous release's app.js after an upgrade - e.g. the login page showed no SSO button although the server reported SSO as enabled. Serve everything no-cache; unchanged files cost an ETag 304. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018Rdb1HFXbXk2xeKrJkzdvb --- server/main.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/server/main.py b/server/main.py index 777e11b..d95ebd9 100644 --- a/server/main.py +++ b/server/main.py @@ -146,26 +146,17 @@ def get_server_time(): class DashboardStaticFiles(StaticFiles): """StaticFiles with explicit Cache-Control headers. - Starlette sends only ETag/Last-Modified, so every dashboard load - re-validates all ~9 assets with conditional requests. The HTML shell stays - no-cache (a deploy shows up on the next load); the JS/CSS assets get a - modest max-age so repeat loads within the hour skip the network entirely, - falling back to ETag re-validation once it expires. + Everything is no-cache: the browser keeps its copy but re-validates it + with the ETag on each load, so an unchanged file costs a 304 and a deploy + shows up on the next load. The asset URLs are not versioned, so a max-age + here kept serving the previous release's JS (e.g. a login page without the + SSO button) for up to an hour after an upgrade — longer behind a CDN. """ async def get_response(self, path: str, scope): response = await super().get_response(path, scope) if response.status_code < 400: - # The HTML shell, the service worker and the manifest must revalidate - # every load so a deploy (or a changed SW) is picked up promptly; the - # hashed-in-practice JS/CSS assets get a modest max-age. - if ( - path in ("", ".", "index.html", "sw.js", "manifest.webmanifest") - or path.endswith(".html") - ): - response.headers["Cache-Control"] = "no-cache" - else: - response.headers["Cache-Control"] = "public, max-age=3600" + response.headers["Cache-Control"] = "no-cache" return response From 67e6be00a4e4b6a04bb371ccd89061ed3310caba Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 07:09:31 +0000 Subject: [PATCH 2/2] OIDC: send a HiveHub User-Agent to the identity provider urllib's default Python-urllib/3.x is rejected by Cloudflare's Browser Integrity Check (error 1010), so SSO could not even fetch the discovery document when authentik sits behind Cloudflare. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018Rdb1HFXbXk2xeKrJkzdvb --- server/oidc.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/server/oidc.py b/server/oidc.py index 4fbf45b..f68d929 100644 --- a/server/oidc.py +++ b/server/oidc.py @@ -48,6 +48,7 @@ OIDC_REQUIRE_EMAIL_VERIFIED, OIDC_SCOPES, PUBLIC_BASE_URL, + SERVER_VERSION, ) logger = logging.getLogger("hivescale.oidc") @@ -124,9 +125,18 @@ def post_logout_redirect_uri(request: Request) -> str: # ── provider HTTP ──────────────────────────────────────────────────────────── +# urllib's default "Python-urllib/3.x" is blocked by Cloudflare's Browser +# Integrity Check (error 1010) when the provider sits behind Cloudflare. +_USER_AGENT = f"HiveHub/{SERVER_VERSION} (OpenID Connect client)" + + def _http_json(url: str, data: Optional[dict] = None, headers: Optional[dict] = None) -> dict: body = urllib.parse.urlencode(data).encode() if data is not None else None - req = urllib.request.Request(url, data=body, headers={"Accept": "application/json", **(headers or {})}) + req = urllib.request.Request( + url, + data=body, + headers={"Accept": "application/json", "User-Agent": _USER_AGENT, **(headers or {})}, + ) try: with urllib.request.urlopen(req, timeout=OIDC_HTTP_TIMEOUT_SECONDS) as resp: return json.loads(resp.read().decode("utf-8"))