From 4c0a79805038875805764425ba2472b9941b52f7 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Fri, 22 May 2026 15:39:45 +0300 Subject: [PATCH] fix: load admin assets from stable paths --- backend/bot/app/web/webapp/assets.py | 15 +++++----- frontend/src/App.svelte | 42 +++++++++++++++++++++++++--- tests/test_webapp_assets.py | 6 ++-- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/backend/bot/app/web/webapp/assets.py b/backend/bot/app/web/webapp/assets.py index 6cbb4a3..b4cd115 100644 --- a/backend/bot/app/web/webapp/assets.py +++ b/backend/bot/app/web/webapp/assets.py @@ -1372,10 +1372,10 @@ def _resolve_webapp_js_asset_name() -> str: def _resolve_webapp_admin_js_asset_name() -> str: - return _resolve_hashed_js_asset_name( - kind="admin-js", - base_name="subscription_webapp_admin", - ) + # 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") def _resolve_hashed_js_asset_name(*, kind: str, base_name: str) -> str: @@ -1405,10 +1405,9 @@ def _resolve_webapp_css_asset_name() -> str: def _resolve_webapp_admin_css_asset_name() -> str: - return _resolve_hashed_css_asset_name( - kind="admin-css", - base_name="subscription_webapp_admin", - ) + # 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") def _resolve_hashed_css_asset_name(*, kind: str, base_name: str) -> str: diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 21bb499..ebb8921 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -635,7 +635,10 @@ link.rel = "stylesheet"; link.href = href; link.onload = () => resolve(); - link.onerror = () => reject(new Error(`stylesheet_load_failed:${href}`)); + link.onerror = () => { + link.remove(); + reject(new Error(`stylesheet_load_failed:${href}`)); + }; document.head.appendChild(link); }); } @@ -648,11 +651,34 @@ script.src = src; script.async = true; script.onload = () => resolve(); - script.onerror = () => reject(new Error(`script_load_failed:${src}`)); + script.onerror = () => { + script.remove(); + reject(new Error(`script_load_failed:${src}`)); + }; document.head.appendChild(script); }); } + async function appendStylesheetWithFallback(id, href, fallbackName) { + const fallbackHref = resolveWebappAssetPath("", fallbackName); + try { + await appendStylesheetOnce(id, href); + } catch (error) { + if (!fallbackHref || href === fallbackHref) throw error; + await appendStylesheetOnce(id, fallbackHref); + } + } + + async function appendScriptWithFallback(id, src, fallbackName) { + const fallbackSrc = resolveWebappAssetPath("", fallbackName); + try { + await appendScriptOnce(id, src); + } catch (error) { + if (!fallbackSrc || src === fallbackSrc) throw error; + await appendScriptOnce(id, fallbackSrc); + } + } + function readAdminBundleApi() { const bundle = window.SubscriptionWebAppAdmin; return bundle?.mount ? bundle : null; @@ -672,8 +698,16 @@ adminBundlePromise = (async () => { const cssHref = resolveWebappAssetPath(CFG.adminCssAsset, "subscription_webapp_admin.css"); const jsSrc = resolveWebappAssetPath(CFG.adminJsAsset, "subscription_webapp_admin.js"); - await appendStylesheetOnce("subscription-webapp-admin-css", cssHref); - await appendScriptOnce("subscription-webapp-admin-js", jsSrc); + await appendStylesheetWithFallback( + "subscription-webapp-admin-css", + cssHref, + "subscription_webapp_admin.css", + ); + await appendScriptWithFallback( + "subscription-webapp-admin-js", + jsSrc, + "subscription_webapp_admin.js", + ); const loaded = readAdminBundleApi(); if (!loaded) throw new Error("admin_bundle_missing_mount"); adminBundleApi = loaded; diff --git a/tests/test_webapp_assets.py b/tests/test_webapp_assets.py index 83ab5b9..e8e139c 100644 --- a/tests/test_webapp_assets.py +++ b/tests/test_webapp_assets.py @@ -488,7 +488,7 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase): "subscription_webapp.min.22222222.js", ) - def test_resolve_webapp_admin_asset_names_prefer_latest_minified_builds(self): + def test_resolve_webapp_admin_asset_names_use_stable_runtime_builds(self): with tempfile.TemporaryDirectory() as tmpdir: asset_dir = Path(tmpdir) (asset_dir / "subscription_webapp_admin.js").write_text( @@ -513,11 +513,11 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase): with patch.object(webapp_assets, "ASSET_DIR", asset_dir): self.assertEqual( subscription_webapp._resolve_webapp_admin_js_asset_name(), - "subscription_webapp_admin.min.22222222.js", + "subscription_webapp_admin.js", ) self.assertEqual( subscription_webapp._resolve_webapp_admin_css_asset_name(), - "subscription_webapp_admin.22222222.css", + "subscription_webapp_admin.css", ) async def test_js_asset_route_sets_immutable_cache_control_for_minified_asset(self):