feat(ui): version-busting query suffix on all served asset URLs
Browser-tester confirmed on spark-1 that the 7 <script src> tags and all
<link href> tags in the served HTML had no cache-busting suffix. Because
there is no service worker, the standard HTTP cache would keep serving stale
JS/CSS to browsers that had already loaded a previous release — the root
cause of yesterday's 'is the user seeing stale JS?' investigation.
Fix: index_page() now appends ?v=<muxplex_version> to every static-asset
URL (src= and href= attributes whose path starts with / and does not begin
with /api/) before returning the HTML response. The version is read once at
module load from importlib.metadata.version('muxplex') — the same source
used by the doctor command — so it is always in sync with the installed
package. Starlette's StaticFiles handler ignores query parameters when
serving files from disk, so the versioned URLs continue to resolve to the
same bytes; they just carry a new cache key on every release.
Affected assets (7 <script> + 6 <link> + 1 <img>):
/vendor/xterm.js, /vendor/xterm-addon-fit.js,
/vendor/xterm-addon-web-links.js, /vendor/xterm-addon-search.js,
/vendor/addon-image.js, /app.js, /terminal.js
/manifest.json, /favicon.ico, /favicon-32.png, /apple-touch-icon.png,
/vendor/xterm.css, /style.css
/wordmark-on-dark.svg
New tests in muxplex/tests/test_main.py:
- test_index_all_asset_urls_have_version_suffix — every <script src> and
<link href> in GET / carries ?v=<version>
- test_index_vendor_scripts_each_versioned — all 7 expected script URLs
are present with the version suffix
- test_versioned_asset_url_resolves_to_static_file — static handler
serves the file correctly when the query string is present
This commit is contained in:
+21
-1
@@ -17,6 +17,7 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import pwd
|
||||
import re
|
||||
import socket
|
||||
import ssl
|
||||
import shutil
|
||||
@@ -527,6 +528,15 @@ _FRONTEND_DIR = pathlib.Path(__file__).parent / "frontend"
|
||||
# which machine each muxplex instance is running on.
|
||||
_HOSTNAME = socket.gethostname().split(".")[0]
|
||||
|
||||
# Canonical version string — sourced from package metadata (same as `app.version`
|
||||
# and the `doctor` command). Used to append `?v=<version>` to every static-asset
|
||||
# URL so browsers immediately pick up new code on each release.
|
||||
_UI_VERSION: str = importlib.metadata.version("muxplex")
|
||||
|
||||
# Matches src="/<path>" and href="/<path>" in served HTML, excluding /api/ URLs.
|
||||
# Used by index_page() to inject cache-busting version query parameters.
|
||||
_ASSET_URL_RE = re.compile(r'((?:src|href)=")((?!/api/)/[^"?#]*)')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routes
|
||||
@@ -1204,12 +1214,22 @@ async def federation_terminal_ws_proxy(websocket: WebSocket, device_id: str) ->
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
@app.get("/index.html", response_class=HTMLResponse)
|
||||
async def index_page():
|
||||
"""Serve index.html with hostname injected into the page title."""
|
||||
"""Serve index.html with hostname injected into the page title.
|
||||
|
||||
Also appends ``?v=<version>`` to every static-asset URL (script src, link
|
||||
href) so browsers immediately pick up new code on each release rather than
|
||||
serving stale JS/CSS from the HTTP cache. API URLs (/api/...) are
|
||||
excluded — they are not HTTP-cached by browsers.
|
||||
"""
|
||||
html = (_FRONTEND_DIR / "index.html").read_text()
|
||||
html = html.replace(
|
||||
"<title>muxplex</title>",
|
||||
f"<title>{_HOSTNAME} \u2014 muxplex</title>",
|
||||
)
|
||||
html = _ASSET_URL_RE.sub(
|
||||
lambda m: f"{m.group(1)}{m.group(2)}?v={_UI_VERSION}",
|
||||
html,
|
||||
)
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user