From 194bf64ebb650bf42d42206f224119acde1bc60c Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Mon, 8 Jun 2026 22:47:44 +0300 Subject: [PATCH] fix: serve admin bundle as hashed immutable assets The lazy-loaded admin CSS/JS resolved to bare runtime names served no-store, the same scheme that left the main bundle vulnerable to stale CSS in iOS WebViews after a deploy. The original reason for keeping them bare (hashed admin files could 404 when nginx fronts aiohttp) no longer holds: the backend image now carries the same deterministically hashed assets nginx serves, and the App.svelte loader already falls back to the bare name if a hashed asset ever 404s. Resolve the admin assets through the same hashed/version-stable path as the main bundle so they are emitted as immutable, cache-busting URLs. Also drop the inert tags from the shell: browsers ignore http-equiv caching directives for the document and use the real HTTP headers, which are already set. --- .../web/templates/subscription_webapp.html | 3 -- backend/bot/app/web/webapp/assets.py | 21 +++++++++----- tests/test_webapp_assets.py | 28 +++++++++++++++++-- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/backend/bot/app/web/templates/subscription_webapp.html b/backend/bot/app/web/templates/subscription_webapp.html index 6a61a37..00f81ff 100644 --- a/backend/bot/app/web/templates/subscription_webapp.html +++ b/backend/bot/app/web/templates/subscription_webapp.html @@ -6,9 +6,6 @@ name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" /> - - - diff --git a/backend/bot/app/web/webapp/assets.py b/backend/bot/app/web/webapp/assets.py index a484470..df44f7e 100644 --- a/backend/bot/app/web/webapp/assets.py +++ b/backend/bot/app/web/webapp/assets.py @@ -1454,10 +1454,15 @@ def _resolve_webapp_js_asset_name() -> str: def _resolve_webapp_admin_js_asset_name() -> str: - # The admin bundle is lazy-loaded from the already running Mini App. In - # deployments where nginx serves static files in front of aiohttp, stale - # hashed admin filenames can 404 even though the runtime build asset exists. - return _set_cached_asset_name("admin-js", "subscription_webapp_admin.js") + # The admin bundle is lazy-loaded from the already running Mini App. It now + # ships content-hashed alongside the main bundle (same build, deterministic + # hashes, served immutable), so iOS WebViews fetch fresh admin assets on every + # deploy. The App.svelte loader falls back to the bare runtime build name if a + # hashed asset ever 404s. + return _resolve_hashed_js_asset_name( + kind="admin-js", + base_name="subscription_webapp_admin", + ) def _resolve_hashed_js_asset_name(*, kind: str, base_name: str) -> str: @@ -1487,9 +1492,11 @@ def _resolve_webapp_css_asset_name() -> str: def _resolve_webapp_admin_css_asset_name() -> str: - # Keep the lazy-loaded admin stylesheet on the stable build filename for - # the same reason as the JS bundle above. - return _set_cached_asset_name("admin-css", "subscription_webapp_admin.css") + # Content-hashed and immutable, same rationale as the admin JS bundle above. + return _resolve_hashed_css_asset_name( + kind="admin-css", + base_name="subscription_webapp_admin", + ) def _resolve_hashed_css_asset_name(*, kind: str, base_name: str) -> str: diff --git a/tests/test_webapp_assets.py b/tests/test_webapp_assets.py index d147192..1179bb5 100644 --- a/tests/test_webapp_assets.py +++ b/tests/test_webapp_assets.py @@ -1009,7 +1009,7 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase): r"^subscription_webapp\.css\?v=[0-9a-f]{8}$", ) - def test_resolve_webapp_admin_asset_names_use_stable_runtime_builds(self): + def test_resolve_webapp_admin_asset_names_prefer_latest_hashed_builds(self): with tempfile.TemporaryDirectory() as tmpdir: asset_dir = Path(tmpdir) (asset_dir / "subscription_webapp_admin.js").write_text( @@ -1032,13 +1032,35 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase): os.utime(new_css, (2, 2)) with patch.object(webapp_assets, "ASSET_DIR", asset_dir): + webapp_assets._ASSET_NAME_CACHE.clear() self.assertEqual( subscription_webapp._resolve_webapp_admin_js_asset_name(), - "subscription_webapp_admin.js", + "subscription_webapp_admin.min.22222222.js", ) self.assertEqual( subscription_webapp._resolve_webapp_admin_css_asset_name(), - "subscription_webapp_admin.css", + "subscription_webapp_admin.22222222.css", + ) + + def test_resolve_webapp_admin_asset_names_fall_back_to_runtime_builds(self): + with tempfile.TemporaryDirectory() as tmpdir: + asset_dir = Path(tmpdir) + (asset_dir / "subscription_webapp_admin.js").write_text( + "console.log('admin fallback');", encoding="utf-8" + ) + (asset_dir / "subscription_webapp_admin.css").write_text( + ".admin{color:red}", encoding="utf-8" + ) + + with patch.object(webapp_assets, "ASSET_DIR", asset_dir): + webapp_assets._ASSET_NAME_CACHE.clear() + self.assertRegex( + subscription_webapp._resolve_webapp_admin_js_asset_name(), + r"^subscription_webapp_admin\.js\?v=[0-9a-f]{8}$", + ) + self.assertRegex( + subscription_webapp._resolve_webapp_admin_css_asset_name(), + r"^subscription_webapp_admin\.css\?v=[0-9a-f]{8}$", ) async def test_js_asset_route_sets_immutable_cache_control_for_minified_asset(self):