fix: asset URLs carry a content-based build id so edited scripts are never served stale
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
3b5e20a4c8
commit
0136842258
@ -50,6 +50,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
- **The page thumbnail is the Review button.** In the thumbnail grid, clicking the (big) page image opens the page in review; the separate *Review* / *Review this page* buttons are gone. A page shared by two or more articles keeps one labelled *Review* button per article, so it stays clear which is meant.
|
||||
|
||||
### Fixed
|
||||
- **Changed scripts and stylesheets now reach the browser immediately.** They were requested under a version that only moved at release time (`…js?v=1.53.4`) and cached as immutable for a year, so a browser or proxy could keep running old code after an update (a button added to a script simply did not appear). The app shell now requests every asset under `<version>-<build id>`, where the build id follows the newest script/stylesheet/manifest change.
|
||||
- **An approved page is no longer purple.** A page the quality check flagged stayed purple even after you approved its article; now your approval wins and the page shows its review colour (the warning badge on the card still lists what the check found). Purple means flagged *and* nobody has approved anything on the page. An old MP3 on an article nobody approved (left over from before pages were linked) no longer turns a page green either: audio only counts for approved articles.
|
||||
- **Reading order is now applied automatically after import, *OCR again* and every per-page fix**, not only by the manual *Fix reading order* button. Until now the order after import was whatever the vision/LLM step returned, so a column's short last box could be read after the whole other column; on the measured issue 101 of 204 pages had a saved order that differs from the column-aware order. Pages whose order you fixed by hand stay untouched, except after *OCR again*, when every region is new and a hand order can no longer apply.
|
||||
- **The review list shows each region's whole text**, not the first 220 characters, so every misspelling gets its red wavy underline (like a word processor). The engine's `[Non-Text]` marker and hairline boxes (under 3 pt tall) are now treated as OCR noise by *Clean up boxes* and after a page re-OCR, so they no longer show up as near-invisible regions. Fitted boxes keep a little air on grown sides.
|
||||
|
||||
@ -7,7 +7,7 @@ import re
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException
|
||||
from fastapi.responses import FileResponse, PlainTextResponse, Response
|
||||
from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse, Response
|
||||
|
||||
from core.constants import STATIC_DIR, __version__
|
||||
|
||||
@ -78,12 +78,34 @@ def _mount_relation(path_str: str, mounts: frozenset[str]) -> tuple[bool, bool]:
|
||||
return is_persisted, leads_to
|
||||
|
||||
|
||||
_ASSET_URL_RE = re.compile(r"(/static/[^\"'\s?]+)\?v=[^\"'\s&]+")
|
||||
|
||||
|
||||
def asset_build_id() -> str:
|
||||
"""A version string that changes whenever any script, stylesheet or module list changes.
|
||||
|
||||
Scripts and stylesheets are requested as ``file?v=<version>`` and cached for a year as
|
||||
immutable, so a version that only moves at release time leaves a browser (or a proxy in front
|
||||
of it) running old code after an edit -- a button added to a script simply does not appear.
|
||||
The newest modification time under static/ js, css and the manifest is folded into the version.
|
||||
"""
|
||||
newest = 0
|
||||
for pattern in ("js/*.js", "css/*.css", "*.css", "loader.js", "nav.js", "module-manifest.json"):
|
||||
for path in STATIC_DIR.glob(pattern):
|
||||
try:
|
||||
newest = max(newest, int(path.stat().st_mtime))
|
||||
except OSError:
|
||||
pass
|
||||
return f"{__version__}-{newest:x}"
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def index():
|
||||
return FileResponse(
|
||||
STATIC_DIR / "index.html",
|
||||
headers={"Cache-Control": "no-store, max-age=0"},
|
||||
)
|
||||
"""The app shell, with every /static asset URL carrying the current build id."""
|
||||
html = (STATIC_DIR / "index.html").read_text("utf-8")
|
||||
build = asset_build_id()
|
||||
return HTMLResponse(_ASSET_URL_RE.sub(lambda m: f"{m.group(1)}?v={build}", html),
|
||||
headers={"Cache-Control": "no-store, max-age=0"})
|
||||
|
||||
|
||||
@router.get("/favicon.ico")
|
||||
|
||||
39
tests/test_asset_versions.py
Normal file
39
tests/test_asset_versions.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""The app shell must request scripts and stylesheets under a version that follows their content."""
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("CONFIG_DIR", tempfile.mkdtemp(prefix="ttvc-assetv-"))
|
||||
|
||||
|
||||
class AssetBuildIdTests(unittest.TestCase):
|
||||
def test_the_index_rewrites_every_static_asset_version(self):
|
||||
import asyncio
|
||||
from routes.admin import asset_build_id, index
|
||||
response = asyncio.run(index())
|
||||
html = response.body.decode("utf-8")
|
||||
build = asset_build_id()
|
||||
self.assertIn(f"/static/style.css?v={build}", html)
|
||||
self.assertIn(f"/static/loader.js?v={build}", html)
|
||||
self.assertNotIn("?v=1.53.4", html)
|
||||
self.assertEqual(response.headers["cache-control"], "no-store, max-age=0")
|
||||
|
||||
def test_the_build_id_changes_when_a_script_changes_and_not_otherwise(self):
|
||||
from core.constants import STATIC_DIR
|
||||
from routes.admin import asset_build_id
|
||||
first = asset_build_id()
|
||||
self.assertEqual(first, asset_build_id())
|
||||
probe = Path(STATIC_DIR) / "js" / "magazine.js"
|
||||
original = probe.stat().st_mtime
|
||||
try:
|
||||
os.utime(probe, (original + 1_000_000, original + 1_000_000))
|
||||
self.assertNotEqual(first, asset_build_id())
|
||||
finally:
|
||||
os.utime(probe, (original, original))
|
||||
self.assertEqual(first, asset_build_id())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in New Issue
Block a user