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):