diff --git a/simulator/app.py b/simulator/app.py index df0f24a..0266abb 100644 --- a/simulator/app.py +++ b/simulator/app.py @@ -234,12 +234,18 @@ def create_app(manifest_path: Optional[Path] = None) -> FastAPI: return {"ok": True, "clip": merged} @app.middleware("http") - async def _no_cache(request, call_next): - # Dev preview server: never let a browser serve a stale app.js/style.css. - # The whole point is by-eye iteration, so a plain refresh must always get - # the latest assets (the heuristic cache otherwise hides JS/CSS edits). + async def _cache_policy(request, call_next): + # Dev preview server: never let a browser serve a stale app.js/style.css — + # by-eye iteration needs a plain refresh to always get the latest assets. + # Media is the exception: the clips are static and large, so they must be + # browser-cacheable. The renderer also preloads them into in-memory blob + # URLs (static/app.js), but a cacheable header keeps the preload fetch and + # any fallback off the network on repeat loads — instant altitude swaps. response = await call_next(request) - response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" + if request.url.path.startswith("/media/"): + response.headers["Cache-Control"] = "public, max-age=31536000, immutable" + else: + response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" return response if MEDIA_DIR.exists(): diff --git a/simulator/static/app.js b/simulator/static/app.js index c954652..51a6082 100644 --- a/simulator/static/app.js +++ b/simulator/static/app.js @@ -75,7 +75,79 @@ function activeClip() { return clipsById[activeClipId] || null; } -function mediaUrl(file) { return "/media/" + file; } +// In-memory preload cache: media file path -> blob object URL. Once a clip's bytes +// are cached, mediaUrl() hands the