mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 12:49:56 +00:00
feat(moon): the moon with any background, and its status in General settings (#718)
The owner decided on 30.09 that the moon is not part of the "Follow the Sun" environment but a switch of its own: with a static background (global or a space's own) the card showed no moon even with the switch on, and the switch said nothing about why the moon was missing right now. With a static background there is no environment, so the moon stands in its own layer, `.hp-moon-sky`: the first child of `.stage` / `.hp-static-stage`, the whole scene, no z-index, filter or will-change, under the plan by DOM order, fading with the #101 View weight. Inside is the very #661 element, so place, size, art and fades are unchanged, and a background switch moves it to its new parent in the same render without a flicker. The phase comes from the same `resolveDayCycle`, computed only while the moon is on and on View; without `sun.sun` both cards keep their 30 s clock ticker and re-render only when the phase changes (the environment is still compared by its whole fingerprint). General settings get a second caption line under the moon switch (`data-moon-status`): one snapshot per opening, judged by the lazy chunk as if the switch were on, first reason wins (no home, day, below 3°, under 3 %), numbers rounded and clamped below the threshold they missed. `moonStatus` decides "shown" with the same `moonShownAt` as the element. It lives in a WeakMap beside the draft, so it never makes the dialog dirty; a closed opening's result is dropped. The dialog loads the chunk through the gate's loader (`withMoon`), now shared by every caller while a load is in flight, so there is still one fingerprint check and one retry token. Bundle (same build, against origin/dev): initial View 300 072 -> 300 248 B gzip (+176 B, under the 500 B of the spec; budget and ceiling not raised); lazy editor 238 558 -> 238 991 B (+433 B, the line and English strings); lazy moon 11 385 -> 11 712 B (+327 B, layer CSS and status). `src/moon.ts` stays out of the initial and the editor graph; bundle-budget now refuses an editor/moon overlap. Monolith metrics: hostRefs 4 885 -> 4 888 — the three `host.` reads of `src/editors/moon-status.ts` (hass, `_settingsDialog`, requestUpdate) through its own three-member interface, not the editor port; the other five metrics are unchanged. houseplan-editor-runtime.ts grows by two lines (import, call). Tests: AC9/AC10/AC15 and the sky layer in test/moon.test.mjs (the #661 "static -> nothing" check inverted), AC14 and the opening lifecycle in test/moon-settings.test.mjs, smokes demo/smoke_moon_static.mjs (AC1-AC6; AC1 and AC3 were red on dev) and demo/smoke_moon_status.mjs (AC11/AC12), AC7 in smoke_daycycle_layer_budget. Golden: two new scenes (static-bg-moon-gibbous-white-light, static-bg-moon-crescent-south-dark, matrix v70), the harness checks the moon's parent by background and waits for the status line in the General settings frames. Four new mutants; the clock ticker one is a browser guard (201 at the guideline of 200). Issue: #718 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:
@@ -741,6 +741,11 @@ export function assertBundleBudget(
|
||||
if (manifest.initialViewFiles.some((path) => manifest.lazyMoonFiles.includes(path))) {
|
||||
throw new Error('initial View graph overlaps lazy moon graph');
|
||||
}
|
||||
// #718 K9: General settings reach the moon's status through the gate's
|
||||
// loader only; a static import would pull its astronomy into the editor.
|
||||
if (manifest.lazyEditorFiles.some((path) => manifest.lazyMoonFiles.includes(path))) {
|
||||
throw new Error('lazy editor graph overlaps lazy moon graph');
|
||||
}
|
||||
// #474: designer furniture artwork is lazy; a static import anywhere in the
|
||||
// View graph would pull ~10 KB gzip back into the initial graph silently.
|
||||
if (!manifest.lazyFurnitureArtFiles?.length) {
|
||||
|
||||
@@ -6302,8 +6302,8 @@ const MUTANT_DEFINITIONS = [
|
||||
because: '#661 C7: the moon chunk is not loaded by day — the initial View graph pays only the gate',
|
||||
patches: [{
|
||||
file: 'src/moon-gate.ts',
|
||||
find: " if (!runtime && state.phase !== 'day' && Date.now() >= nextAttempt) {",
|
||||
replace: ' if (!runtime && Date.now() >= nextAttempt) {',
|
||||
find: " if (!runtime && state.phase !== 'day') withMoon(() => host.requestUpdate());",
|
||||
replace: ' if (!runtime) withMoon(() => host.requestUpdate());',
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -6322,6 +6322,58 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: " + '.hp-moon{--hp-moon-box:min(200px,25cqmin);position:absolute;z-index:2;'",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-static-sky-off',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#718" test/moon.test.mjs',
|
||||
because: '#718 AC15/AC1: a static background — global or the space\'s own — shows the moon in its '
|
||||
+ 'own sky layer; back to «no environment, no moon» it would give nothing again',
|
||||
patches: [{
|
||||
file: 'src/moon-gate.ts',
|
||||
find: ' return !daynight && viewWeight > 0 && moonOn(settings) ? resolveDayCycle(hass, now) : null;',
|
||||
replace: ' return daynight && viewWeight > 0 && moonOn(settings) ? resolveDayCycle(hass, now) : null;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-static-clock-tick-off',
|
||||
guard: 'node demo/smoke_moon_static.mjs',
|
||||
because: '#718 AC3/K5: without sun.sun a static background has only the card\'s 30 s clock ticker '
|
||||
+ 'to reach 18:00 and 08:00; not armed, the moon never rises by the clock in the real card '
|
||||
+ '(page clock, UTC context, no state update), which only the browser run observes',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' const [state, key] = dayCycleClock(this._dayCycleState(), this._moonSkyState());\n'
|
||||
+ ' this._dayCycleClockKey = key;\n const needsTimer',
|
||||
replace: ' const [state, key] = dayCycleClock(this._dayCycleState(), null);\n'
|
||||
+ ' this._dayCycleClockKey = key;\n const needsTimer',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-status-order',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#718" test/moon.test.mjs',
|
||||
because: '#718 AC9/K7: without home coordinates the status says so even by day — the day reason '
|
||||
+ 'checked first would name the sun of a home the card cannot place',
|
||||
patches: [{
|
||||
file: 'src/moon.ts',
|
||||
find: " if (!sky.home) return { reason: 'no_home' };\n"
|
||||
+ " if (sky.phase === 'day') return sky.source === 'sun' ? { reason: 'day_sun', sun: Math.round(sky.sun ?? 0) } : { reason: 'day_clock' };\n",
|
||||
replace: " if (sky.phase === 'day') return sky.source === 'sun' ? { reason: 'day_sun', sun: Math.round(sky.sun ?? 0) } : { reason: 'day_clock' };\n"
|
||||
+ " if (!sky.home) return { reason: 'no_home' };\n",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-status-clamp-off',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#718" test/moon.test.mjs',
|
||||
because: '#718 AC9/K7: a hidden reason never rounds up to its threshold — 2.6° or 2.6 % read «2», '
|
||||
+ 'not «at 3°, shows from 3°»',
|
||||
patches: [{
|
||||
file: 'src/moon.ts',
|
||||
find: " return sky.altitude < MOON_ELEVATION_MIN ? { reason: 'low', alt: Math.min(alt, 2) } : { reason: 'new', pct: Math.min(pct, 2) };",
|
||||
replace: " return sky.altitude < MOON_ELEVATION_MIN ? { reason: 'low', alt } : { reason: 'new', pct };",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'moon-validation-accepts-anything',
|
||||
guard: 'python3 -m pytest tests_backend/test_settings_moon.py -q -p no:cacheprovider',
|
||||
|
||||
@@ -42,11 +42,16 @@ export const SMOKE_LINKS = [
|
||||
symbols: [
|
||||
'moonLayer', 'renderMoon', 'moonTick', 'moonView', 'moonShownAt', 'moonPhasePath',
|
||||
'moonPosition', 'moonIllumination', 'moonFingerprint', 'renderDayCycleEnvironment',
|
||||
'withMoon', 'moonSkyState', 'dayCycleClock', 'renderMoonSky', '_moonSkyState', '_dayCycleClock',
|
||||
'moonStatus', 'moonStatusOf', 'openMoonStatus', 'moonStatusText',
|
||||
],
|
||||
smokes: ['smoke_moon.mjs', 'smoke_daycycle_layer_budget.mjs'],
|
||||
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 '
|
||||
+ 'chunk arriving at night, the 30 s ticker and state updates carrying it through 3° with the '
|
||||
+ '2 s fade, the plan painting over it pixel for pixel, and no extra composited layer (CDP); '
|
||||
+ '#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); '
|
||||
+ 'no smoke names the functions',
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user