Release v1.62.0-beta.7 candidate
Validate / hacs (push) Failing after 10s
Validate / hassfest (push) Failing after 11s
Validate / frontend (push) Successful in 4m20s
Validate / backend (push) Failing after 4m47s
Validate / golden (push) Failing after 6m33s
Validate / performance_smoke (push) Failing after 9m34s
Validate / smoke (push) Failing after 19m8s

This commit is contained in:
Matysh
2026-08-12 15:13:38 +03:00
parent 36e81e9fb1
commit 01980ac3e6
15 changed files with 145 additions and 73 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ PLAN_ORPHAN_TTL_S = 3600
SCHEDULED_GRACE_S = 30 * 24 * 3600
FILES_DIR = "houseplan/files"
CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.62.0-beta.6"
VERSION = "1.62.0-beta.7"
# Portable backup format. This is deliberately independent from the Home
# Assistant Store version above: storage migrations and files exported by a
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -16,5 +16,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [],
"single_config_entry": true,
"version": "1.62.0-beta.6"
"version": "1.62.0-beta.7"
}
+16 -2
View File
@@ -40,13 +40,18 @@ const res = await page.evaluate(async () => {
const previewFace = face(preview?.renderRoot?.querySelector('.dev'));
const providerShown = /demo/i.test(preview?.renderRoot?.querySelector('.previewfacts')?.textContent || '');
// Issue #100: dynamic option creation must not leave the native select on
// its first row when a different source/position is already persisted.
// Dynamic option creation must not leave a freshly mounted native select on
// its first row when a different display/room/badge choice is persisted.
// Unmount the dialog first: replacing one open marker can leave old option
// nodes around and would miss the real first-open regression.
c._markerDialog = null;
await c.updateComplete;
const badgeDevice = c._devices.find((item) => item.id === 'd_temp');
c._openMarkerDialog({
...badgeDevice,
marker: {
...(badgeDevice.marker || {}),
display: 'value',
value_badge: {
enabled: true,
source: { kind: 'derived_lqi' },
@@ -55,10 +60,17 @@ const res = await page.evaluate(async () => {
},
});
await c.updateComplete;
const displaySelect = sr().querySelector('#marker-display');
const roomSelect = sr().querySelector('#marker-room');
const badgeSourceSelect = sr().querySelector('#marker-value-badge-source');
const badgePositionSelect = sr().querySelector('#marker-value-badge-position');
const savedBadgePreview = sr().querySelector('hp-device-preview');
await savedBadgePreview?.updateComplete;
const savedDisplaySelected = displaySelect?.value === 'value'
&& displaySelect?.selectedOptions?.[0]?.value === 'value';
const expectedRoom = c._markerDialog?.room || '';
const savedRoomSelected = expectedRoom !== '' && roomSelect?.value === expectedRoom
&& roomSelect?.selectedOptions?.[0]?.value === expectedRoom;
const savedBadgeSourceSelected = badgeSourceSelect?.value === 'derived:lqi'
&& badgeSourceSelect?.selectedOptions?.[0]?.value === 'derived:lqi';
const savedBadgePositionSelected = badgePositionSelect?.value === 'left'
@@ -109,6 +121,8 @@ const res = await page.evaluate(async () => {
planPreviewEqual: JSON.stringify(planFace) === JSON.stringify(previewFace),
planStaticEqual: JSON.stringify(planFace) === JSON.stringify(staticFace),
providerShown,
savedDisplaySelected,
savedRoomSelected,
savedBadgeSourceSelected,
savedBadgePositionSelected,
savedBadgePreviewMatches,
+10 -5
View File
@@ -93,18 +93,23 @@ const res = await page.evaluate(async () => {
light_entity: draft.lightEntity || null,
},
} : realPreview(draft);
c._openMarkerDialog(multi); await c.updateComplete;
c._setMarkerLightRole('always'); await c.updateComplete;
c._openMarkerDialog({
...multi,
marker: { ...(multi.marker || {}), is_light: true, light_entity: extra },
});
await c.updateComplete;
out.leadingDraftIsAlways = c._markerDialog.lightRole === 'always';
out.leadingPreviewHasExtra = c._markerPreviewDevice(c._markerDialog)?.entities?.includes(extra) === true;
const leading = sr().querySelector('hp-dialog #marker-light-entity');
out.leadingSelectorForMultiple = !!leading && leading.options.length === 3;
out.savedLeadingEntitySelected = leading?.value === extra
&& leading?.selectedOptions?.[0]?.value === extra;
if (leading) {
leading.value = extra;
leading.value = multi.primary;
leading.dispatchEvent(new Event('change', { bubbles: true }));
await c.updateComplete;
out.leadingUpdatesPreview = c._markerDialog.lightEntity === extra
&& c._markerSpatialSource(c._markerDialog)?.eid === extra;
out.leadingUpdatesPreview = c._markerDialog.lightEntity === multi.primary
&& c._markerSpatialSource(c._markerDialog)?.eid === multi.primary;
} else {
out.leadingUpdatesPreview = false;
}
File diff suppressed because one or more lines are too long
+18 -14
View File
File diff suppressed because one or more lines are too long
+10
View File
@@ -2,6 +2,16 @@
## Unreleased
## v1.62.0-beta.7 — 2026-08-12
- Fixed the first opening of device settings: **Display**, **Room** and
**Leading light entity** now immediately show the value that is actually
persisted instead of the first option. The form and its preview can no
longer report different settings.
- Added a shared native-select contract and browser checks that reopen a device
with non-first persisted choices. Future selectors can no longer silently
reintroduce the unsafe binding pattern.
## v1.62.0-beta.6 — 2026-08-12
- Fixed the remaining cause of invisible window sun rays in real Home Assistant
+11
View File
@@ -8,6 +8,17 @@
## Unreleased
## v1.62.0-beta.7 — 2026-08-12
- Исправлено первое открытие настроек устройства: поля «Отображение»,
«Комната» и «Ведущая сущность света» теперь сразу показывают фактически
сохранённые значения, а не первый пункт списка. Предпросмотр и форма больше
не могут сообщать пользователю разные настройки.
- Добавлен общий контракт для нативных выпадающих списков и браузерные
проверки повторного открытия устройства с непервыми сохранёнными вариантами.
Новый селектор больше нельзя случайно реализовать прежним небезопасным
способом.
## v1.62.0-beta.6 — 2026-08-12
- Исправлена оставшаяся причина невидимых солнечных лучей из окон в реальных
+5 -5
View File
@@ -1,14 +1,14 @@
<!-- release: v1.62.0-beta.6 -->
<!-- release: v1.62.0-beta.7 -->
## Основное
- Исправлено отображение солнечных лучей из окон в реальных установках Home Assistant ([#102](https://github.com/Matysh/houseplan-card/issues/102)).
- Настройки устройства теперь сразу показывают фактически сохранённые варианты отображения, комнаты и источника света.
- Мелкие исправления и улучшения.
## Highlights
- Fixed window sun rays in real Home Assistant installations ([#102](https://github.com/Matysh/houseplan-card/issues/102)).
- Device settings now immediately show the persisted display, room and light-source choices.
- Small fixes and improvements.
[Полный список изменений на русском](https://github.com/Matysh/houseplan-card/blob/v1.62.0-beta.6/docs/CHANGELOG.ru.md)
· [Full changelog in English](https://github.com/Matysh/houseplan-card/blob/v1.62.0-beta.6/docs/CHANGELOG.md)
[Полный список изменений на русском](https://github.com/Matysh/houseplan-card/blob/v1.62.0-beta.7/docs/CHANGELOG.ru.md)
· [Full changelog in English](https://github.com/Matysh/houseplan-card/blob/v1.62.0-beta.7/docs/CHANGELOG.md)
+2 -2
View File
@@ -21,8 +21,8 @@ metadata). Only an explicit owner-approved emergency hotfix may skip this gate.
| Item | State |
|---|---|
| Version | **v1.62.0-beta.6** everywhere (manifest, const.py, package.json, CARD_VERSION) — prerelease candidate for issue #102 |
| Current local cycle | v1.62.0-beta.6 fixes the remaining real-HA `sun.sun` projection regression and adds a browser-painted golden crop that fails when a window ray is absent. Issues #89 and #103 remain specification/backlog-only. |
| Version | **v1.62.0-beta.7** everywhere (manifest, const.py, package.json, CARD_VERSION) — prerelease candidate |
| Current local cycle | v1.62.0-beta.7 keeps the device editor's persisted Display, Room and leading-light selections aligned with their first rendered native selects, adds a source-level guard against unsafe select bindings and extends fresh-dialog browser coverage. Issues #89 and #103 remain specification/backlog-only. |
| Workflow | Owner's rule since 2026-08-07: ordinary fixes/features are made **locally, without tests and without commits**. A requested pre-release gets a production build plus the smallest targeted unit/smoke set covering the changed surfaces, one tested `dev` commit/tag and a GitHub Release with `prerelease=true`; `main` stays untouched. The complete local frontend/backend/smoke gate runs only before a stable release, after which `main` is fast-forwarded to the exact tested `dev` SHA and the GitHub Release uses `prerelease=false`. Release bodies are short and bilingual (Russian first): only significant user changes get individual bullets, while minor/code-only work is grouped as `Мелкие исправления и улучшения` / `Small fixes and improvements`; every body ends with separate links to the Russian and English changelogs. Detailed RU/EN changelog bullets may link the corresponding closed GitHub Issues; open or partially delivered issues are never presented as shipped. Telegram announcements are sent only for stable releases; beta and RC publication is silent. `docs/RELEASE-NOTES.md` is the current canonical body instance; `npm run release:prerelease -- <tag> --issues=… --yes` is the primary local publication path and the manual `Publish prerelease` workflow is its GitHub-only equivalent once present on `main`. Nothing is copied to the home instance by hand |
| GitHub | https://github.com/Matysh/houseplan-card — [Issues](https://github.com/Matysh/houseplan-card/issues) are the canonical task records and the linked [Project v2](https://github.com/users/Matysh/projects/1) is the canonical priority/status view; both must stay current. `main` carries stable releases; pre-release tags may point directly at `dev`. Work lands on `dev` and is merged into `main` for a stable release, so `dev` is normally equal to or ahead of `main`, never behind. Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
| CI | Prerelease publication requires a green exact-SHA Validate: frontend/backend, smoke (including the #73 rAF frame sampler), golden, HACS/Hassfest and a short absolute-ceiling performance smoke. Obsolete same-ref Validate runs are cancelled. Full seven-sample base/candidate performance moved to `performance.yml` (`main` push, weekly, manual); stable release assets fail closed unless Validate and Full Performance are green for the exact tagged SHA and the stable-only CDP compositor screencast finds no empty/black presented frame. |
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "houseplan-card",
"version": "1.62.0-beta.6",
"version": "1.62.0-beta.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "houseplan-card",
"version": "1.62.0-beta.6",
"version": "1.62.0-beta.7",
"license": "MIT",
"dependencies": {
"lit": "^3.1.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "houseplan-card",
"version": "1.62.0-beta.6",
"version": "1.62.0-beta.7",
"description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT",
"type": "module",
+16 -12
View File
@@ -183,7 +183,7 @@ import {
import { renderOpeningTunnelFills } from './render/opening-tunnels';
import { safeStoredColor } from './color';
const CARD_VERSION = '1.62.0-beta.6';
const CARD_VERSION = '1.62.0-beta.7';
/** Keeps every previously valid scale at the maximum 20 cm grid scale lossless. */
const DECOR_TEXT_CM_MAX = 2000;
const CELL_CM_MIN = 0.1;
@@ -8640,7 +8640,7 @@ class HouseplanCard extends LitElement {
</datalist>
${ent ? html`
<label>${this._t('decor.live_attr')}</label>
<select class="namein" .value=${''}
<select id="decor-live-attribute" class="namein" .value=${''}
@change=${(e: Event) => {
const value = (e.target as HTMLSelectElement).value;
if (value) this._decorInsertLiveVariable(value === '__state__' ? null : value);
@@ -16636,12 +16636,14 @@ class HouseplanCard extends LitElement {
: nothing}
</div>
<label>${this._t('marker.room_label')}${isVirtual ? '' : this._t('marker.room_override')}</label>
<select class="areasel" .value=${d.room}
<label for="marker-room">${this._t('marker.room_label')}${isVirtual ? '' : this._t('marker.room_override')}</label>
<select id="marker-room" class="areasel"
@change=${(e: Event) => (this._markerDialog = { ...d, room: (e.target as HTMLSelectElement).value })}>
<option value="">${isVirtual ? this._t('marker.room_choose') : this._t('marker.room_auto')}</option>
<option value="" ?selected=${!d.room}>
${isVirtual ? this._t('marker.room_choose') : this._t('marker.room_auto')}
</option>
${this._allRoomsFlat().map(
(r) => html`<option value=${r.value}>${r.label}</option>`,
(r) => html`<option value=${r.value} ?selected=${r.value === d.room}>${r.label}</option>`,
)}
</select>
@@ -16764,18 +16766,18 @@ class HouseplanCard extends LitElement {
${this._help('marker.light_entity.help')}
</div>
<select id="marker-light-entity" class="areasel"
.value=${staleLeading ? '' : d.lightEntity}
@change=${(e: Event) => (this._markerDialog = {
...d,
lightEntity: (e.target as HTMLSelectElement).value,
lightEntityTouched: true,
})}>
<option value="">
<option value="" ?selected=${staleLeading || !d.lightEntity}>
${this._t('marker.light_entity_auto', {
entity: effectiveLeading || this._t('marker.light_entity_none'),
})}
</option>
${leadingEntities.map((eid) => html`<option value=${eid}>
${leadingEntities.map((eid) => html`<option value=${eid}
?selected=${!staleLeading && eid === d.lightEntity}>
${this.hass.states[eid]?.attributes?.friendly_name
|| this._fullRegistryHass.entities[eid]?.name || eid} · ${eid}
</option>`)}
@@ -16877,14 +16879,16 @@ class HouseplanCard extends LitElement {
</button></p>`
: nothing}
<label>${this._t('marker.display_label')}</label>
<select class="areasel" .value=${d.display}
<label for="marker-display">${this._t('marker.display_label')}</label>
<select id="marker-display" class="areasel"
@change=${(e: Event) => (this._markerDialog = {
...d,
display: normalizeDeviceDisplay((e.target as HTMLSelectElement).value),
})}>
${DISPLAY_MODES.map((v) => [v, 'display.' + v] as const).map(
([v, k]) => html`<option value=${v}>${this._t(k as any)}</option>`,
([v, k]) => html`<option value=${v} ?selected=${v === d.display}>
${this._t(k as any)}
</option>`,
)}
</select>
<p class="muted">${this._t('marker.display_hint')}</p>
+16
View File
@@ -0,0 +1,16 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
test('native selects never rely on value assignment before dynamic options mount', () => {
const blocks = [...source.matchAll(/<select\b[\s\S]*?<\/select>/g)].map((match) => match[0]);
const valueBoundIds = blocks
.filter((block) => /\.value\s*=\s*\$\{/.test(block))
.map((block) => block.match(/\bid="([^"]+)"/)?.[1] || '(missing id)');
// This is a command menu, not persisted state: it intentionally resets to
// its empty first row after inserting an HA variable into decorative text.
assert.deepEqual(valueBoundIds, ['decor-live-attribute']);
});