mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-27 02:31:09 +00:00
Bonsai: cache-bust webui static assets so shipped JS/CSS changes reach users
Browsers were caching /static/js and /static/css for the standalone webui (costing, gantt, drawings, index, demo pages) indefinitely, so a shipped JS fix (e.g. the Download CSV button) would only reach a user after a manual hard refresh. Two changes, applied consistently across all five webui pages. 1. Every locally served link/script tag in the pystache templates now carries a ?v=<bonsai version> query string, falling back to a static asset mtime hash when BONSAI_VERSION isn't set (e.g. running sioserver.py standalone). Since get_bonsai_version() includes the build's commit hash, the token changes on every shipped update. 2. Responses under /static/ and /jsgantt/ now carry Cache-Control: no-cache, must-revalidate. This covers what query stamping alone can't reach: cost.js and gantt.js statically import utilities/costui.js by a fixed relative path with no query string, so that nested module still needed server side revalidation to pick up changes. Verified against a live aiohttp instance of sioserver.py: rendered HTML for all five routes shows the stamped URLs, and the token changes when BONSAI_VERSION changes between two server runs. A conditional GET against a static file with a stale If-Modified-Since header confirms the cheap 304 revalidation path still works. Also used this instance plus a real headless Chromium (Playwright) to click test the previously untested Download CSV button on the costing page. The ribbon renders it correctly, and clicking it (with a synthetic cost-items table injected into the DOM to stand in for a connected Blender's data) triggers a real Blob download with the correct filename and CSV content. No bug found, the button works as intended. AI-generated with Claude Code.
This commit is contained in:
committed by
Massimo Fabbro
parent
1df738d968
commit
e759135608
@@ -10,6 +10,7 @@ if bonsai_lib_path:
|
|||||||
import argparse
|
import argparse
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
import urllib.parse
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
import pystache
|
import pystache
|
||||||
@@ -18,8 +19,53 @@ from aiohttp import web
|
|||||||
|
|
||||||
sio_port = 8080 # default port
|
sio_port = 8080 # default port
|
||||||
|
|
||||||
|
|
||||||
|
def get_asset_version() -> str:
|
||||||
|
"""A cache-busting token appended to locally served static asset URLs.
|
||||||
|
|
||||||
|
Browsers otherwise keep serving a stale cached copy of static/js and
|
||||||
|
static/css after Bonsai ships a code change, until the user does a hard
|
||||||
|
refresh. Using the Bonsai version (which includes the build's commit
|
||||||
|
hash) means the token changes on every shipped update.
|
||||||
|
"""
|
||||||
|
if bonsai_version:
|
||||||
|
return urllib.parse.quote(bonsai_version, safe="")
|
||||||
|
# Fallback for standalone runs without BONSAI_VERSION set (e.g. running
|
||||||
|
# sioserver.py directly outside of Blender): derive a token from the
|
||||||
|
# newest mtime among the static assets, so it still changes whenever the
|
||||||
|
# shipped files change.
|
||||||
|
static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
||||||
|
latest_mtime = 0
|
||||||
|
for root, _dirs, files in os.walk(static_dir):
|
||||||
|
for name in files:
|
||||||
|
latest_mtime = max(latest_mtime, int(os.path.getmtime(os.path.join(root, name))))
|
||||||
|
return f"dev-{latest_mtime}"
|
||||||
|
|
||||||
|
|
||||||
|
asset_version = get_asset_version()
|
||||||
|
|
||||||
|
|
||||||
|
@web.middleware
|
||||||
|
async def no_cache_static_middleware(request: web.Request, handler):
|
||||||
|
"""Force revalidation of locally served static assets.
|
||||||
|
|
||||||
|
Query-string version stamping (see `asset_version`) busts the cache for
|
||||||
|
the HTML-referenced entry points, but JS files that statically import
|
||||||
|
other local modules (e.g. cost.js/gantt.js importing utilities/costui.js)
|
||||||
|
reference those modules by an un-stamped relative path. Marking all
|
||||||
|
/static/ and /jsgantt/ responses as no-cache makes browsers always
|
||||||
|
revalidate with the server (a cheap conditional GET / 304 when nothing
|
||||||
|
changed), so nested imports also pick up shipped changes without
|
||||||
|
requiring a hard refresh.
|
||||||
|
"""
|
||||||
|
response = await handler(request)
|
||||||
|
if request.path.startswith("/static/") or request.path.startswith("/jsgantt/"):
|
||||||
|
response.headers["Cache-Control"] = "no-cache, must-revalidate"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
sio = socketio.AsyncServer(cors_allowed_origins="*", async_mode="aiohttp", max_http_buffer_size=10000000)
|
sio = socketio.AsyncServer(cors_allowed_origins="*", async_mode="aiohttp", max_http_buffer_size=10000000)
|
||||||
app = web.Application()
|
app = web.Application(middlewares=[no_cache_static_middleware])
|
||||||
sio.attach(app)
|
sio.attach(app)
|
||||||
|
|
||||||
|
|
||||||
@@ -199,28 +245,28 @@ class BlenderNamespace(socketio.AsyncNamespace):
|
|||||||
async def schedules(request):
|
async def schedules(request):
|
||||||
with open("templates/index.html", "r") as f:
|
with open("templates/index.html", "r") as f:
|
||||||
template = f.read()
|
template = f.read()
|
||||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||||
return web.Response(text=html_content, content_type="text/html")
|
return web.Response(text=html_content, content_type="text/html")
|
||||||
|
|
||||||
|
|
||||||
async def costing(request):
|
async def costing(request):
|
||||||
with open("templates/costing.html", "r") as f:
|
with open("templates/costing.html", "r") as f:
|
||||||
template = f.read()
|
template = f.read()
|
||||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||||
return web.Response(text=html_content, content_type="text/html")
|
return web.Response(text=html_content, content_type="text/html")
|
||||||
|
|
||||||
|
|
||||||
async def sequencing(request):
|
async def sequencing(request):
|
||||||
with open("templates/gantt.html", "r") as f:
|
with open("templates/gantt.html", "r") as f:
|
||||||
template = f.read()
|
template = f.read()
|
||||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||||
return web.Response(text=html_content, content_type="text/html")
|
return web.Response(text=html_content, content_type="text/html")
|
||||||
|
|
||||||
|
|
||||||
async def documentation(request):
|
async def documentation(request):
|
||||||
with open("templates/drawings.html", "r") as f:
|
with open("templates/drawings.html", "r") as f:
|
||||||
template = f.read()
|
template = f.read()
|
||||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||||
return web.Response(text=html_content, content_type="text/html")
|
return web.Response(text=html_content, content_type="text/html")
|
||||||
|
|
||||||
|
|
||||||
@@ -229,7 +275,7 @@ async def documentation(request):
|
|||||||
async def demo(request):
|
async def demo(request):
|
||||||
with open("templates/demo.html", "r") as f:
|
with open("templates/demo.html", "r") as f:
|
||||||
template = f.read()
|
template = f.read()
|
||||||
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
|
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version, "v": asset_version})
|
||||||
return web.Response(text=html_content, content_type="text/html")
|
return web.Response(text=html_content, content_type="text/html")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
type="text/css"
|
type="text/css"
|
||||||
href="/static/css/cost.css"
|
href="/static/css/cost.css?v={{v}}"
|
||||||
id="index-stylesheet"
|
id="index-stylesheet"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
/>
|
/>
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="/static/js/jquery.min.js"
|
src="/static/js/jquery.min.js?v={{v}}"
|
||||||
></script>
|
></script>
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<script>
|
<script>
|
||||||
var SOCKET_PORT = {{port}};
|
var SOCKET_PORT = {{port}};
|
||||||
</script>
|
</script>
|
||||||
<script type="module" defer src="./static/js/cost.js"></script>
|
<script type="module" defer src="./static/js/cost.js?v={{v}}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
|
|||||||
@@ -12,14 +12,14 @@
|
|||||||
/>
|
/>
|
||||||
<!-- here we request the CSS file from the server, -->
|
<!-- here we request the CSS file from the server, -->
|
||||||
<!-- using registered static path in the server -->
|
<!-- using registered static path in the server -->
|
||||||
<link rel="stylesheet" href="/static/css/demo.css" id="demo-stylesheet" />
|
<link rel="stylesheet" href="/static/css/demo.css?v={{v}}" id="demo-stylesheet" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="/static/js/jquery.min.js"
|
src="/static/js/jquery.min.js?v={{v}}"
|
||||||
></script>
|
></script>
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</script>
|
</script>
|
||||||
<!-- here we request the JS file from the server-->
|
<!-- here we request the JS file from the server-->
|
||||||
<!-- using registered static path in the server -->
|
<!-- using registered static path in the server -->
|
||||||
<script defer src="./static/js/demo.js"></script>
|
<script defer src="./static/js/demo.js?v={{v}}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- the navigation bar at the top of the page. -->
|
<!-- the navigation bar at the top of the page. -->
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="/static/css/drawings.css"
|
href="/static/css/drawings.css?v={{v}}"
|
||||||
id="drawings-stylesheet"
|
id="drawings-stylesheet"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
/>
|
/>
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="/static/js/jquery.min.js"
|
src="/static/js/jquery.min.js?v={{v}}"
|
||||||
></script>
|
></script>
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<script>
|
<script>
|
||||||
var SOCKET_PORT = {{port}};
|
var SOCKET_PORT = {{port}};
|
||||||
</script>
|
</script>
|
||||||
<script defer src="./static/js/drawings.js"></script>
|
<script defer src="./static/js/drawings.js?v={{v}}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
|
|||||||
@@ -9,25 +9,25 @@
|
|||||||
type="image/x-icon"
|
type="image/x-icon"
|
||||||
href="https://bonsaibim.org/assets/images/favicon-blender.png"
|
href="https://bonsaibim.org/assets/images/favicon-blender.png"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css" />
|
<link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css?v={{v}}" />
|
||||||
<link rel="stylesheet" href="/static/css/gantt.css" id="gantt-stylesheet" />
|
<link rel="stylesheet" href="/static/css/gantt.css?v={{v}}" id="gantt-stylesheet" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="/static/js/jquery.min.js"
|
src="/static/js/jquery.min.js?v={{v}}"
|
||||||
></script>
|
></script>
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="https://cdn.socket.io/4.0.0/socket.io.min.js"
|
src="https://cdn.socket.io/4.0.0/socket.io.min.js"
|
||||||
></script>
|
></script>
|
||||||
<script type="text/javascript" src="./jsgantt/jsgantt.js"></script>
|
<script type="text/javascript" src="./jsgantt/jsgantt.js?v={{v}}"></script>
|
||||||
<script>
|
<script>
|
||||||
var SOCKET_PORT = {{port}};
|
var SOCKET_PORT = {{port}};
|
||||||
</script>
|
</script>
|
||||||
<script type="module" defer src="./static/js/gantt.js"></script>
|
<script type="module" defer src="./static/js/gantt.js?v={{v}}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="no-print">
|
<nav class="no-print">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
type="image/x-icon"
|
type="image/x-icon"
|
||||||
href="https://bonsaibim.org/assets/images/favicon-blender.png"
|
href="https://bonsaibim.org/assets/images/favicon-blender.png"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="/static/css/index.css" id="index-stylesheet" />
|
<link rel="stylesheet" href="/static/css/index.css?v={{v}}" id="index-stylesheet" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
id="tabulator-stylesheet"
|
id="tabulator-stylesheet"
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
/>
|
/>
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="/static/js/jquery.min.js"
|
src="/static/js/jquery.min.js?v={{v}}"
|
||||||
></script>
|
></script>
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<script>
|
<script>
|
||||||
var SOCKET_PORT = {{port}};
|
var SOCKET_PORT = {{port}};
|
||||||
</script>
|
</script>
|
||||||
<script defer src="./static/js/index.js"></script>
|
<script defer src="./static/js/index.js?v={{v}}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
|
|||||||
Reference in New Issue
Block a user