mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Network failure of the editor runtime is no longer terminal: the loader
re-arms to idle and the next explicit press starts a fresh cycle, while a
fingerprint mismatch on either attempt stays terminal. The toast now says
what actually helps — retry advice for the network, refresh advice for a
foreign build — via one shared lazyLoadFailureMessage helper (new i18n key
editor.retry_advice in en/ru/de).
The field smoke caught a second, deeper bug on the way: Chromium records a
FAILED module in the page module map permanently, so retrying the same URL
(even the cache-busted one) never touched the network again. Every retry
now carries a per-cycle nonce and becomes a genuinely new module request.
A proxy-cached stale entry no longer kills the card silently: the entry
facade is rewritten at build time from a static re-export into a top-level
`try{await import(...)}catch{...}` — importers keep the happy-path
guarantee (await import(entry) still resolves only after
customElements.define), and the catch defines a fallback element with a
localized "reload the page" panel. Content-hashed chunks are served with
`public, max-age=31536000, immutable`, and verifyBundleTree now fails on
orphan chunks that the manifest does not name.
Proofs: loader units for re-arm/terminality/toast wording + an AST check
that both loaders forward the terminality flag; smoke_entry_stale (en/ru)
against a tree without the main chunk; smoke_lazy_editor_chunk extended —
second press after network failure now really opens the editor; pytest for
the immutable header; orphan-tree unit; five new registry mutants.
TESTING.md budget line updated to the #352 ceiling alongside.
Issue: #353
User-Visible: yes
43 lines
1.5 KiB
Python
43 lines
1.5 KiB
Python
"""Public, manifest-gated JavaScript chunks for the Lovelace card."""
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
from aiohttp import web
|
||
from homeassistant.components.http import HomeAssistantView
|
||
try:
|
||
from homeassistant.components.http import KEY_HASS
|
||
except ImportError: # pragma: no cover - old HA compatibility
|
||
KEY_HASS = "hass" # type: ignore[assignment]
|
||
|
||
from .const import FRONTEND_ASSETS_URL
|
||
from .frontend_asset_manifest import ASSET_CACHE_CONTROL, resolve_frontend_asset
|
||
|
||
_FRONTEND_ROOT = Path(__file__).parent / "frontend"
|
||
|
||
|
||
class HouseplanFrontendAssetView(HomeAssistantView):
|
||
"""Serve only generated chunks named by the current build manifest."""
|
||
|
||
url = f"{FRONTEND_ASSETS_URL}/{{filename}}"
|
||
name = "api:houseplan:frontend-asset"
|
||
requires_auth = False
|
||
|
||
async def get(self, request: web.Request, filename: str) -> web.StreamResponse:
|
||
hass = request.app[KEY_HASS]
|
||
path = await hass.async_add_executor_job(
|
||
resolve_frontend_asset, _FRONTEND_ROOT, filename
|
||
)
|
||
if path is None:
|
||
raise web.HTTPNotFound()
|
||
# Chunk names are content-hashed, so a served body can never change
|
||
# under its URL — immutable caching is safe and keeps a stale proxy
|
||
# from re-serving an old graph after an update (#353 К4).
|
||
return web.FileResponse(
|
||
path,
|
||
headers={
|
||
"Cache-Control": ASSET_CACHE_CONTROL,
|
||
"X-Content-Type-Options": "nosniff",
|
||
},
|
||
)
|