Let the panel reach the card by its hashed name, not by the one address without a version

The sidebar page could serve a previous card for hours. It imported the
card through the stable facade, `./houseplan-card.js` — a relative
specifier, and relative resolution does not inherit a query. A dashboard
reaches the same file as `houseplan-card.js?v=1.74.0`, so an upgrade
changes its URL and the browser must refetch. The panel always asked for
the same address, and entries are served with no Cache-Control at all —
only ETag and Last-Modified — so the browser applies heuristic freshness
and may answer from cache without asking. A stale 1164-byte loader names
a stale chunk, chunks are immutable for a year, and the panel then ran a
previous card against the current backend without a single error. The
version banner was telling the truth; reloading could not help, because
the address never changed.

Rollup already emits the right edge: the panel's side-effect import
points straight at the shared implementation. The rewrite in
entryFallbackPlugin replaced it with the facade for a fallback that the
hashed name gives anyway — and better: a chunk the manifest no longer
serves now raises the panel's own "House Plan was updated" notice
instead of silently working on old code.

Two #486 assertions change meaning and are rewritten, not adjusted: the
panel no longer routes through the facade, and its initial graph no
longer contains it. The invariant they defended — the panel reuses the
exact card graph, never a second copy — is now stated over the
implementation, and a new test pins that no built entry reaches the card
by an address without a version.

Issue: #535
User-Visible: yes
This commit is contained in:
Claude
2026-09-11 22:12:11 +00:00
committed by claude[bot]
parent a52dcab77a
commit 063de0eef4
57 changed files with 536 additions and 428 deletions
+16 -7
View File
@@ -358,10 +358,18 @@ export function entryFallbackPlugin() {
);
// Rollup folds the small panel shell into its stable entry and points its
// side-effect import directly at the shared card implementation. Route it
// through the stable card facade instead: the real startup closure then
// contains the exact card graph (not a second copy), and a stale card
// implementation renders the existing card fallback inside the panel.
// side-effect import directly at the shared card implementation. Keep that
// edge (#535). Routing it through the stable card facade instead used to
// look harmless — the chunk is the same URL either way, so no second copy
// arises — but the facade is the ONE address in the distribution with no
// version in it: a dashboard reaches the same file through the Lovelace
// resource's `?v=`, while a relative specifier cannot inherit that query.
// Entries are served without Cache-Control (only ETag/Last-Modified), so
// a browser may keep its copy for hours; a stale panel entry then pulled
// a stale chunk — both cached — and silently ran a previous card against
// the current backend, with nothing but the version banner to show for it.
// The hashed name changes with the content, so now either the matching
// implementation arrives or the panel's own fallback says so out loud.
const panelContract = ENTRY_CONTRACTS[1];
const panelEntry = exactEntryChunk(bundle, panelContract);
const panelPattern = new RegExp(
@@ -376,11 +384,12 @@ export function entryFallbackPlugin() {
}
panelEntry.code = panelEntry.code.replace(
panelPattern,
`try{await import("./${CARD_ENTRY_FILE}")}`
`try{await import("${cardAsset}")}`
+ `catch(e){${fallbackDefinition(panelContract)}`
+ 'console.error("[houseplan] stale houseplan-panel.js: the card entry is unavailable",e)}',
+ 'console.error("[houseplan] stale houseplan-panel.js: the card implementation '
+ 'chunk is unavailable",e)}',
);
panelEntry.imports = [CARD_ENTRY_FILE];
panelEntry.imports = [cardAsset.replace(/^\.\//, '')];
},
};
}