mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
Owner's report: «план перезагружается при возврате на вкладку, хотя страница жива». Diagnosis confirmed: Lovelace re-creates the card element when the websocket reconnects after a long-backgrounded tab, and the fresh instance ran the FULL first-open boot — veil + BOOT_MIN_MS + quiescence — reading as a plan reload. On top of that, _loadFromServer's catch nulled _serverCfg after 8 failed tries, so a slow reconnect could genuinely blank an already-shown plan. DEV-B703-01 warm re-mount: module-scoped memo (lives with the PAGE, not the instance) of the settled header height, keyed by viewport size × card config. A repeat instance adopts the settled geometry in setConfig and skips the veil entirely — synchronous reveal at the saved zoom (HP-1551); a window resize between instances changes the key and brings the full protective boot back. The memo follows the live geometry (updated()'s measure) and is written on every _bootSettled. Test hook: static _warmBootReset(). DEV-B703-02 stale-while-revalidate: an instance that already renders a valid config (LS snapshot or a successful load) NEVER clears it on WS failures — the local-only fallback is reserved for a card that never had a backend. A self-driven retry (backoff, cap 8 s) keeps revalidating after willUpdate's 8-try budget is spent, and a connection 'ready' hook resets the budget and quietly re-reads the config the moment the socket is back (the event subscriptions re-subscribe on their own inside home-assistant-js-websocket). Smokes: smoke_warm_remount (fails pre-fix: veil + hidden plan on re-mount; resize invalidation stays cold), smoke_ws_resilience (fails pre-fix: _serverCfg cleared after 8 tries, no revalidation after recovery); the preloader smokes now reset the warm memo — they simulate a COLD first open.
81 lines
3.3 KiB
JavaScript
81 lines
3.3 KiB
JavaScript
// DEV-B703-02: temporary WS failures must NEVER blank a plan that was already
|
|
// shown. On the build before the fix _loadFromServer's catch nulled
|
|
// _serverCfg after 8 failed tries — the plan disappeared even though a valid
|
|
// config (the LS snapshot) was on screen. Stale-while-revalidate: the last
|
|
// valid config lives until a successful reload replaces it, and once the
|
|
// socket recovers the card revalidates QUIETLY on its own retry clock
|
|
// (willUpdate stops driving loads after 8 tries).
|
|
// planSurvivesOutage + reloadedAfterRecovery FAIL on the pre-fix build.
|
|
import { launch, check, finish } from './serve.mjs';
|
|
|
|
const { page, browser } = await launch({ width: 820, height: 760 });
|
|
|
|
const res = await page.evaluate(async () => {
|
|
const out = {};
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
customElements.get('houseplan-card')?._warmBootReset?.();
|
|
// the LS snapshot is in place (the page's main card cached it): the new
|
|
// instance renders instantly from it — a wall tablet rejoining wifi
|
|
out.lsCachePresent = !!localStorage.getItem('houseplan_card_cfg_v1');
|
|
|
|
let fail = true;
|
|
let failedCalls = 0;
|
|
const base = window.__mkHass();
|
|
const mkFlaky = () => ({
|
|
...base,
|
|
callWS: async (m) => {
|
|
if (fail) { failedCalls++; throw new Error('ws down'); }
|
|
return base.callWS(m);
|
|
},
|
|
});
|
|
const c = document.createElement('houseplan-card');
|
|
c.setConfig({ type: 'custom:houseplan-card' });
|
|
c.hass = mkFlaky();
|
|
const wrap = document.createElement('div');
|
|
wrap.style.cssText = 'position:fixed;left:0;top:0;width:800px;z-index:99;background:#000';
|
|
wrap.appendChild(c);
|
|
document.body.appendChild(wrap);
|
|
await sleep(50);
|
|
out.rendersFromCache = !!c._serverCfg;
|
|
|
|
// hass keeps ticking while the socket is down — drive past the 8-try budget
|
|
for (let i = 0; i < 12; i++) {
|
|
c.hass = mkFlaky();
|
|
await sleep(60);
|
|
}
|
|
out.wsFailedEnough = failedCalls >= 8 ? true : `only ${failedCalls} failed calls`;
|
|
out.planSurvivesOutage = c._serverCfg ? true : '_serverCfg was cleared by the outage';
|
|
out.stillServerMode = c._serverStorage === true;
|
|
const sr = () => c.shadowRoot || c.renderRoot;
|
|
const stage = sr().querySelector('.stage');
|
|
out.roomsStillRendered = !!stage && stage.querySelectorAll('.room').length > 0;
|
|
|
|
// ---- the socket comes back: quiet self-driven revalidation ----
|
|
fail = false;
|
|
const t0 = performance.now();
|
|
while (!c._loadOk && performance.now() - t0 < 12000) await sleep(100);
|
|
out.reloadedAfterRecovery = c._loadOk === true ? true : 'never revalidated after recovery';
|
|
out.cfgValidAfterRecovery = !!c._serverCfg && Array.isArray(c._serverCfg.spaces)
|
|
&& c._serverCfg.spaces.length > 0;
|
|
wrap.remove();
|
|
return out;
|
|
});
|
|
for (const [k, v] of Object.entries(res)) check(k, v);
|
|
|
|
// ---- a LIVE instance: a failing config reload keeps the last config ----
|
|
const live = await page.evaluate(async () => {
|
|
const out = {};
|
|
const c = window.__card;
|
|
const before = c._serverCfg;
|
|
const origHass = c.hass;
|
|
c.hass = { ...c.hass, callWS: async () => { throw new Error('ws down'); } };
|
|
await c._reloadConfigOnly(true);
|
|
out.liveReloadFailKeepsCfg = c._serverCfg === before
|
|
? true : 'a failed reload replaced/cleared the live config';
|
|
c.hass = origHass;
|
|
return out;
|
|
});
|
|
for (const [k, v] of Object.entries(live)) check(k, v);
|
|
|
|
await finish(browser, { ...res, ...live });
|