Compare commits

...
Author SHA1 Message Date
claude[bot] 27920a7919 docs: review document for #415
Issue: #415
User-Visible: no
2026-09-02 05:08:47 +00:00
Sergey Matyunin b012280373 docs: refresh reviewed support screenshots
Canonical Linux capture: https://github.com/Matysh/houseplan-card/actions/runs/33592550716
Eight changed frames were reviewed; two byte-identical frames remain as environment witnesses.

Issue: #415
User-Visible: no
2026-09-02 07:57:08 +03:00
claude[bot]andSergey Matyunin c6fac8bf1c docs: review document for #415
Issue: #415
User-Visible: no
2026-09-02 07:53:28 +03:00
Sergey Matyunin d1691f4bc2 fix: align support controls
Issue: #415
User-Visible: yes
2026-09-02 07:53:28 +03:00
Matysh 206732e9f5 ci: refuse a review round that cites an unreachable SHA
SPEC-REVIEW-403-r2 объявил материал раунда на `HEAD = 83005c3c`, и тот же SHA
независимо назвал автор ТЗ в комментарии issue. Разбор подтвердил находку и
уточнил её: коммит существовал, но к моменту публикации был осиротевшим.
Ветку перебазировали за пятнадцать минут ДО публикации документа — спец-коммит
переехал в 94502d3d с тем же сообщением и тем же содержимым (блоб ТЗ у обоих
56a92e12). Через раунд команда `git diff 83005c3c..HEAD` из §2.10 буквально не
работала, и r3 восстанавливал коммит по содержимому диффа руками.

Гейт судит только объявление материала в шапке документа, а не каждое
шестнадцатеричное слово: в прозе SHA упоминаются исторически, и обещания
воспроизводимости на них нет. Границы кандидата подобраны по корпусу — 7–40
знаков, хотя бы одна буква, не после `#`, не внутри длинного хеша; это
отсекает sha256, цвета и номера прогонов.

Достижимость считается от refs/remotes/origin, а не от локальных ссылок.
Разница не теоретическая: осиротевший 83005c3c до сих пор достижим в клоне
автора из необновлённой локальной ветки — локальная проверка сказала бы «всё в
порядке» ровно на той машине, где ошибку и совершили.

Шаг стоит ПОСЛЕ публикации и ДО перестановки метки. Артефакт ревью терялся
здесь трижды (#171, #220), и «вердикт без документа» дороже мёртвой ссылки:
документ сначала спасается, потом судится. Инвариант «метка не сменилась =
прогон упал» при этом сохраняется.

Проверено на настоящих документах: SPEC-REVIEW-403-r2 отказ, CODE-REVIEW-390-r1
проходит, документ без объявления материала не судится.

Issue: #413
User-Visible: no
2026-09-02 07:45:39 +03:00
39 changed files with 796 additions and 181 deletions
+36
View File
@@ -745,6 +745,42 @@ jobs:
fi
echo "документ опубликован в $target: $doc"
# Материал раунда обязан быть достижим с origin (#413).
#
# SPEC-REVIEW-403-r2 объявил материал на `HEAD = 83005c3c`, и тот же SHA
# независимо назвал автор ТЗ в комментарии issue. Коммит существовал, но
# к моменту публикации был осиротевшим: ветку перебазировали за 15 минут
# ДО публикации документа, спец-коммит переехал в 94502d3d с тем же
# сообщением и тем же содержимым. Через раунд команда `git diff
# 83005c3c..HEAD` из §2.10 буквально не работала, и r3 восстанавливал
# реальный коммит по содержимому диффа руками.
#
# Проверка стоит ПОСЛЕ публикации намеренно. Артефакт ревью терялся здесь
# трижды (#171, #220), и «вердикт без документа» в этом репозитории
# дороже мёртвой ссылки: документ сначала спасается, потом судится. Шаг
# при этом идёт ДО «Переставить метку», поэтому инвариант «метка не
# сменилась = прогон упал» сохраняется.
#
# Достижимость считается от `refs/remotes/origin/*`, а не от локальных
# ссылок: осиротевший 83005c3c до сих пор лежит в клоне автора и
# достижим там из необновлённой локальной ветки. Читателю отчёта от этого
# пользы нет — он достанет только то, что есть на origin.
- name: "Материал раунда воспроизводим (#413)"
if: steps.rebase.outputs.conflict != 'true'
env:
NUM: ${{ github.event.issue.number }}
STAGE: ${{ needs.guard.outputs.stage }}
CYCLE: ${{ needs.guard.outputs.cycle }}
BRANCH: ${{ steps.branch.outputs.name }}
run: |
marker=CODE-REVIEW
if [ "$STAGE" = "spec" ]; then marker=SPEC-REVIEW; fi
doc="docs/reviews/${marker}-${NUM}-r${CYCLE}.md"
target="${BRANCH:-dev}"
git fetch -q origin "$target"
# Судится опубликованная версия, а не рабочая копия: именно её прочтёт
# следующий раунд.
git show "origin/$target:$doc" | node scripts/review-doc-guard.mjs --doc=-
- name: Решение по вердикту
id: decide
if: steps.rebase.outputs.conflict != 'true'
@@ -1,125 +1,125 @@
{
"schema": 1,
"fingerprint": "cfeec183260c075204dd0fdd0333fdf69f1bab8b9e9142483b49b645e0526710",
"fingerprint": "baf59953cfd6eb3543f7da5af21787b20dc76f136b74782ae917f811d2fc1ab5",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-DdbeLYki.js",
"houseplan-assets/houseplan-card-BGFqEA3T.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 290375,
"initialViewGzipBytes": 290391,
"lazyFiles": [
"houseplan-assets/backdrop-pick-BDzJ-1w0.js",
"houseplan-assets/de-h-9CiLUA.js",
"houseplan-assets/editor-BcMfn7wA.js",
"houseplan-assets/fr-JMJhplAF.js",
"houseplan-assets/houseplan-editor-runtime-DBsb8Jgp.js",
"houseplan-assets/houseplan-onboarding-runtime-DuBh8LrV.js"
"houseplan-assets/backdrop-pick-78QbziGu.js",
"houseplan-assets/de-CNmZS2Bl.js",
"houseplan-assets/editor-CcKD8T48.js",
"houseplan-assets/fr-ChOylnG-.js",
"houseplan-assets/houseplan-editor-runtime-BKOSee7A.js",
"houseplan-assets/houseplan-onboarding-runtime-B8zj38Pl.js"
],
"lazyGzipBytes": 205825,
"lazyGzipBytes": 205820,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-BDzJ-1w0.js",
"houseplan-assets/editor-BcMfn7wA.js",
"houseplan-assets/houseplan-editor-runtime-DBsb8Jgp.js"
"houseplan-assets/backdrop-pick-78QbziGu.js",
"houseplan-assets/editor-CcKD8T48.js",
"houseplan-assets/houseplan-editor-runtime-BKOSee7A.js"
],
"lazyEditorGzipBytes": 148717,
"lazyEditorGzipBytes": 148720,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-BDzJ-1w0.js",
"houseplan-assets/houseplan-onboarding-runtime-DuBh8LrV.js"
"houseplan-assets/backdrop-pick-78QbziGu.js",
"houseplan-assets/houseplan-onboarding-runtime-B8zj38Pl.js"
],
"lazyOnboardingGzipBytes": 14030,
"lazyOnboardingGzipBytes": 14028,
"lazyLocaleFiles": [
"houseplan-assets/de-h-9CiLUA.js",
"houseplan-assets/fr-JMJhplAF.js"
"houseplan-assets/de-CNmZS2Bl.js",
"houseplan-assets/fr-ChOylnG-.js"
],
"lazyLocaleGzipBytes": 50148,
"lazyLocaleGzipBytes": 50142,
"files": [
{
"path": "houseplan-assets/backdrop-pick-BDzJ-1w0.js",
"sha256": "056fb0975dfa0f753ff8a606d2276add24f02a92517d3536f02be93857970e83",
"path": "houseplan-assets/backdrop-pick-78QbziGu.js",
"sha256": "26a9c96fdb60fb7fe12df6a09b2c27c26b2b90479cf4968bfc1b32a895c4e944",
"rawBytes": 20636,
"gzipBytes": 7070,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DdbeLYki.js"
"houseplan-assets/houseplan-card-BGFqEA3T.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-h-9CiLUA.js",
"sha256": "de934d5d3553dd90a853777514c7ec6e67b765adbd4b379bdd5e15b80448bc97",
"path": "houseplan-assets/de-CNmZS2Bl.js",
"sha256": "7850782105783e0dcdb910c7071a101e9a02b00c1c2015dbc05baeb3c47e15cc",
"rawBytes": 83981,
"gzipBytes": 25345,
"gzipBytes": 25342,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-BcMfn7wA.js",
"sha256": "367dd4bd9ef49463e12b3d9f405c5aa221d34fac6559f886a98c27946f4b341e",
"path": "houseplan-assets/editor-CcKD8T48.js",
"sha256": "61452fe308e80615a91df46691524b34a71022e6aa9168b3010acf8608899c45",
"rawBytes": 3826,
"gzipBytes": 1581,
"gzipBytes": 1582,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DdbeLYki.js"
"houseplan-assets/houseplan-card-BGFqEA3T.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-JMJhplAF.js",
"sha256": "d11639e11eac93a821abd81307203d8ca00ab14d3fade8e4049be725fee60df5",
"path": "houseplan-assets/fr-ChOylnG-.js",
"sha256": "11a94a647483fa90c9b79f9f45206ffc5f9d5008cb0c2227f53654399ad8e574",
"rawBytes": 86212,
"gzipBytes": 24803,
"gzipBytes": 24800,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DdbeLYki.js",
"sha256": "85dbbe6644990d350c9658b44514ae5db444203ed537c2d01726f267d2766679",
"rawBytes": 1032271,
"gzipBytes": 289578,
"path": "houseplan-assets/houseplan-card-BGFqEA3T.js",
"sha256": "2f556d1f30b8368e1a866a171031f42968b0c6f00aa40d9f25d650b8bc469fa6",
"rawBytes": 1032340,
"gzipBytes": 289594,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-h-9CiLUA.js",
"houseplan-assets/editor-BcMfn7wA.js",
"houseplan-assets/fr-JMJhplAF.js",
"houseplan-assets/houseplan-editor-runtime-DBsb8Jgp.js",
"houseplan-assets/houseplan-onboarding-runtime-DuBh8LrV.js"
"houseplan-assets/de-CNmZS2Bl.js",
"houseplan-assets/editor-CcKD8T48.js",
"houseplan-assets/fr-ChOylnG-.js",
"houseplan-assets/houseplan-editor-runtime-BKOSee7A.js",
"houseplan-assets/houseplan-onboarding-runtime-B8zj38Pl.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-DBsb8Jgp.js",
"sha256": "5889a92ae87a014f6e858b94da6f4683c10637384b44263156d23e2e52fea3e3",
"path": "houseplan-assets/houseplan-editor-runtime-BKOSee7A.js",
"sha256": "8361e79183dfc42c6aae966758c29018b4143d7cfae6a86d7bba886f68238dd1",
"rawBytes": 543272,
"gzipBytes": 140066,
"gzipBytes": 140068,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BDzJ-1w0.js",
"houseplan-assets/houseplan-card-DdbeLYki.js"
"houseplan-assets/backdrop-pick-78QbziGu.js",
"houseplan-assets/houseplan-card-BGFqEA3T.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-DuBh8LrV.js",
"sha256": "68031aa68413c961ea1dd1c10fd0b3fe40775425dad227ea680ddc22c2e96f09",
"path": "houseplan-assets/houseplan-onboarding-runtime-B8zj38Pl.js",
"sha256": "c8eed0234c0928fcf49cca348ab38640f4662c85d2f3fc9b84a68f28e26ac214",
"rawBytes": 28088,
"gzipBytes": 6960,
"gzipBytes": 6958,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BDzJ-1w0.js",
"houseplan-assets/houseplan-card-DdbeLYki.js"
"houseplan-assets/backdrop-pick-78QbziGu.js",
"houseplan-assets/houseplan-card-BGFqEA3T.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "cacb921be9d92f674ba3fca7bd7bf07427d1a6e2e626b8a36d56aca54ae1d7e9",
"sha256": "588c8aef7a904a7f4522ee1fa04949f3dc29dec8e9c3910234ff76d996a151cf",
"rawBytes": 1183,
"gzipBytes": 797,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-DdbeLYki.js"
"houseplan-assets/houseplan-card-BGFqEA3T.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="cfeec183260c075204dd0fdd0333fdf69f1bab8b9e9142483b49b645e0526710";import{b as e,l as o,t,d8 as s,A as a,d9 as i,da as l,db as n,E as r,c}from"./houseplan-card-DdbeLYki.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="baf59953cfd6eb3543f7da5af21787b20dc76f136b74782ae917f811d2fc1ab5";import{b as e,l as o,t,d8 as s,A as a,d9 as i,da as l,db as n,E as r,c}from"./houseplan-card-BGFqEA3T.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>h[e.name]||e.name}
.computeLabel=${e=>f[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(f.slice(0,3))}
${d(h.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
${d(h.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="cfeec183260c075204dd0fdd0333fdf69f1bab8b9e9142483b49b645e0526710";try{await import("./houseplan-assets/houseplan-card-DdbeLYki.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="baf59953cfd6eb3543f7da5af21787b20dc76f136b74782ae917f811d2fc1ab5";try{await import("./houseplan-assets/houseplan-card-BGFqEA3T.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
+11 -1
View File
@@ -125,7 +125,12 @@ const result = await page.evaluate(async ({ version, previewText, previewSha })
const supportCallsBeforeOpen = calls.length;
await open();
const dialog = root().querySelector('#support-dialog');
const touchRect = root().querySelector('.support-button').getBoundingClientRect();
const supportButton = root().querySelector('.support-button');
const settingsButton = supportButton?.previousElementSibling;
const touchRect = supportButton.getBoundingClientRect();
const settingsRect = settingsButton?.getBoundingClientRect();
const message = dialog?.querySelector('#support-message');
const contact = dialog?.querySelector('#support-contact');
const guide = dialog?.querySelector('#support-docs-heading + a');
out.openIsLocal = calls.length === supportCallsBeforeOpen;
out.aboutAndEnglishGuide = !!dialog?.querySelector('.aboutver')
@@ -135,6 +140,11 @@ const result = await page.evaluate(async ({ version, previewText, previewSha })
&& dialog?.querySelector('#support-message')?.value === ''
&& dialog?.querySelector('.supportattach input')?.checked === false;
out.touchTarget = touchRect.width >= 44 && touchRect.height >= 44;
out.headerActionsMatch = !!settingsRect
&& Math.abs(touchRect.width - settingsRect.width) < 0.01
&& Math.abs(touchRect.height - settingsRect.height) < 0.01;
out.messageSurfaceMatchesContact = !!message && !!contact
&& getComputedStyle(message).backgroundColor === getComputedStyle(contact).backgroundColor;
out.noHorizontalOverflow = dialog.scrollWidth <= dialog.clientWidth + 1
&& dialog.querySelector('.supportbody').scrollWidth
<= dialog.querySelector('.supportbody').clientWidth + 1;
+54 -54
View File
@@ -1,125 +1,125 @@
{
"schema": 1,
"fingerprint": "cfeec183260c075204dd0fdd0333fdf69f1bab8b9e9142483b49b645e0526710",
"fingerprint": "baf59953cfd6eb3543f7da5af21787b20dc76f136b74782ae917f811d2fc1ab5",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-DdbeLYki.js",
"houseplan-assets/houseplan-card-BGFqEA3T.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 290375,
"initialViewGzipBytes": 290391,
"lazyFiles": [
"houseplan-assets/backdrop-pick-BDzJ-1w0.js",
"houseplan-assets/de-h-9CiLUA.js",
"houseplan-assets/editor-BcMfn7wA.js",
"houseplan-assets/fr-JMJhplAF.js",
"houseplan-assets/houseplan-editor-runtime-DBsb8Jgp.js",
"houseplan-assets/houseplan-onboarding-runtime-DuBh8LrV.js"
"houseplan-assets/backdrop-pick-78QbziGu.js",
"houseplan-assets/de-CNmZS2Bl.js",
"houseplan-assets/editor-CcKD8T48.js",
"houseplan-assets/fr-ChOylnG-.js",
"houseplan-assets/houseplan-editor-runtime-BKOSee7A.js",
"houseplan-assets/houseplan-onboarding-runtime-B8zj38Pl.js"
],
"lazyGzipBytes": 205825,
"lazyGzipBytes": 205820,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-BDzJ-1w0.js",
"houseplan-assets/editor-BcMfn7wA.js",
"houseplan-assets/houseplan-editor-runtime-DBsb8Jgp.js"
"houseplan-assets/backdrop-pick-78QbziGu.js",
"houseplan-assets/editor-CcKD8T48.js",
"houseplan-assets/houseplan-editor-runtime-BKOSee7A.js"
],
"lazyEditorGzipBytes": 148717,
"lazyEditorGzipBytes": 148720,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-BDzJ-1w0.js",
"houseplan-assets/houseplan-onboarding-runtime-DuBh8LrV.js"
"houseplan-assets/backdrop-pick-78QbziGu.js",
"houseplan-assets/houseplan-onboarding-runtime-B8zj38Pl.js"
],
"lazyOnboardingGzipBytes": 14030,
"lazyOnboardingGzipBytes": 14028,
"lazyLocaleFiles": [
"houseplan-assets/de-h-9CiLUA.js",
"houseplan-assets/fr-JMJhplAF.js"
"houseplan-assets/de-CNmZS2Bl.js",
"houseplan-assets/fr-ChOylnG-.js"
],
"lazyLocaleGzipBytes": 50148,
"lazyLocaleGzipBytes": 50142,
"files": [
{
"path": "houseplan-assets/backdrop-pick-BDzJ-1w0.js",
"sha256": "056fb0975dfa0f753ff8a606d2276add24f02a92517d3536f02be93857970e83",
"path": "houseplan-assets/backdrop-pick-78QbziGu.js",
"sha256": "26a9c96fdb60fb7fe12df6a09b2c27c26b2b90479cf4968bfc1b32a895c4e944",
"rawBytes": 20636,
"gzipBytes": 7070,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DdbeLYki.js"
"houseplan-assets/houseplan-card-BGFqEA3T.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-h-9CiLUA.js",
"sha256": "de934d5d3553dd90a853777514c7ec6e67b765adbd4b379bdd5e15b80448bc97",
"path": "houseplan-assets/de-CNmZS2Bl.js",
"sha256": "7850782105783e0dcdb910c7071a101e9a02b00c1c2015dbc05baeb3c47e15cc",
"rawBytes": 83981,
"gzipBytes": 25345,
"gzipBytes": 25342,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-BcMfn7wA.js",
"sha256": "367dd4bd9ef49463e12b3d9f405c5aa221d34fac6559f886a98c27946f4b341e",
"path": "houseplan-assets/editor-CcKD8T48.js",
"sha256": "61452fe308e80615a91df46691524b34a71022e6aa9168b3010acf8608899c45",
"rawBytes": 3826,
"gzipBytes": 1581,
"gzipBytes": 1582,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DdbeLYki.js"
"houseplan-assets/houseplan-card-BGFqEA3T.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-JMJhplAF.js",
"sha256": "d11639e11eac93a821abd81307203d8ca00ab14d3fade8e4049be725fee60df5",
"path": "houseplan-assets/fr-ChOylnG-.js",
"sha256": "11a94a647483fa90c9b79f9f45206ffc5f9d5008cb0c2227f53654399ad8e574",
"rawBytes": 86212,
"gzipBytes": 24803,
"gzipBytes": 24800,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DdbeLYki.js",
"sha256": "85dbbe6644990d350c9658b44514ae5db444203ed537c2d01726f267d2766679",
"rawBytes": 1032271,
"gzipBytes": 289578,
"path": "houseplan-assets/houseplan-card-BGFqEA3T.js",
"sha256": "2f556d1f30b8368e1a866a171031f42968b0c6f00aa40d9f25d650b8bc469fa6",
"rawBytes": 1032340,
"gzipBytes": 289594,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-h-9CiLUA.js",
"houseplan-assets/editor-BcMfn7wA.js",
"houseplan-assets/fr-JMJhplAF.js",
"houseplan-assets/houseplan-editor-runtime-DBsb8Jgp.js",
"houseplan-assets/houseplan-onboarding-runtime-DuBh8LrV.js"
"houseplan-assets/de-CNmZS2Bl.js",
"houseplan-assets/editor-CcKD8T48.js",
"houseplan-assets/fr-ChOylnG-.js",
"houseplan-assets/houseplan-editor-runtime-BKOSee7A.js",
"houseplan-assets/houseplan-onboarding-runtime-B8zj38Pl.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-DBsb8Jgp.js",
"sha256": "5889a92ae87a014f6e858b94da6f4683c10637384b44263156d23e2e52fea3e3",
"path": "houseplan-assets/houseplan-editor-runtime-BKOSee7A.js",
"sha256": "8361e79183dfc42c6aae966758c29018b4143d7cfae6a86d7bba886f68238dd1",
"rawBytes": 543272,
"gzipBytes": 140066,
"gzipBytes": 140068,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BDzJ-1w0.js",
"houseplan-assets/houseplan-card-DdbeLYki.js"
"houseplan-assets/backdrop-pick-78QbziGu.js",
"houseplan-assets/houseplan-card-BGFqEA3T.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-DuBh8LrV.js",
"sha256": "68031aa68413c961ea1dd1c10fd0b3fe40775425dad227ea680ddc22c2e96f09",
"path": "houseplan-assets/houseplan-onboarding-runtime-B8zj38Pl.js",
"sha256": "c8eed0234c0928fcf49cca348ab38640f4662c85d2f3fc9b84a68f28e26ac214",
"rawBytes": 28088,
"gzipBytes": 6960,
"gzipBytes": 6958,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BDzJ-1w0.js",
"houseplan-assets/houseplan-card-DdbeLYki.js"
"houseplan-assets/backdrop-pick-78QbziGu.js",
"houseplan-assets/houseplan-card-BGFqEA3T.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "cacb921be9d92f674ba3fca7bd7bf07427d1a6e2e626b8a36d56aca54ae1d7e9",
"sha256": "588c8aef7a904a7f4522ee1fa04949f3dc29dec8e9c3910234ff76d996a151cf",
"rawBytes": 1183,
"gzipBytes": 797,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-DdbeLYki.js"
"houseplan-assets/houseplan-card-BGFqEA3T.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="cfeec183260c075204dd0fdd0333fdf69f1bab8b9e9142483b49b645e0526710";import{b as e,l as o,t,d8 as s,A as a,d9 as i,da as l,db as n,E as r,c}from"./houseplan-card-DdbeLYki.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="baf59953cfd6eb3543f7da5af21787b20dc76f136b74782ae917f811d2fc1ab5";import{b as e,l as o,t,d8 as s,A as a,d9 as i,da as l,db as n,E as r,c}from"./houseplan-card-BGFqEA3T.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>h[e.name]||e.name}
.computeLabel=${e=>f[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(f.slice(0,3))}
${d(h.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
${d(h.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="cfeec183260c075204dd0fdd0333fdf69f1bab8b9e9142483b49b645e0526710";try{await import("./houseplan-assets/houseplan-card-DdbeLYki.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="baf59953cfd6eb3543f7da5af21787b20dc76f136b74782ae917f811d2fc1ab5";try{await import("./houseplan-assets/houseplan-card-BGFqEA3T.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
+4
View File
@@ -2,6 +2,10 @@
## Unreleased
- **Help & feedback** now uses the same 44 px header button as General settings,
and its message field uses the ordinary input surface instead of a grey tint
([#415](https://github.com/Matysh/houseplan-card/issues/415)).
## v1.70.0-beta.3 — 2026-09-02
- The new **Help & feedback** dialog keeps version, documentation and project
+5
View File
@@ -8,6 +8,11 @@
## Не выпущено
- Кнопка **«Помощь и обратная связь»** теперь совпадает по размеру 44 px с
кнопкой «Общие настройки», а поле сообщения использует обычный фон поля ввода
вместо серой заливки
([#415](https://github.com/Matysh/houseplan-card/issues/415)).
## v1.70.0-beta.3 — 2026-09-02
- Новый диалог **«Помощь и обратная связь»** объединяет версию, документацию и
Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 116 KiB

+21 -24
View File
@@ -3,7 +3,7 @@
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": "oxipng 10.2.0",
"sourceFingerprint": "c3e3e7dba1910e09c702472e6ea09d7ef13b91e082c09d2f9474437c776fb26e",
"sourceFingerprint": "51672b157f9dfe4f36747b0c8963b079295b67ec3c93e33f3c18287aca59fa90",
"captureScriptSha256": "135dd9a566653b05cae3010d7861fca6453daef0ecdcdf1ba10059e65f0d4de8",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,8 +15,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3e3e7dba1910e09c702472e6ea09d7ef13b91e082c09d2f9474437c776fb26e",
"imageSha256": "f4ecb7fd1f107ccda6c8a1e90be1bce0f2690f93e5e42776ca783297f4143135"
"sourceSha256": "51672b157f9dfe4f36747b0c8963b079295b67ec3c93e33f3c18287aca59fa90",
"imageSha256": "e0d39cfae61d194e85147ca36499abc5a173efdf00195f2f00a7111866dd54bd"
},
"view-touch": {
"file": "02-view-touch.png",
@@ -26,7 +26,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3e3e7dba1910e09c702472e6ea09d7ef13b91e082c09d2f9474437c776fb26e",
"sourceSha256": "51672b157f9dfe4f36747b0c8963b079295b67ec3c93e33f3c18287aca59fa90",
"imageSha256": "946801f6475a40c0e31a70e4b67eaa34c6f70df5329a63915f445e6e4811ca6a"
},
"space-create": {
@@ -37,7 +37,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3e3e7dba1910e09c702472e6ea09d7ef13b91e082c09d2f9474437c776fb26e",
"sourceSha256": "51672b157f9dfe4f36747b0c8963b079295b67ec3c93e33f3c18287aca59fa90",
"imageSha256": "617b51b3648498787b5039980c9f3eceb75ba56ed63a1a20e616bc05bc304362"
},
"room-contour-close": {
@@ -48,8 +48,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3e3e7dba1910e09c702472e6ea09d7ef13b91e082c09d2f9474437c776fb26e",
"imageSha256": "9a79b7e517741e74701d1172ec3c66a340aaa9049a7f7a7b2fe86f4c3a2eccf0"
"sourceSha256": "51672b157f9dfe4f36747b0c8963b079295b67ec3c93e33f3c18287aca59fa90",
"imageSha256": "f81cfc6b9e4a923d36031c514f02927a33e8b8b2c75f007e8aed45dee16e491d"
},
"plan-context-tray": {
"file": "05-plan-context-tray.png",
@@ -59,8 +59,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3e3e7dba1910e09c702472e6ea09d7ef13b91e082c09d2f9474437c776fb26e",
"imageSha256": "8fb569f326627700e08156c1ca35a4042bcb529a955663b54228d04357c881d0"
"sourceSha256": "51672b157f9dfe4f36747b0c8963b079295b67ec3c93e33f3c18287aca59fa90",
"imageSha256": "6b8bfaada24ff1fadcda1b6946a5894d90d981e2c7f7096273e23bfa58ac3218"
},
"device-editor": {
"file": "06-device-editor.png",
@@ -70,8 +70,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3e3e7dba1910e09c702472e6ea09d7ef13b91e082c09d2f9474437c776fb26e",
"imageSha256": "720fc3aa2070d1fcf6907091941751c623f30cc6a001ff95e22de9be998136df"
"sourceSha256": "51672b157f9dfe4f36747b0c8963b079295b67ec3c93e33f3c18287aca59fa90",
"imageSha256": "a13536d93f5bf70207e7ece0d3f4617d4ba919cb556342de67eaa81961589fb2"
},
"device-display-preview": {
"file": "06-device-display-preview.png",
@@ -81,8 +81,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3e3e7dba1910e09c702472e6ea09d7ef13b91e082c09d2f9474437c776fb26e",
"imageSha256": "b2294c6de0b18ef386b5a2d18422a866b84d095f133d31e56e307664e31e5bc0"
"sourceSha256": "51672b157f9dfe4f36747b0c8963b079295b67ec3c93e33f3c18287aca59fa90",
"imageSha256": "d93be8761de4451fa792cc4415789b4624f38638eccc3623502ea61dcf3b3a13"
},
"background-editor": {
"file": "07-background-editor.png",
@@ -92,8 +92,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3e3e7dba1910e09c702472e6ea09d7ef13b91e082c09d2f9474437c776fb26e",
"imageSha256": "8dbc5864124c4bddbe076722a1503e666c40f432cfe7fff86e6d196c428365f3"
"sourceSha256": "51672b157f9dfe4f36747b0c8963b079295b67ec3c93e33f3c18287aca59fa90",
"imageSha256": "054170fd9ef45762b602b4d5c9c3b9ea9724858be61af137970c246f485c13bb"
},
"room-card": {
"file": "08-room-card.png",
@@ -103,8 +103,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3e3e7dba1910e09c702472e6ea09d7ef13b91e082c09d2f9474437c776fb26e",
"imageSha256": "d09709ae1479cfaa1902068cb2a6015dfee3b58f1bbd349bafaa2c0592812463"
"sourceSha256": "51672b157f9dfe4f36747b0c8963b079295b67ec3c93e33f3c18287aca59fa90",
"imageSha256": "2ae4a58853d98e10d12456b2078ec2f6a0b597722c8310d7b016abd2bc42561e"
},
"device-info": {
"file": "09-device-info.png",
@@ -114,15 +114,13 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3e3e7dba1910e09c702472e6ea09d7ef13b91e082c09d2f9474437c776fb26e",
"imageSha256": "c0b20a31c2987c8737e7b746c7d358e6c97231ea7e06d73050a2d3aebb216558"
"sourceSha256": "51672b157f9dfe4f36747b0c8963b079295b67ec3c93e33f3c18287aca59fa90",
"imageSha256": "cb37f7eefd936f98ef44969b21dfe8e40ee27d1f558abe8d53c0223f6488b8a6"
}
},
"acceptance": {
"declared": [
"view-desktop",
"view-touch",
"space-create",
"room-contour-close",
"plan-context-tray",
"device-editor",
@@ -131,8 +129,7 @@
"room-card",
"device-info"
],
"witnesses": 0,
"floor": 1,
"witnessesSkippedBecause": "сменились флаги растеризации и обрезка стала целой (#410)"
"witnesses": 2,
"floor": 1
}
}
+178
View File
@@ -0,0 +1,178 @@
# CODE-REVIEW-415-r1
Issue: #415 — «Полиш диалога помощи: размер кнопки и белый фон сообщения»
Ветка: `issue/415-support-visual-polish`
SHA ревью: `2fc8c1203782026bb45647ad54ef11857f7eb2c3`
Трек: `trivial` (AC в теле issue, без файла в `docs/specs/`) — этап ревью: **code**.
Заход: r1 (первый цикл, предыдущих раундов и вердиктов нет — разбор полный).
## Скоуп
Issue фиксирует два визуальных расхождения после #43:
- **AC1 (smoke):** кнопки «Общие настройки» и «Помощь и обратная связь» в одной
шапке должны иметь одинаковые фактические ширину и высоту; отдельный размер
для кнопки помощи не задаётся.
- **AC2 (smoke + golden перед бетой):** поле «Сообщение» должно использовать ту
же поверхность ввода, что поле контакта (белую в светлой теме, штатную
тёмную — в тёмной); read-only preview (`.supportraw`) не меняется.
Затронутые файлы по диффу: `src/houseplan-card.ts`, `src/styles/plan.styles.ts`,
`src/styles/dialogs.styles.ts`, `demo/smoke_support_feedback.mjs`,
`test/support-feedback.test.mjs`, оба changelog, сгенерированные бандлы (класс D).
Без i18n, без миграции, без geometry — совпадает с заявленным в issue «Затрагиваемое».
## Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Типы | `npx tsc --noEmit` | зелёный, без вывода |
| Юнит-тесты | `npm test` | 1724 passed / 0 failed / 1 skipped (совпадает с хендоффом автора) |
| Сборка + сверка бандла | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` | идентичны |
| Новый `any` | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | 7 добавленных строк в 3 файлах, новых `any` нет |
| Выбор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | см. раздел ниже |
| Целевой смок (AC1+AC2) | `node demo/smoke_support_feedback.mjs` | **OK**, включая новые поля `headerActionsMatch`, `messageSurfaceMatchesContact` |
| Смок-спецпроверка на падение | тот же смок на `src/*` из родительского коммита `afe5e989` | `headerActionsMatch: false`, `messageSurfaceMatchesContact: false` — тест умеет падать |
| Документационный гейт | `node scripts/check-docs.mjs` | **ERROR: screenshot source fingerprint is stale** |
| То же на `origin/dev` (`afe5e989`) | `node scripts/check-docs.mjs` (с исходниками родителя) | зелёный — подтверждает, что именно этот диф делает отпечаток устаревшим |
Рабочее дерево после всех экспериментов с временным `git checkout` родительских
версий файлов возвращено в исходное состояние; `git status --porcelain` пуст,
`git rev-parse HEAD` = `2fc8c120...`, сгенерированные файлы побайтово совпадают
с `HEAD` (`cmp` зелёный).
### Выбор браузерных смоков (`scripts/smoke-select.mjs`)
```
Изменено файлов src/**: 3 · символов проекта на изменённых строках: 2
Матрица: 213 смоков · порог «широкого» символа: больше 42 смоков
Прямое совпадение (9):
demo/smoke_backup_transfer.mjs ← _openSettingsDialog
demo/smoke_bg_color.mjs ← _openSettingsDialog
demo/smoke_color_picker_consumers.mjs ← _openSettingsDialog
demo/smoke_dialog_zombie.mjs ← _openSettingsDialog
demo/smoke_esc_dialogs.mjs ← _openSettingsDialog
demo/smoke_general_settings.mjs ← _openSettingsDialog
demo/smoke_ha_controls.mjs ← _openSettingsDialog
demo/smoke_help_affordance.mjs ← _openSettingsDialog
demo/smoke_sun.mjs ← _openSettingsDialog
```
Инструмент не назвал `demo/smoke_support_feedback.mjs` (совпадение идёт по
имени JS-символа на изменённой строке, а не по предметной области; сам смок
селектор `.support-button`, а не `_openSupportDialog`, не матчит построчно) —
он взят отдельно, так как прямо назван в AC1/AC2.
Решение по 9 найденным смокам: причина совпадения — обе кнопки шапки лежат на
строке, где стоит вызов `_openSettingsDialog`/`_openSupportDialog`; сама функция
не менялась, менялся только список CSS-классов элемента. Прогнал два самых
предметно близких — `smoke_general_settings.mjs` (диалог настроек целиком) и
`smoke_help_affordance.mjs` (соседняя механика помощи/подсказок) — оба **OK**
без регрессий в открытии диалогов и фокусе. Остальные 7 (`backup_transfer`,
`bg_color`, `color_picker_consumers`, `dialog_zombie`, `esc_dialogs`,
`ha_controls`, `sun`) не прогонял: они используют `_openSettingsDialog` только
как способ добраться до необязанной с этим issue функциональности, а
изменение — чисто визуальное (класс `header-action`, без изменения обработчика
клика или разметки диалогов). Полный прогон всех 213 смоков — предрелизный
гейт, не гейт ревью (§8), и здесь ему делать нечего: диф не «задевает всё».
### Что не проверял и почему
- **`npm run golden:verify`** — не прогонял. AC2 сам относит golden-проверку к
«перед бетой», а не к код-ревью; в `demo/golden/` нет сценариев, ссылающихся
на `.support-button`, `.header-action` или `.supportmessage` — совпадает с
этим разделением.
- **`node scripts/model-invariants.mjs`** — не прогонял. Диф не касается
геометрии, рёбер, `layout`, `marker.space`, `open_spans`.
- **`python -m pytest tests_backend`** — не прогонял. `custom_components/**/*.py`
не тронут.
- **Performance-профили** — не названы в AC, не прогонял.
- Остальные 7 из 9 «прямое совпадение» смоков — см. решение выше.
## Находки
### High — check-docs: отпечаток скриншотов документации устарел
`node scripts/check-docs.mjs` падает на этом SHA:
`ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs`.
Этот гейт — часть `validate.yml` (`node scripts/check-docs.mjs --external`),
то есть часть CI Validate, а не только локальная рекомендация. Проверено, что
регрессия внесена именно этим коммитом: на исходниках родителя
(`afe5e989`, = `origin/dev`) тот же скрипт зелёный
(«Documentation checks passed»); после возврата `src/houseplan-card.ts`,
`src/styles/plan.styles.ts`, `src/styles/dialogs.styles.ts` к текущему SHA
скрипт снова красный. Причина устройства гейта задокументирована в PROCESS.md
§8: отпечаток считается по всему `src/**`, поэтому любая правка фронтенда
делает его устаревшим безусловно — «выбирать тут нечего». Ровно этот пропуск
уже дважды стоил продукту красного `docs`-джоба на `dev` (#230, #234, #237);
хендофф автора («Передача на код-ревью») не упоминает пересъёмку скриншотов
вообще.
**Воспроизведение:**
```
$ node scripts/check-docs.mjs
ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs
```
**Почему High:** гейт входит в обязательную часть CI (`validate.yml`) и
детерминированно красный на этом SHA; слияние в `dev` в этом состоянии
повторяет уже случавшийся инцидент (`dev` с красным `docs`-джобом до следующей
задачи). Это не вопрос вкуса и не техническое несогласие — факт, проверенный
запуском.
**Что нужно для закрытия:** прогнать джобу `Docs screenshots`
(`workflow_dispatch`), принять артефакт локально
`npm run docs:accept -- --reviewed --from=<распакованный артефакт>` и
закоммитить эталоны вместе с задачей (класс D, трейлер `Baseline-Reviewed`).
Это не в моих полномочиях как ревьюера (я не запускаю релизные workflow и не
пишу в репозиторий) и не то, что можно решить локальной правкой кода — гейт
обслуживается человеком/автором по регламенту §8.
## Что проверено и корректно
- **AC1 (кнопки одного размера).** `.support-button` больше не несёт стилей
размера — правило переименовано в `.header-action`
(`src/styles/plan.styles.ts:188`, было `.support-button {min-width:44px...}`)
и навешено на **обе** кнопки шапки (`src/houseplan-card.ts:11496,11499`);
класс `support-button` остался только как селектор-хук для JS/смоков, без
собственных правил стиля (проверено чтением — единственное вхождение строки
`support-button` в `src/**` осталось в шаблоне, ни одного в `*.styles.ts`).
Доказано автотестом: `out.headerActionsMatch` в
`demo/smoke_support_feedback.mjs` сравнивает `getBoundingClientRect()` обеих
кнопок с допуском 0.01px и **умеет падать** — воспроизведено на исходниках
родительского коммита (оба новых поля дают `false`).
- **AC2 (единая поверхность поля).** `.supportmessage` получил явный
`background: var(--hp-bg)` (`src/styles/dialogs.styles.ts:1189-1192`), что
побайтово тот же токен, что использует поле контакта `.namein`
(`src/styles/dialogs.styles.ts:353-355`, тоже `var(--hp-bg)`) — источник
значения один (переменная темы), а не два литерала, вычисляющих одно и то же
число/цвет. `.supportraw` (read-only preview) не тронут: делит с
`.supportmessage` только общий базовый блок правил (border/padding/шрифт), а
собственный фон (`color-mix(...)`) остаётся прежним — соответствует «preview
не меняется». Доказано автотестом `out.messageSurfaceMatchesContact`
(сравнение `getComputedStyle(...).backgroundColor`), тоже проверено на
падение тем же способом.
- **Трейлеры и changelog.** Коммит `2fc8c120` несёт `Issue: #415` и
`User-Visible: yes`; оба changelog (`docs/CHANGELOG.md`,
`docs/CHANGELOG.ru.md`) правлены в том же коммите, формулировки совпадают с
терминологией `docs/USER-GUIDE.ru.md` («Общие настройки», «Помощь и
обратная связь» — см. `docs/USER-GUIDE.ru.md:58,1920`).
- **Тестовый контракт остался честным.** `test/support-feedback.test.mjs`
проверяет ровно две вещи по коду (не только по смоку): что в шапке ровно два
вхождения `header-action` и что `.header-action`/`.supportmessage` содержат
нужные правила — статический дубль-контроль поверх браузерного смока.
- **Никакого расширения скоупа.** Диф ограничен двумя классами CSS и одной
строкой фона; ни новых сценариев, ни миграции, ни i18n, ни touch/perf.
- **Одно число — один источник (44px, фон).** Оба случая сведены к общему
правилу (`.header-action`, `var(--hp-bg)`), а не к двум местам, вычисляющим
одно значение по отдельности — ровно то, о чём предупреждает §8.
## Вывод
Единственная находка — High, блокирующая: `check-docs` детерминированно красный
на этом SHA и является частью CI Validate. AC1 и AC2 при этом выполнены и
доказаны автотестом, который умеет падать. Возврат автору для прогона джобы
скриншотов и коммита эталонов; повторный цикл — по дельте (§2.10), с разделами
«Закрытие раунда r1» и «Унаследовано из r1».
+194
View File
@@ -0,0 +1,194 @@
# CODE-REVIEW-415-r2
Issue: #415 — «Полиш диалога помощи: размер кнопки и белый фон сообщения»
Ветка: `issue/415-support-visual-polish`
SHA ревью: `b0122803737d47950f8c2295b479afa53b8369e0`
Трек: `trivial` (AC в теле issue, без файла в `docs/specs/`) — этап ревью: **code**.
Заход: r2 · блокирующих циклов израсходовано 1 из 2 (зелёный вердикт этого
захода цикл не образует, #227).
## Почему разбор снова полный, а не только по дельте
Между r1 и r2 ветка была **перебазирована на ушедший вперёд `dev`**: r1 стоял на
`afe5e989` (тогдашний `origin/dev`), сейчас `origin/dev` = `206732e9` («ci:
refuse a review round that cites an unreachable SHA», issue #413, чужая
задача — попала в базу только ребейзом). Старый SHA r1
(`2fc8c1203782026bb45647ad54ef11857f7eb2c3`) в дереве больше не существует
(`git cat-file -t` — «could not get object info»): команда `git diff <тот
SHA>..HEAD` из §2.10 не выполнима буквально. Это ровно случай §7.2 «ребейз на
ушедший вперёд `dev` — после ребейза это другой код», поэтому материал этого
раунда — `git diff origin/dev...HEAD` целиком, а не узкая дельта поверх
осиротевшего SHA.
Продуктовый диф при этом **побайтово не менялся** — сверено построчно с телом
CODE-REVIEW-415-r1 (см. ниже раздел «Закрытие раунда r1»): те же три строки в
`src/houseplan-card.ts`, `src/styles/plan.styles.ts`,
`src/styles/dialogs.styles.ts`. Новый код в диапазоне — только пересъёмка
скриншотов и сам документ r1.
## Скоуп
Issue фиксирует два визуальных расхождения после #43:
- **AC1 (smoke):** кнопки «Общие настройки» и «Помощь и обратная связь» в одной
шапке должны иметь одинаковые фактические ширину и высоту; отдельный размер
для кнопки помощи не задаётся.
- **AC2 (smoke + golden перед бетой):** поле «Сообщение» должно использовать ту
же поверхность ввода, что поле контакта (белую в светлой теме, штатную
тёмную — в тёмной); read-only preview (`.supportraw`) не меняется.
Диапазон `origin/dev...HEAD` (3 коммита):
`d1691f4b` fix: align support controls (класс A+B+C, `Issue: #415`,
`User-Visible: yes`) → `c6fac8bf` docs: review document for #415 (класс C,
`User-Visible: no`) → `b0122803` docs: refresh reviewed support screenshots
(класс C — `docs/images/**`, не класс D; `User-Visible: no`).
Затронуто по коду: `src/houseplan-card.ts`, `src/styles/plan.styles.ts`,
`src/styles/dialogs.styles.ts`, `demo/smoke_support_feedback.mjs`,
`test/support-feedback.test.mjs`, оба changelog, бандлы (класс D),
`docs/images/*.png` + `docs/images/screenshots.json` (класс C, пересъёмка).
Без i18n, без миграции, без geometry — совпадает с заявленным в issue
«Затрагиваемое».
## Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Типы | `npx tsc --noEmit` | зелёный, без вывода |
| Юнит-тесты | `npm test` | 1729 passed / 0 failed / 1 skipped (совпадает с хендоффом автора) |
| Сборка | `npm run build` | зелёный |
| Сверка бандла (3 копии) | `npm run bundle:sync` → `cmp dist/houseplan-card.js custom_components/.../houseplan-card.js`, `cmp dist/houseplan-assets.json custom_components/.../houseplan-assets.json`, `git status --porcelain` | все три идентичны, рабочее дерево чисто |
| Документационный гейт | `node scripts/check-docs.mjs` | **зелёный** («Documentation checks passed (7 files, 10 external links)») — была High-находка r1, закрыта |
| Новый `any` | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | 7 добавленных строк в 3 файлах, новых `any` нет |
| Оффлайн-гейт процесса | `node scripts/process-gate.mjs` | «гейт пройден, предупреждений 1» — WARN п.3 (нет `docs/specs/415-*.md`) ожидаемо для трека `trivial` |
| Выбор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | см. раздел ниже — идентичен выбору r1 (продуктовый диф не менялся) |
| Целевой смок (AC1+AC2) | `node demo/smoke_support_feedback.mjs` | **OK**, `headerActionsMatch: true`, `messageSurfaceMatchesContact: true` |
| Смок умеет падать | тот же смок на `src/houseplan-card.ts` + оба `*.styles.ts` из `afe5e989` (временный `git checkout`, независимо от r1) | `headerActionsMatch: false`, `messageSurfaceMatchesContact: false` — воспроизведено лично, дерево возвращено (`git status --porcelain` пуст, `cmp` бандла зелёный) |
| Точечные смежные смоки | `node demo/smoke_general_settings.mjs`, `node demo/smoke_help_affordance.mjs` | оба **OK** |
| CI-прогон пересъёмки | `gh run view 33592550716` | `conclusion: success`, workflow «Скриншоты документации», `headSha: c6fac8bf` — ссылка в коммите `b0122803` подтверждена, не выдумана |
| Провенанс пересъёмки | `git diff origin/dev...HEAD -- docs/images/screenshots.json` | 10 сценариев, у всех 10 обновился `sourceSha256` (глобальный отпечаток), у 8 обновился `imageSha256` — ровно совпадает с текстом коммита «Eight changed frames were reviewed; two byte-identical frames remain as environment witnesses» |
### Выбор браузерных смоков (`scripts/smoke-select.mjs`)
Вывод идентичен r1 (продуктовый диф не менялся):
```
Изменено файлов src/**: 3 · символов проекта на изменённых строках: 2
Матрица: 213 смоков · порог «широкого» символа: больше 42 смоков
Прямое совпадение (9): smoke_backup_transfer, smoke_bg_color,
smoke_color_picker_consumers, smoke_dialog_zombie, smoke_esc_dialogs,
smoke_general_settings, smoke_ha_controls, smoke_help_affordance, smoke_sun
← _openSettingsDialog (совпадение по имени JS-символа на изменённой строке,
сама функция не менялась — менялся только список CSS-классов)
```
Решение по 9 найденным — то же, что в r1, и оно не устарело: причина
совпадения (общая строка вызова `_openSettingsDialog`/`_openSupportDialog`, не
сама функция) не изменилась, диф не изменился. Прогнал повторно два самых
предметно близких — `smoke_general_settings.mjs`, `smoke_help_affordance.mjs` —
оба **OK**. Остальные 7 не прогонял по той же причине, что и в r1: чисто
визуальная правка класса, не затрагивающая обработчик клика или разметку
диалогов. Полный прогон всех 213 смоков — предрелизный гейт (§8), не гейт
ревью.
### Что не проверял и почему
- **`npm run golden:verify`** — не прогонял. В `demo/golden/` нет сценариев,
ссылающихся на `.support-button`, `.header-action` или `.supportmessage`
(проверено `grep -rl` по `demo/golden/`) — совпадает с тем, что AC2 сам
относит golden-проверку к «перед бетой», не к код-ревью.
- **`node scripts/model-invariants.mjs`** — не прогонял. Диф не касается
геометрии, рёбер, `layout`, `marker.space`, `open_spans`.
- **`python -m pytest tests_backend`** — не прогонял. `custom_components/**/*.py`
не тронут.
- **Performance-профили** — не названы в AC, не прогонял.
- Остальные 7 из 9 «прямое совпадение» смоков — решение см. выше, не изменилось
с r1.
- Полный прогон 213 смоков — предрелизный гейт, здесь не нужен: диф не «задевает
всё».
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| **High** — `node scripts/check-docs.mjs` красный («screenshot source fingerprint is stale»): диф трогает `src/**`, отпечаток документации считается по всему `src/**` безусловно (§8) | Джоба `Docs screenshots` (`workflow_dispatch`) прогнана, артефакт принят `npm run docs:accept -- --reviewed --from=<артефакт>` с явным `--expect-change` на 8 сценариях, эталоны закоммичены в `b0122803` | `node scripts/check-docs.mjs` → «Documentation checks passed (7 files, 10 external links)» — перепроверено лично на SHA `b0122803`, не только со слов автора; CI-прогон `33592550716` подтверждён `gh run view` как реально существующий `success` |
Других находок в r1 не было — таблица из одной строки полна.
## Унаследовано из r1
Ниже — то, что подтверждено уже в r1 (`docs/reviews/CODE-REVIEW-415-r1.md`, SHA
`2fc8c1203782026bb45647ad54ef11857f7eb2c3`) и **дополнительно перепроверено
лично в этом раунде** тем же способом (документ ревью недостижим по SHA, но
процитированные в нём строки кода сверены построчно с текущим диффом — они не
изменились):
- **AC1.** `.support-button` не несёт стилей размера — правило переименовано в
`.header-action` (`src/styles/plan.styles.ts:188`) и навешено на обе кнопки
шапки (`src/houseplan-card.ts:11496,11499`); `support-button` остался только
как селектор-хук, ноль правил стиля на нём (перепроверено `grep -rn
"support-button" src/` — одно вхождение, в шаблоне). Автотест
`headerActionsMatch` умеет падать — перепроверено лично на исходниках
`afe5e989`, независимо от r1.
- **AC2.** `.supportmessage` получил `background: var(--hp-bg)`
(`src/styles/dialogs.styles.ts`), тот же токен, что `.namein`
(`src/styles/dialogs.styles.ts:355`, тоже `var(--hp-bg)`) — перепроверено
`grep -n ".namein" -A5`. `.supportraw` не тронут — не входит в диф. Автотест
`messageSurfaceMatchesContact` умеет падать — перепроверено тем же способом.
- **Трейлеры и changelog коммита `d1691f4b`** — `Issue: #415`,
`User-Visible: yes`, оба changelog правлены в том же коммите, терминология
совпадает с `docs/USER-GUIDE.ru.md:58,1920` («Общие настройки», «Помощь и
обратная связь») — перепроверено повторным `grep` по гайду.
- **Тестовый контракт `test/support-feedback.test.mjs`** — статический
дубль-контроль поверх браузерного смока (ровно два вхождения
`header-action` в шапке, правило `.header-action` содержит `min-width:44px`,
`.supportmessage` содержит `background: var(--hp-bg)`) — диф файла
перечитан целиком в этом раунде, не только унаследован.
Не перепроверялось заново, унаследовано без замечаний (диф этих утверждений не
касается): продуктовое рассуждение r1 о том, что задача не расширяет скоуп
(«ни новых сценариев, ни миграции, ни i18n, ни touch/perf») — дельта раунда
(рёбейз + пересъёмка скриншотов + документ) этого вывода не задевает.
## Находки
Новых находок нет. Находка r1 (High, check-docs) закрыта и перепроверена лично
(см. выше), новых High/Medium/Low в дельте этого раунда (рёбейз на `206732e9`,
пересъёмка 8 кадров, документ ревью r1) не обнаружено — рёбейз принёс только
чужой, не пересекающийся с этим диффом коммит #413 (CI-инструмент, не
продуктовый код), конфликтов слияния нет (`git status --porcelain` пуст на
каждом шаге проверки).
## Что проверено и корректно
- **AC1 и AC2** — доказаны автотестом, который умеет падать; перепроверено
лично в этом раунде независимо от r1 (см. «Унаследовано из r1»).
- **check-docs** — зелёный на точном SHA `b0122803`, ссылка на реальный
успешный CI-прогон пересъёмки подтверждена через `gh run view`, а не принята
на слово.
- **Провенанс пересъёмки** — 8 изменившихся кадров и 2 кадра-свидетеля в
`screenshots.json` совпадают с формулировкой коммита `b0122803` число в
число.
- **Рёбейз не сломал ничего**: `npm test` (1729/1730), `tsc`, `build`+`cmp`
трёх копий бандла, `process-gate.mjs`, целевой и точечные смоки — все
зелёные на актуальном `origin/dev` (`206732e9`), а не на устаревшей базе.
- **Трейлеры класса C.** `b0122803` трогает только `docs/images/**`
(класс C по таблице §1, не класс D — `dist/**`,
`custom_components/houseplan/frontend/**`, `demo/golden/baselines/**`), и
`scripts/validate-commit-provenance.mjs` требует `Release`/`Baseline-Reviewed`
только для `demo/golden/baselines/*.png|.json` — эта пара трейлеров сюда не
относится; вместо неё коммит несёт содержательную строку «Canonical Linux
capture: <ссылка>» в стиле вывода `scripts/docs-accept.mjs`. Формальных
требований гейта это не нарушает.
- **Никакого расширения скоупа.** Диф раунда — рёбейз, документ ревью,
пересъёмка скриншотов; продуктовый код не менялся ни строкой.
## Вывод
Единственная находка r1 (High, `check-docs`) закрыта и лично перепроверена, а
не принята со слов автора: гейт зелёный, CI-прогон пересъёмки существует и
успешен, провенанс `screenshots.json` совпадает с текстом коммита. Рёбейз на
ушедший вперёд `dev` потребовал полного разбора (§7.2), а не узкой дельты —
проведён полностью: оба AC передоказаны автотестом, который умеет падать,
независимо от r1. Новых находок нет. Вердикт — зелёный.
+129
View File
@@ -23,6 +23,7 @@
* который пуш добавит в целевую ветку. Пустой список — тоже отказ: публиковать
* нечего, значит что-то пошло не так раньше.
*/
import { spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
export const REVIEW_DOC_ALLOWLIST = ['docs/reviews/'];
@@ -59,10 +60,138 @@ export function reviewDocPushRefusal(paths, allowlist = REVIEW_DOC_ALLOWLIST) {
+ ' превратилось в «затереть dev целиком» (#365).';
}
/**
* Сколько первых строк документа считаются шапкой. Материал раунда объявляется
* там — измерено по корпусу: из 555 опубликованных ревью 409 называют SHA в
* первых пятнадцати строках. Дальше начинается проза, и в ней SHA упоминаются
* исторически («коммит bb2919f откатил dev»), проверять их нечего.
*/
export const REVIEW_HEADER_LINES = 20;
/** Строки шапки, объявляющие материал раунда. */
const MATERIAL_MARKER = /(Материал|Коммит дельты|SHA|HEAD\s*=|коммит)/;
/**
* Кандидаты в SHA. Границы подобраны по корпусу, а не по вкусу:
*
* - 7–40 знаков: короче не бывает сокращений git, длиннее не бывает sha1.
* Отсекает заодно sha256 (64) — их в отчётах много, и они не коммиты;
* - хотя бы одна буква a–f: иначе в кандидаты попадают номера прогонов и даты
* вида `20260901`;
* - не после `#`: цвет `#607d8bff` — восемь шестнадцатеричных знаков;
* - не внутри более длинной шестнадцатеричной последовательности и не через
* дефис: `sha256-…` и обрезанные хвосты хешей кандидатами не считаются.
*/
const SHA_CANDIDATE = /(?<![0-9a-f#-])[0-9a-f]{7,40}(?![0-9a-f-])/g;
/**
* SHA, объявленные материалом раунда: `[{ line, sha }]`.
*
* Зачем отдельная функция и почему только шапка. PROCESS.md §2.10 требует
* называть SHA предыдущего раунда затем, чтобы дельта следующего объявлялась
* воспроизводимой командой `git diff <sha>..HEAD`. Проверять имеет смысл ровно
* то, что этой командой пользуются: объявление материала. Исторические
* упоминания в прозе — не обещание воспроизводимости.
*/
export function citedMaterialShas(text, headerLines = REVIEW_HEADER_LINES) {
const found = [];
String(text ?? '').split('\n').slice(0, headerLines).forEach((line, index) => {
if (!MATERIAL_MARKER.test(line)) return;
for (const sha of line.match(SHA_CANDIDATE) || []) {
if (!/[a-f]/.test(sha)) continue;
found.push({ line: index + 1, sha });
}
});
return found;
}
/**
* Вердикт: `null` — все объявленные SHA существуют коммитами.
*
* Зачем этот рубеж (#413). `SPEC-REVIEW-403-r2.md` объявил материал раунда на
* `HEAD = 83005c3c`, и тот же SHA независимо назвал автор ТЗ в комментарии
* issue. Коммита с таким именем в репозитории нет и не было: клон не мелкий,
* `git rev-list --all` его не знает. Скорее всего значение снято до `amend`
* или `rebase` при публикации — то есть проверка `git rev-parse HEAD` перед
* выводом отчёта, которую требует §7.2, не выполнялась ни у автора, ни у
* ревьюера.
*
* Цена уже заплачена на следующем раунде: пункт «найти SHA, на котором получен
* предыдущий вердикт» выполнить буквально не удалось, реальный коммит
* реконструировали по содержимому диффа.
*
* Чего этот рубеж НЕ умеет, и это важно знать. Он судит момент публикации.
* Ветка задачи после ревью нередко перебазируется или сквошится, и SHA умирает
* уже потом — по корпусу таких объявлений 98 из 804. Здесь ловится другой
* класс: SHA, мёртвый уже в момент, когда его объявляют воспроизводимым.
*
* Достижимость проверяется от ссылок ПУБЛИКАЦИИ (`refs/remotes/origin/*` и
* теги), а не от локальных. Разница не теоретическая: осиротевший `83005c3c`
* до сих пор лежит объектом в клоне Codex и достижим там из локальной
* `refs/heads/issue/403-area-relocation-safety`, не обновлённой после ребейза.
* Читателю отчёта от этого нет никакой пользы — он может достать только то,
* что есть на origin. Локальная проверка дала бы «всё в порядке» ровно на той
* машине, где ошибку и совершили.
*
* @param resolveReachable функция `(shas) => Map<sha, ref|null>`
*/
export function danglingMaterialRefusal(text, resolveReachable, headerLines = REVIEW_HEADER_LINES) {
const cited = citedMaterialShas(text, headerLines);
if (!cited.length) return null;
const refs = resolveReachable([...new Set(cited.map((item) => item.sha))]);
const bad = cited.filter((item) => !refs.get(item.sha));
if (!bad.length) return null;
const lines = bad
.map((item) => ` строка ${item.line}: ${item.sha} → не достижим ни из одной ссылки origin`)
.join('\n');
return 'ревью-документ объявляет материал раунда на SHA, которого нет на'
+ ` origin:\n${lines}\n`
+ 'Команда `git diff <sha>..HEAD` из PROCESS.md §2.10 на таком отчёте не'
+ ' работает, а следующий раунд восстанавливает коммит по содержимому'
+ ' диффа руками (#413). Сверьте SHA командой `git rev-parse HEAD`'
+ ' непосредственно перед выводом отчёта — §7.2 требует именно этого,'
+ ' а не значения, записанного до amend или rebase.';
}
const invokedDirectly = process.argv[1]
&& import.meta.url === new URL(`file://${process.argv[1]}`).href;
if (invokedDirectly) {
const argv = process.argv.slice(2);
// Режим проверки объявленного материала (#413): на входе сам документ.
const docArg = argv.find((item) => item.startsWith('--doc='));
if (docArg) {
const path = docArg.slice('--doc='.length);
let text;
try {
text = readFileSync(path === '-' ? 0 : path, 'utf8');
} catch (error) {
console.error(`::error::ревью-документ не прочитан: ${path} (${error.code || error.message})`);
process.exit(1);
}
const resolveReachable = (shas) => {
const map = new Map(shas.map((sha) => [sha, null]));
for (const sha of shas) {
const probe = spawnSync('git', [
'for-each-ref', '--contains', sha, '--count=1',
'--format=%(refname)', 'refs/remotes/origin', 'refs/tags',
], { encoding: 'utf8' });
const ref = (probe.stdout || '').trim().split('\n')[0];
if (probe.status === 0 && ref) map.set(sha, ref);
}
return map;
};
const refusal = danglingMaterialRefusal(text, resolveReachable);
if (refusal) {
console.error(`::error::${refusal.split('\n')[0]}`);
console.error(refusal);
process.exit(1);
}
const cited = citedMaterialShas(text);
console.log(cited.length
? `материал раунда объявлен и достижим с origin: ${cited.map((item) => item.sha).join(', ')}`
: 'материал раунда в шапке не объявлен — проверять нечего');
process.exit(0);
}
const allowArg = argv.find((item) => item.startsWith('--allow='));
const allowlist = allowArg
? allowArg.slice('--allow='.length).split(',').map((item) => item.trim()).filter(Boolean)
+2 -2
View File
@@ -11493,10 +11493,10 @@ export class HouseplanCard extends LitElement {
<button class="btn zb" @click=${() => this._stepZoom(1)} title=${this._t('title.zoom_in')}><ha-icon icon="mdi:plus"></ha-icon></button>
</div>
${this._norm && this._canEdit
? html`<button class="btn" @click=${this._openSettingsDialog} title=${this._t('title.general_settings')}>
? html`<button class="btn header-action" @click=${this._openSettingsDialog} title=${this._t('title.general_settings')}>
<ha-icon icon="mdi:cog-outline"></ha-icon>
</button>
<button class="btn support-button" @click=${this._openSupportDialog}
<button class="btn header-action support-button" @click=${this._openSupportDialog}
title=${this._t('support.title')} aria-label=${this._t('support.title')}>
<ha-icon icon="mdi:help-circle-outline"></ha-icon>
</button>`
+4 -1
View File
@@ -1186,7 +1186,10 @@ export const dialogsStyles = css`
background: color-mix(in srgb, var(--card-background-color, var(--hp-bg)) 92%, var(--hp-txt));
font: inherit;
}
.supportmessage { min-height: 120px; }
.supportmessage {
min-height: 120px;
background: var(--hp-bg);
}
.supportraw {
min-height: 220px;
margin-top: var(--sp-2);
+1 -1
View File
@@ -185,7 +185,7 @@ export const planStyles = css`
justify-content: center;
padding: var(--sp-3);
}
.support-button {
.header-action {
min-width: 44px;
min-height: 44px;
justify-content: center;
+51 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import {
REVIEW_DOC_ALLOWLIST, pathsOutsideAllowlist, reviewDocPushRefusal,
REVIEW_DOC_ALLOWLIST, citedMaterialShas, danglingMaterialRefusal, pathsOutsideAllowlist, reviewDocPushRefusal,
} from '../scripts/review-doc-guard.mjs';
// #365. 28.08 шаг публикации ревью-дока запушил в dev коммит bb2919f с тридцатью
@@ -87,3 +87,53 @@ test('шаг публикации в конвейере проверяет и и
// Индексируется один путь, а не каталог.
assert.match(step, /git add -- "\$doc"/);
});
// --- материал раунда обязан быть достижим (#413) ----------------------------
test('SHA из шапки извлекаются, а из прозы — нет (#413)', () => {
const doc = [
'# SPEC-REVIEW-403-r2',
'',
'## Скоуп',
'',
'- Материал: спец-файл на `HEAD = 83005c3c` (ветка `issue/403-x`,',
' коммит «docs: revise area relocation safety spec»)',
'- Ревизия: 2',
].join('\n') + '\n'.repeat(30) + 'Так коммит bb2919f7 откатил dev на три часа.\n';
const cited = citedMaterialShas(doc);
assert.deepEqual(cited.map((item) => item.sha), ['83005c3c']);
assert.equal(cited[0].line, 5);
});
test('не-SHA в шапку не попадают: цвета, sha256, номера (#413)', () => {
const doc = [
'- Материал: коммит `cbf5cc1b`, цвет #607d8bff, прогон 20260901,',
' imageSha256 `9119ab87502038f787529f621c39e1e0d01f3bc3b0289051c3791a1886e97a6b`,',
' ссылка sha256-abc1234def',
].join('\n');
assert.deepEqual(citedMaterialShas(doc).map((item) => item.sha), ['cbf5cc1b']);
});
test('недостижимый SHA останавливает раунд и объясняет, почему (#413)', () => {
const doc = '- Материал: спец-файл на `HEAD = 83005c3c`\n';
const refusal = danglingMaterialRefusal(doc, () => new Map([['83005c3c', null]]));
assert.match(refusal, /83005c3c/);
assert.match(refusal, /не достижим ни из одной ссылки origin/);
// Отказ обязан называть и команду из канона, и способ не повторить:
// на #403 ревьюер снял HEAD до ребейза и не сверился перед выводом.
assert.match(refusal, /git diff/);
assert.match(refusal, /git rev-parse HEAD/);
});
test('достижимый SHA раунд не задерживает (#413)', () => {
const doc = '- Материал: коммит `cbf5cc1b`\n';
const resolve = () => new Map([['cbf5cc1b', 'refs/remotes/origin/dev']]);
assert.equal(danglingMaterialRefusal(doc, resolve), null);
});
test('шапка без объявления материала не судится (#413)', () => {
// Часть документов материал не объявляет вовсе — по корпусу таких 146 из 555.
// Требовать объявление — отдельное решение о каноне, а не дело гейта.
assert.equal(danglingMaterialRefusal('# CODE-REVIEW-1-r1\n\nтекст\n', () => new Map()), null);
assert.deepEqual(citedMaterialShas('# CODE-REVIEW-1-r1\n\nтекст\n'), []);
});
+4 -1
View File
@@ -63,13 +63,16 @@ test('Help is lazy, ordered after settings, and owns the single About/Guide surf
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const runtime = readFileSync(new URL('../src/houseplan-editor-runtime.ts', import.meta.url), 'utf8');
const styles = readFileSync(new URL('../src/styles/plan.styles.ts', import.meta.url), 'utf8');
const dialogStyles = readFileSync(new URL('../src/styles/dialogs.styles.ts', import.meta.url), 'utf8');
const header = card.slice(card.indexOf('<div class="zoomctl">'), card.indexOf('</div>\n ${this._canEdit'));
assert.ok(header.indexOf('_openSettingsDialog') < header.indexOf('_openSupportDialog'));
assert.match(card, /if \(!this\._editorRuntime\)[\s\S]*?_ensureEditorRuntime\(\)[\s\S]*?_openSupportDialog/);
assert.equal((runtime.match(/_t\('gs\.about_version'/g) || []).length, 1);
assert.match(runtime, /docs\/USER-GUIDE\.ru\.md/);
assert.match(runtime, /docs\/USER-GUIDE\.md/);
assert.match(styles, /\.support-button\s*\{[\s\S]*?min-width:\s*44px;[\s\S]*?min-height:\s*44px;/);
assert.equal((header.match(/header-action/g) || []).length, 2);
assert.match(styles, /\.header-action\s*\{[\s\S]*?min-width:\s*44px;[\s\S]*?min-height:\s*44px;/);
assert.match(dialogStyles, /\.supportmessage\s*\{[\s\S]*?background:\s*var\(--hp-bg\);/);
});
test('the consent copy names exact geometry, project relay, retention and network address', () => {