feat: PWA update strategy with hashed assets and cache control
Move frontend sources to frontend/, build content-hashed assets at deploy, network-first SW for shell/JS, immutable cache for /assets/, version.json update banner, and Cache-Control middleware. Docs in docs/PWA-STRATEGY.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,6 +20,7 @@ from server.push import save_subscription, send_slot_reminder
|
||||
from server.scheduler import schedule_snooze, start_scheduler
|
||||
from server.settings import settings
|
||||
from server.slots import build_today
|
||||
from server.static_cache import CacheControlMiddleware
|
||||
from server.stats import build_history, check_milestones, compute_stats, MILESTONE_DEFS
|
||||
|
||||
NO_STORE = {"Cache-Control": "no-store"}
|
||||
@@ -63,6 +64,7 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
|
||||
app = FastAPI(title="TakeYourMeds", version=settings.app_version, lifespan=lifespan)
|
||||
app.add_middleware(CacheControlMiddleware)
|
||||
|
||||
|
||||
@app.post("/api/auth/pin")
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ class Settings(BaseSettings):
|
||||
messages_path: str = "messages.yaml"
|
||||
|
||||
public_dir: str = "public"
|
||||
app_version: str = "1.0.0"
|
||||
app_version: str = "1.1.0"
|
||||
|
||||
|
||||
settings = Settings() # type: ignore[call-arg]
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||
|
||||
NO_CACHE = b"no-cache, must-revalidate"
|
||||
NO_STORE = b"no-store"
|
||||
IMMUTABLE = b"public, max-age=31536000, immutable"
|
||||
|
||||
|
||||
def _cache_control_for_path(path: str) -> bytes | None:
|
||||
if path in ("/sw.js", "/index.html", "/manifest.json"):
|
||||
return NO_CACHE
|
||||
if path == "/version.json":
|
||||
return NO_STORE
|
||||
if path.startswith("/assets/"):
|
||||
return IMMUTABLE
|
||||
if path.endswith((".js", ".css")) and not path.startswith("/assets/"):
|
||||
return NO_CACHE
|
||||
return None
|
||||
|
||||
|
||||
class CacheControlMiddleware:
|
||||
def __init__(self, app: ASGIApp) -> None:
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
if scope["type"] != "http":
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
path = scope.get("path", "")
|
||||
cache_control = _cache_control_for_path(path)
|
||||
|
||||
async def send_wrapper(message: dict) -> None:
|
||||
if message["type"] == "http.response.start" and cache_control is not None:
|
||||
headers = list(message.get("headers", []))
|
||||
headers = [(k, v) for k, v in headers if k.lower() != b"cache-control"]
|
||||
headers.append((b"cache-control", cache_control))
|
||||
message = {**message, "headers": headers}
|
||||
await send(message)
|
||||
|
||||
await self.app(scope, receive, send_wrapper)
|
||||
Reference in New Issue
Block a user