chart/main.py
Chris Amow e69d1c190f Version static assets so a fixed bug stops reproducing in an open tab
The trendline slope fix was already correct — measured on the live chart, the
two rendered segments came out at screen slopes 0.4910 and 0.4903, collinear.
It still reproduced in the browser because the browser was not running it.

StaticFiles sends an ETag but no Cache-Control, and the asset URLs carried no
version, so nothing forced a refetch. A tab left open across an edit never
fetches at all: it keeps executing the JavaScript it loaded when the page was
first opened. Every fix since that tab was opened was invisible in it.

The page now stamps its own asset URLs with a digest of their contents and is
itself served no-store. Hashing rather than stamping mtimes, because a deploy
checks every file out fresh and would otherwise invalidate assets that never
changed.

This also removes a trap the README already half-documented for deploys: an
/api-only change leaves the HTML byte-identical, and until now a JavaScript
change could leave the served page byte-identical too.

The debug handle used to measure the geometry is kept deliberately. Chart
rendering bugs are invisible from the outside, and window.__chart.lineData()
against timeToCoordinate() is what settled this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 04:29:56 -05:00

70 lines
2.1 KiB
Python

"""chart.amow.com FastAPI backend."""
import asyncio
from contextlib import asynccontextmanager
from pathlib import Path
import re
from hashlib import sha256
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from app.api.meta import router as meta_router
from app.api.routes import router as api_router
from app.api.ws import router as ws_router
from app.config import Settings
from app.runtime import Runtime
BASE_DIR = Path(__file__).parent
STATIC_DIR = BASE_DIR / "static"
@asynccontextmanager
async def lifespan(app: FastAPI):
runtime = Runtime(Settings())
app.state.runtime = runtime
task = await runtime.start()
yield
runtime.stream.stop()
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
app = FastAPI(title="chart", lifespan=lifespan)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
app.include_router(meta_router)
app.include_router(api_router)
app.include_router(ws_router)
ASSET_REF = re.compile(r'((?:src|href)="/static/[^"?]+)"')
def asset_version() -> str:
"""A digest of the served assets, so the URL changes iff the content does.
StaticFiles sends an ETag but no Cache-Control, so a browser is free to keep
using the copy it already has — and a tab left open simply never fetches
again. That turned a fixed bug into a bug that still reproduced, because the
page was running the JavaScript it had loaded hours earlier.
Hashing rather than stamping mtimes: a deploy checks every file out fresh,
which would otherwise invalidate assets that never changed.
"""
digest = sha256()
for path in sorted(STATIC_DIR.glob("*.*")):
digest.update(path.read_bytes())
return digest.hexdigest()[:12]
@app.get("/")
def index():
html = (STATIC_DIR / "index.html").read_text(encoding="utf-8")
html = ASSET_REF.sub(rf'\1?v={asset_version()}"', html)
# The document itself must never be cached, or the versioned URLs inside it
# are the stale thing instead.
return HTMLResponse(html, headers={"Cache-Control": "no-store"})