Files
houseplan-card/custom_components/houseplan/frontend_asset_manifest.py
T
Codexandclaude[bot] 78c6020747 fix: lazy delivery survives flaky networks and stale caches (#353)
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
2026-08-28 14:32:08 +00:00

42 lines
1.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Pure manifest/path resolver for generated frontend assets."""
from __future__ import annotations
import json
from pathlib import Path
_MANIFEST = "houseplan-assets.json"
_ASSET_DIR = "houseplan-assets"
# Content-hashed chunk bodies never change under their URL (#353 К4). The
# stable entry `houseplan-card.js` is served elsewhere and stays uncached.
ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable"
def resolve_frontend_asset(frontend_root: Path, filename: object) -> Path | None:
"""Resolve one generated JS asset only when the current manifest lists it."""
if not isinstance(filename, str) or not filename or filename != Path(filename).name:
return None
if "/" in filename or "\\" in filename or filename in {".", ".."}:
return None
if not filename.endswith(".js"):
return None
try:
manifest = json.loads((frontend_root / _MANIFEST).read_text(encoding="utf-8"))
except (OSError, ValueError, TypeError):
return None
if not isinstance(manifest, dict) or manifest.get("schema") != 1:
return None
expected = f"{_ASSET_DIR}/{filename}"
listed = {
item.get("path")
for item in manifest.get("files", [])
if isinstance(item, dict)
}
if expected not in listed:
return None
asset_root = (frontend_root / _ASSET_DIR).resolve()
candidate = (asset_root / filename).resolve()
if candidate.parent != asset_root or not candidate.is_file():
return None
return candidate