diff --git a/fire_planner/app.py b/fire_planner/app.py index 17190a0..4f8a769 100644 --- a/fire_planner/app.py +++ b/fire_planner/app.py @@ -166,17 +166,34 @@ class _SPAStaticFiles(StaticFiles): `StaticFiles(html=True)` only serves index.html for *directories*, not arbitrary not-found paths — that's not enough for a SPA. + + Cache policy: Vite hashes assets (e.g. ``index-XjyVM1-C.js``) so they + are immutable and can be cached forever. ``index.html`` references + those by hash — if the browser caches a stale ``index.html`` it'll + keep loading the OLD bundle. Force ``index.html`` (and any non-hashed + response, including SPA fallbacks) to revalidate on every request. """ async def get_response(self, path: str, scope: Scope): # type: ignore[no-untyped-def] try: - return await super().get_response(path, scope) + response = await super().get_response(path, scope) except StarletteHTTPException as exc: if exc.status_code == 404: index = Path(self.directory) / "index.html" # type: ignore[arg-type] if index.is_file(): - return FileResponse(index) + return FileResponse( + index, + headers={"Cache-Control": "no-cache, must-revalidate"}, + ) raise + # Hashed Vite assets live under /assets/ and contain an 8-char + # hash — safe to cache aggressively. index.html and anything + # else (logos, favicons) revalidate. + if path.startswith("assets/") and "-" in path: + response.headers["Cache-Control"] = "public, max-age=31536000, immutable" + else: + response.headers["Cache-Control"] = "no-cache, must-revalidate" + return response # Mount the SPA last so it catches any path the API didn't claim.