mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 20:29:00 +00:00
fix(moon): the status line after a warm revive of General settings (#731)
#718 K7 takes the moon status once per opening of General settings, outside the draft. A warm remount revives the open dialog on a new card instance, but `_warmReviveDialog` restored only the draft: the new instance had no opening of its own, so the "Now: ..." line never came back. A revive is an opening too. The `settings` branch now asks for the status the way `_openSettingsDialog` does - through the lazy editor runtime (`_openMoonStatus` -> `openMoonStatus`): at once when the runtime is there (an editor revives after `_requestMode(..., adopt)` has installed it), after it loads in View; once per revive and only while that revived dialog is still open. The snapshot of now, `hass.config` and `sun.sun` is the revive's own, nothing of the dead instance's opening is carried over, the draft key and the dirty flag do not change. The View graph gets no static moon-status import; other dialog kinds never ask for the moon chunk. demo/smoke_moon_status.mjs gains the revive scenarios - View, the plan editor, a revive while the chunk is still loading, a space-dialog revive that must not load the chunk; the first three are red on dev. test/moon-settings.test.mjs executes the revive as a new opening; the wiring itself is proven by the smoke, not by reading the monolith as text (#624). docs/SUN.md and docs/WARM-REMOUNT.md say a revive is an opening; scripts/smoke-links.mjs links the two new symbols to the smoke. Issue: #731 User-Visible: yes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
@@ -80,6 +80,29 @@ const closeDialog = (page) => page.evaluate(async () => {
|
||||
const result = await window.__hpTest.close(dialog);
|
||||
return { closed: result.closed, confirm: !!result.confirm };
|
||||
});
|
||||
/**
|
||||
* #731: Lovelace replaces the card — remove, then a new element with the same
|
||||
* config in the same slot (as in smoke_warm_dialogs); `window.__card` follows
|
||||
* the successor. True once the successor has revived the dialog of `kind`.
|
||||
*/
|
||||
const remount = (page, kind) => page.evaluate(async (kind) => {
|
||||
const old = window.__card;
|
||||
const host = old.parentNode;
|
||||
const hass = old.hass;
|
||||
old.remove();
|
||||
await new Promise((done) => setTimeout(done, 20));
|
||||
const card = document.createElement('houseplan-card');
|
||||
card.setConfig({ type: 'custom:houseplan-card', title: 'House Plan', icon_size: 3.4 });
|
||||
card.hass = hass;
|
||||
host.appendChild(card);
|
||||
window.__card = card;
|
||||
const revived = () => !!card.renderRoot?.querySelector(`hp-dialog[data-kind="${kind}"]`);
|
||||
// The page clock is fixed: count steps, not milliseconds.
|
||||
for (let step = 0; step < 250 && !revived(); step++) await new Promise((done) => setTimeout(done, 20));
|
||||
await card.updateComplete;
|
||||
return revived();
|
||||
}, kind);
|
||||
const draftOf = (page) => page.evaluate(() => JSON.stringify(window.__card._settingsDialog));
|
||||
|
||||
// ─── AC11 and AC12 (chunk already loaded by the View at night) ───
|
||||
{
|
||||
@@ -161,6 +184,74 @@ const closeDialog = (page) => page.evaluate(async () => {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
// ─── #731 AC1/AC2: a warm revive is an opening of its own ───
|
||||
{
|
||||
const { page, browser } = await open({ moon: true });
|
||||
await openDialog(page);
|
||||
const opened = await line(page);
|
||||
const draft = await draftOf(page);
|
||||
// The sun rises while the dialog is open: this opening keeps its snapshot…
|
||||
await pushHass(page, { sun: { azimuth: 200, elevation: 25.4, rising: false } });
|
||||
const stale = await line(page);
|
||||
// …and the card is replaced. The revived dialog is a new opening: its own
|
||||
// snapshot now, nothing carried over from the dead instance.
|
||||
const revived = await remount(page, 'settings');
|
||||
const arrived = await waitLine(page, 4000);
|
||||
const after = await line(page);
|
||||
const draftAfter = await draftOf(page);
|
||||
await closeDialog(page);
|
||||
// A regular opening at the same moment (the page clock is fixed).
|
||||
await openDialog(page);
|
||||
const regular = await line(page);
|
||||
report.r731 = { opened, stale, revived, after, regular };
|
||||
checks.r731_ac1_revivedDialogHasTheLine = revived && arrived && after.count === 1;
|
||||
checks.r731_ac1_sameAsARegularOpening = after.text === regular.text && after.reason === regular.reason
|
||||
&& after.text === DAY_SUN;
|
||||
checks.r731_ac2_reviveTakesItsOwnSnapshot = opened.reason === 'shown' && stale.reason === 'shown'
|
||||
&& after.reason === 'day_sun';
|
||||
checks.r731_ac2_lineLeavesTheRevivedDraftClean = after.saveDisabled && draftAfter === draft;
|
||||
await closeDialog(page);
|
||||
// In an editor the successor first waits for its own lazy runtime, then
|
||||
// adopts the mode and only then revives the dialog.
|
||||
await page.evaluate(() => window.__hpTest.setMode('plan'));
|
||||
await openDialog(page);
|
||||
const revivedInPlan = await remount(page, 'settings');
|
||||
const arrivedInPlan = await waitLine(page, 4000);
|
||||
const inPlan = await line(page);
|
||||
const planMode = await page.evaluate(() => window.__card._mode);
|
||||
report.r731.plan = { revivedInPlan, planMode, inPlan };
|
||||
checks.r731_ac1_revivedInAnEditorHasTheLine = revivedInPlan && arrivedInPlan && planMode === 'plan'
|
||||
&& inPlan.count === 1 && inPlan.text === regular.text && inPlan.saveDisabled;
|
||||
await closeDialog(page);
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
// ─── #731 AC3: other revives never ask for the chunk; a revive while it loads ───
|
||||
{
|
||||
const delay = async (route) => { await new Promise((done) => setTimeout(done, 1000)); await route.fallback(); };
|
||||
const { page, browser, moonRequests } = await open({ route: delay });
|
||||
const space = await page.evaluate(() => window.__card._space);
|
||||
await page.evaluate((id) => window.__hpTest.openSpaceDialog('edit', id), space);
|
||||
const revivedSpace = await remount(page, 'space');
|
||||
await page.waitForTimeout(600);
|
||||
const afterSpace = moonRequests.length;
|
||||
await page.evaluate(async () => {
|
||||
const dialog = window.__card.renderRoot.querySelector('hp-dialog[data-kind="space"]');
|
||||
if (dialog) await window.__hpTest.close(dialog);
|
||||
});
|
||||
// The opening asks for the chunk; the card is replaced before it lands.
|
||||
await openDialog(page);
|
||||
const revived = await remount(page, 'settings');
|
||||
const arrived = await waitLine(page, 3000);
|
||||
const late = await line(page);
|
||||
report.r731.ac3 = { revivedSpace, afterSpace, revived, late, requests: moonRequests.length };
|
||||
checks.r731_ac3_otherRevivesLeaveTheChunkAlone = revivedSpace && afterSpace === 0;
|
||||
checks.r731_ac3_reviveWhileTheChunkLoads = revived && arrived && late.count === 1
|
||||
&& late.reason === 'shown' && late.text === SHOWN && late.saveDisabled && moonRequests.length === 1;
|
||||
await closeDialog(page);
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
// ─── AC12: the chunk request is refused ───
|
||||
{
|
||||
const { page, browser } = await open({ route: (route) => route.abort() });
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
that switch a new line tells whether the moon is shown right now and, if not,
|
||||
why — daytime, the moon below 3°, new moon or no home location in Home
|
||||
Assistant ([#718](https://github.com/Matysh/houseplan-card/issues/718)).
|
||||
- General settings restored after the card reloads — for example on returning
|
||||
to the tab — now show the moon's "Now: …" line too, worked out afresh at that
|
||||
moment ([#731](https://github.com/Matysh/houseplan-card/issues/731)).
|
||||
|
||||
## v1.79.0-beta.1 — 2026-09-30
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
появилась строка: видна ли луна сейчас, а если нет — почему (день, луна ниже
|
||||
3°, новолуние или в Home Assistant не заданы координаты дома)
|
||||
([#718](https://github.com/Matysh/houseplan-card/issues/718)).
|
||||
- Общие настройки, восстановленные после перезагрузки карточки — например, при
|
||||
возврате на вкладку, — тоже показывают строку луны «Сейчас: …», рассчитанную
|
||||
заново на этот момент
|
||||
([#731](https://github.com/Matysh/houseplan-card/issues/731)).
|
||||
|
||||
## v1.79.0-beta.1 — 2026-09-30
|
||||
|
||||
|
||||
+4
-1
@@ -494,7 +494,10 @@ do not change it. `moonStatus` in `src/moon.ts` decides «shown» with the same
|
||||
month), rounds to whole numbers and keeps a hidden reason's number below its
|
||||
threshold (2.6° reads «2°»). The status lives beside the draft, never in it:
|
||||
the line arriving leaves «Save» disabled. While the chunk loads, or when it
|
||||
failed, there is no line; a closed opening's result is dropped. The line
|
||||
failed, there is no line; a closed opening's result is dropped. A warm revive
|
||||
of the dialog (the card replaced, `docs/WARM-REMOUNT.md`) is an opening too:
|
||||
its own snapshot, asked through the lazy editor runtime once it is there;
|
||||
nothing is carried over from the replaced card (#731). The line
|
||||
belongs to the browser the dialog is open in — a wall tablet with another
|
||||
clock or time zone may differ.
|
||||
|
||||
|
||||
@@ -231,6 +231,10 @@ hover. После долгого сна общий `VisualContinuityController`
|
||||
обвязкой (`_roomEditId`, `_roomFill`, источники температуры и влажности,
|
||||
масштабы подписей, `_areaSel`/`_nameSel`, `_pendingSplit`, `_path`).
|
||||
|
||||
Строка статуса луны в общих настройках (#718 K7) живёт вне черновика и не
|
||||
переносится: восстановление — новое открытие, статус снимается заново, как
|
||||
только у нового экземпляра есть ленивый рантайм редактора (#731).
|
||||
|
||||
Информационные попапы — `_infoCard` (карточка устройства) и `_openingInfo` —
|
||||
восстанавливаются **по id**, а не по объекту: конфиг мог перезагрузиться под
|
||||
нами, и карточка, отрисованная из устаревшего объекта, была бы враньём. Если
|
||||
|
||||
@@ -43,7 +43,7 @@ export const SMOKE_LINKS = [
|
||||
'moonLayer', 'renderMoon', 'moonTick', 'moonView', 'moonShownAt', 'moonPhasePath',
|
||||
'moonPosition', 'moonIllumination', 'moonFingerprint', 'renderDayCycleEnvironment',
|
||||
'withMoon', 'moonSkyState', 'dayCycleClock', 'renderMoonSky', '_moonSkyState', '_dayCycleClock',
|
||||
'moonStatus', 'moonStatusOf', 'openMoonStatus', 'moonStatusText',
|
||||
'moonStatus', 'moonStatusOf', 'openMoonStatus', 'moonStatusText', '_reviveMoonStatus', '_openMoonStatus',
|
||||
],
|
||||
smokes: ['smoke_moon.mjs', 'smoke_moon_static.mjs', 'smoke_moon_status.mjs', 'smoke_daycycle_layer_budget.mjs'],
|
||||
because: '#661: the moon is observed only as `.hp-moon` in the production bundle — the lazy '
|
||||
@@ -52,6 +52,7 @@ export const SMOKE_LINKS = [
|
||||
+ '#718: the same over a static background as `.hp-moon-sky` (the clock ticker at 08:00 and '
|
||||
+ '18:00, tab and background switches, the #101 View weight, houseplan-space-card) and the '
|
||||
+ 'General settings line as `[data-moon-status]` (delayed, refused and preloaded chunk); '
|
||||
+ '#731: that line in a dialog revived after a warm remount (View, plan editor, chunk loading); '
|
||||
+ 'no smoke names the functions',
|
||||
},
|
||||
{
|
||||
|
||||
+10
-1
@@ -3497,7 +3497,7 @@ export class HouseplanCard extends LitElement {
|
||||
switch (d.kind) {
|
||||
case 'space': this._spaceDialog = { ...d.data, busy: false, savedBusy: false }; break;
|
||||
case 'marker': this._markerDialog = { ...d.data, busy: false }; break;
|
||||
case 'settings': this._settingsDialog = { ...d.data, busy: false }; break;
|
||||
case 'settings': this._settingsDialog = { ...d.data, busy: false }; this._reviveMoonStatus(); break;
|
||||
case 'rules': this._rulesDialog = { ...d.data, busy: false }; break;
|
||||
case 'opening': this._openingDialog = { ...d.data }; break;
|
||||
case 'backdrop': this._backdropDialog = { ...d.data }; break;
|
||||
@@ -10125,6 +10125,15 @@ export class HouseplanCard extends LitElement {
|
||||
return this._editorRuntime._openSettingsDialog();
|
||||
}
|
||||
|
||||
/** #731: a warm revive is an opening too (#718 K7) — its own status, asked once the lazy runtime is here. */
|
||||
private _reviveMoonStatus(dialog = this._settingsDialog): void {
|
||||
if (!this._editorRuntime) {
|
||||
void this._ensureEditorRuntime().then((ready) => { if (ready) this._reviveMoonStatus(dialog); });
|
||||
return;
|
||||
}
|
||||
if (dialog && this._settingsDialog === dialog) this._editorRuntime._openMoonStatus();
|
||||
}
|
||||
|
||||
private _openSupportDialog = (): void => {
|
||||
if (!this._editorRuntime) {
|
||||
void this._ensureEditorRuntime().then((ready) => {
|
||||
|
||||
@@ -8468,6 +8468,9 @@ public _openSettingsDialog = (): void => {
|
||||
openMoonStatus(this.host); // #718 K7: the status line, outside the draft
|
||||
};
|
||||
|
||||
/** #731: the card revived General settings — a new opening, so a new snapshot. */
|
||||
public _openMoonStatus(): void { openMoonStatus(this.host); }
|
||||
|
||||
public _openSupportDialog = (): void => {
|
||||
if (!this.host._norm || !this.host._canEdit) return;
|
||||
clearTimeout(this._supportExpiryTimer);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
// #661 AC10: the moon switch in General settings — «Sun and Moon» section,
|
||||
// reset to on, save writes `true` or removes the key; its strings exist in all
|
||||
// four dictionaries. #718: the status line under it (AC14, K7). The dialog is
|
||||
// a lazy Lit module (its layout is read as a source contract); reading and
|
||||
// writing the key and the status of an opening are executed.
|
||||
// four dictionaries. #718: the status line under it (AC14, K7); #731: a warm
|
||||
// revive of the dialog is an opening of its own. The dialog is a lazy Lit
|
||||
// module (its layout is read as a source contract); reading and writing the
|
||||
// key and the status of an opening are executed.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { generalDraftKey, moonDraftOf, writeMoonSetting } from '../test-build/editors/general-form-state.js';
|
||||
import {
|
||||
generalDirty, generalDraftKey, moonDraftOf, rememberGeneralBaseline, writeMoonSetting,
|
||||
} from '../test-build/editors/general-form-state.js';
|
||||
import { restoreWarmDialogBaseline, warmDialogBaseline } from '../test-build/editors/dialog-baseline.js';
|
||||
import { moonStatusOf, openMoonStatus } from '../test-build/editors/moon-status.js';
|
||||
|
||||
const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8');
|
||||
@@ -155,3 +159,35 @@ test('#718 K7: one snapshot per opening, outside the draft; a closed opening’s
|
||||
openMoonStatus(host, new Date(2026, 9, 21, 12, 0));
|
||||
assert.deepEqual(moonStatusOf(host), { reason: 'day_clock' });
|
||||
});
|
||||
|
||||
test('#731 AC2: a warm revive is a new opening — its own snapshot, nothing carried over, the draft untouched', async () => {
|
||||
const at = new Date('2026-10-21T18:00:00Z');
|
||||
// The instance Lovelace replaced: opened at night, judged, its draft clean.
|
||||
const dead = dialogHost(hassWith(NIGHT));
|
||||
rememberGeneralBaseline(dead, dead._settingsDialog);
|
||||
openMoonStatus(dead, at);
|
||||
await settle();
|
||||
assert.deepEqual(moonStatusOf(dead), { reason: 'shown', alt: 24, pct: 79 });
|
||||
// Its successor, as `_warmReviveDialog` hands the dialog over — the draft
|
||||
// copied, the baseline restored — after the sun has risen.
|
||||
const live = dialogHost(hassWith({ azimuth: 180, elevation: 40, rising: false }));
|
||||
live._settingsDialog = { ...dead._settingsDialog, busy: false };
|
||||
restoreWarmDialogBaseline(live, 'settings', warmDialogBaseline(dead, 'settings'));
|
||||
assert.equal(moonStatusOf(live), undefined, 'the dead opening’s result is not carried over');
|
||||
const key = generalDraftKey(live._settingsDialog);
|
||||
assert.equal(generalDirty(live, live._settingsDialog), false);
|
||||
// The revive's own opening (the runtime's `_openMoonStatus`): the snapshot of now.
|
||||
openMoonStatus(live, at);
|
||||
assert.deepEqual(moonStatusOf(live), { reason: 'day_sun', sun: 40 });
|
||||
assert.equal(live.updates, 1);
|
||||
assert.equal(generalDraftKey(live._settingsDialog), key, 'the line stays outside the draft');
|
||||
assert.equal(generalDirty(live, live._settingsDialog), false, 'the line does not make it dirty');
|
||||
// A dirty draft travels the same way: the line does not clean it either.
|
||||
live._settingsDialog = { ...live._settingsDialog, moon: false };
|
||||
openMoonStatus(live, at);
|
||||
assert.equal(generalDirty(live, live._settingsDialog), true);
|
||||
// The same element re-attached: its earlier opening is replaced, not reused.
|
||||
live.hass = hassWith(NIGHT);
|
||||
openMoonStatus(live, at);
|
||||
assert.deepEqual(moonStatusOf(live), { reason: 'shown', alt: 24, pct: 79 });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user