mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
committed by
claude[bot]
parent
8273af59ca
commit
372d838ec2
@@ -1,125 +1,125 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "bd405d5ce3531f7484e3d780d84bf9a6120e8bf7226acbfbcd15eb29ef573e40",
|
||||
"fingerprint": "53446fa7a809213ad1e999195c01161c132792fa83995d46ad61a91ada09a6bf",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-cmbllkdu.js",
|
||||
"houseplan-assets/houseplan-card-D7UbumXf.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 287575,
|
||||
"initialViewGzipBytes": 287320,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-D66Ner2L.js",
|
||||
"houseplan-assets/de-C9HvYpJv.js",
|
||||
"houseplan-assets/editor-CyKPZvNS.js",
|
||||
"houseplan-assets/fr-BjAKtQwB.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js"
|
||||
"houseplan-assets/backdrop-pick-BCqduHv0.js",
|
||||
"houseplan-assets/de-AtSCoayi.js",
|
||||
"houseplan-assets/editor-IBy3vVVQ.js",
|
||||
"houseplan-assets/fr-ByO3R-ts.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BNEf5mnq.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-IojorVEt.js"
|
||||
],
|
||||
"lazyGzipBytes": 199808,
|
||||
"lazyGzipBytes": 199360,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-D66Ner2L.js",
|
||||
"houseplan-assets/editor-CyKPZvNS.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js"
|
||||
"houseplan-assets/backdrop-pick-BCqduHv0.js",
|
||||
"houseplan-assets/editor-IBy3vVVQ.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BNEf5mnq.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 144676,
|
||||
"lazyEditorGzipBytes": 144683,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-D66Ner2L.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js"
|
||||
"houseplan-assets/backdrop-pick-BCqduHv0.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-IojorVEt.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 14024,
|
||||
"lazyOnboardingGzipBytes": 14028,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-C9HvYpJv.js",
|
||||
"houseplan-assets/fr-BjAKtQwB.js"
|
||||
"houseplan-assets/de-AtSCoayi.js",
|
||||
"houseplan-assets/fr-ByO3R-ts.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 48176,
|
||||
"lazyLocaleGzipBytes": 47720,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-D66Ner2L.js",
|
||||
"sha256": "6f8e893fe459b025f2f89ef3cf48afd07be82699a34489d446e6d2bbea1e0f44",
|
||||
"path": "houseplan-assets/backdrop-pick-BCqduHv0.js",
|
||||
"sha256": "683fb618bc084cf2c8b373ad56d1d113b73c9a78aa9bb513005c686184a1bb09",
|
||||
"rawBytes": 20636,
|
||||
"gzipBytes": 7068,
|
||||
"gzipBytes": 7071,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-cmbllkdu.js"
|
||||
"houseplan-assets/houseplan-card-D7UbumXf.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-C9HvYpJv.js",
|
||||
"sha256": "c48bad47855be263551852c9e4350e6148c3bc0206ddd63136982d4414110c48",
|
||||
"rawBytes": 81096,
|
||||
"gzipBytes": 24342,
|
||||
"path": "houseplan-assets/de-AtSCoayi.js",
|
||||
"sha256": "b7daaaec1d63a75a919fa577f284c8a7d5a3cc9430398a462e654d46075f21a7",
|
||||
"rawBytes": 79660,
|
||||
"gzipBytes": 24110,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-CyKPZvNS.js",
|
||||
"sha256": "f285f0141083e5ea3bb7069126474c1e9cfb7e9073f58cb8fed8963cc131848a",
|
||||
"path": "houseplan-assets/editor-IBy3vVVQ.js",
|
||||
"sha256": "2a7fadc3f67dc1c69b6a3168b47393d18eb1aafd81ba6d85d41522cd12df032e",
|
||||
"rawBytes": 3826,
|
||||
"gzipBytes": 1578,
|
||||
"gzipBytes": 1581,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-cmbllkdu.js"
|
||||
"houseplan-assets/houseplan-card-D7UbumXf.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-BjAKtQwB.js",
|
||||
"sha256": "b443ec61052a852b1f7d1982b402ee3cf85f9600382dd317b55caa0543da39e2",
|
||||
"rawBytes": 83283,
|
||||
"gzipBytes": 23834,
|
||||
"path": "houseplan-assets/fr-ByO3R-ts.js",
|
||||
"sha256": "ca6c58cb0bb9537d038cf00626ec9c8d1e0759edda76923ac6375e6a7b559ea6",
|
||||
"rawBytes": 81807,
|
||||
"gzipBytes": 23610,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-cmbllkdu.js",
|
||||
"sha256": "1ac69d06e6d8717d0daa9634bb0ce911e4fded918904b06c34ac8290b3842d75",
|
||||
"rawBytes": 1019911,
|
||||
"gzipBytes": 286781,
|
||||
"path": "houseplan-assets/houseplan-card-D7UbumXf.js",
|
||||
"sha256": "2a074b9627f0f8a81a23f97686e194030059efd2d64ebcaaa14fc887f9433561",
|
||||
"rawBytes": 1017381,
|
||||
"gzipBytes": 286525,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-C9HvYpJv.js",
|
||||
"houseplan-assets/editor-CyKPZvNS.js",
|
||||
"houseplan-assets/fr-BjAKtQwB.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js"
|
||||
"houseplan-assets/de-AtSCoayi.js",
|
||||
"houseplan-assets/editor-IBy3vVVQ.js",
|
||||
"houseplan-assets/fr-ByO3R-ts.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BNEf5mnq.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-IojorVEt.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js",
|
||||
"sha256": "01529edd319c84ce373d6086db952d6bdcad27d303c96c0dadb717bf16f9306e",
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-BNEf5mnq.js",
|
||||
"sha256": "2fcdbacd2b2d043790efce384ae8c43438208787388cd1bbd21b5cf667d04318",
|
||||
"rawBytes": 527184,
|
||||
"gzipBytes": 136030,
|
||||
"gzipBytes": 136031,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-D66Ner2L.js",
|
||||
"houseplan-assets/houseplan-card-cmbllkdu.js"
|
||||
"houseplan-assets/backdrop-pick-BCqduHv0.js",
|
||||
"houseplan-assets/houseplan-card-D7UbumXf.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js",
|
||||
"sha256": "67bbf22592e7fcb3060012f104c0ff3ebe2b6ea29fa771e650d2fae8b4085ccd",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-IojorVEt.js",
|
||||
"sha256": "e85b4995d8609b561e35ad93f3f7dd2bdd975428c25348916c82dcdb32dd1a8a",
|
||||
"rawBytes": 28088,
|
||||
"gzipBytes": 6956,
|
||||
"gzipBytes": 6957,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-D66Ner2L.js",
|
||||
"houseplan-assets/houseplan-card-cmbllkdu.js"
|
||||
"houseplan-assets/backdrop-pick-BCqduHv0.js",
|
||||
"houseplan-assets/houseplan-card-D7UbumXf.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "46e873cfe52720c02c77556c3e39ec7f96b7171f7be332c59116216b49009691",
|
||||
"sha256": "7d9bdcd89732ba3d226b0306f48d44c72ae8efa1778947e8b25399659dfbcb0f",
|
||||
"rawBytes": 1183,
|
||||
"gzipBytes": 794,
|
||||
"gzipBytes": 795,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-cmbllkdu.js"
|
||||
"houseplan-assets/houseplan-card-D7UbumXf.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
+1
-1
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
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="bd405d5ce3531f7484e3d780d84bf9a6120e8bf7226acbfbcd15eb29ef573e40";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-cmbllkdu.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__="53446fa7a809213ad1e999195c01161c132792fa83995d46ad61a91ada09a6bf";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-D7UbumXf.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
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+13
-9
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+28
-28
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="bd405d5ce3531f7484e3d780d84bf9a6120e8bf7226acbfbcd15eb29ef573e40";try{await import("./houseplan-assets/houseplan-card-cmbllkdu.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__="53446fa7a809213ad1e999195c01161c132792fa83995d46ad61a91ada09a6bf";try{await import("./houseplan-assets/houseplan-card-D7UbumXf.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)}
|
||||
|
||||
@@ -320,6 +320,53 @@ const res = await page.evaluate(async () => {
|
||||
const staticEl = staticCard.renderRoot.querySelector('.dev[data-id="d_leak"]');
|
||||
const staticLeft = Number.parseFloat(staticEl?.style.left || '0');
|
||||
|
||||
// Lifecycle metadata is bounded by the authoritative registry, not by
|
||||
// explicit editor cleanup alone. A disappeared device with no saved marker
|
||||
// must lose its snapshot on the next full pass.
|
||||
const orphanId = 'device-that-disappeared';
|
||||
c._serverCfg = {
|
||||
...c._serverCfg,
|
||||
settings: {
|
||||
...c._serverCfg.settings,
|
||||
marker_area_snapshot: {
|
||||
...c._serverCfg.settings.marker_area_snapshot,
|
||||
[orphanId]: { binding: `device:${orphanId}`, area: 'living_room' },
|
||||
},
|
||||
},
|
||||
};
|
||||
c._regSignature = '';
|
||||
c._maybeRebuildDevices();
|
||||
await wait(450);
|
||||
const authoritativeOrphanRemoved = !c._serverCfg.settings.marker_area_snapshot?.[orphanId];
|
||||
|
||||
// A separate connection whose full registry calls fail models startup or a
|
||||
// limited account. The exact same orphan evidence is not authoritative and
|
||||
// therefore must not be destructive.
|
||||
const limitedConfig = structuredClone(c._serverCfg);
|
||||
limitedConfig.settings.marker_area_snapshot[orphanId] = {
|
||||
binding: `device:${orphanId}`, area: 'living_room',
|
||||
};
|
||||
const limitedHass = window.__mkHass();
|
||||
const limitedCallWS = limitedHass.callWS;
|
||||
limitedHass.connection = {};
|
||||
limitedHass.callWS = async (message) => {
|
||||
if (message.type === 'config/device_registry/list'
|
||||
|| message.type === 'config/entity_registry/list') throw new Error('synthetic limited registry');
|
||||
if (message.type === 'houseplan/config/get') {
|
||||
return { config: limitedConfig, rev: 30, can_write: true };
|
||||
}
|
||||
return limitedCallWS(message);
|
||||
};
|
||||
const limitedCard = document.createElement('houseplan-card');
|
||||
limitedCard.setConfig({ type: 'custom:houseplan-card', title: 'Limited registry' });
|
||||
limitedCard.hass = limitedHass;
|
||||
document.body.append(limitedCard);
|
||||
await wait(500);
|
||||
await paint(limitedCard);
|
||||
const nonAuthoritativeOrphanPreserved = !!limitedCard._serverCfg?.settings
|
||||
?.marker_area_snapshot?.[orphanId];
|
||||
limitedCard.remove();
|
||||
|
||||
return {
|
||||
movedToRegistryArea: moved?.area === 'kitchen' && moved?.space === 'f1',
|
||||
staleLayoutDeleted: !c._layout.d_light1,
|
||||
@@ -362,6 +409,8 @@ const res = await page.evaluate(async () => {
|
||||
message.type === 'houseplan/config/set'
|
||||
|| ['houseplan/layout/set', 'houseplan/layout/update', 'houseplan/layout/delete']
|
||||
.includes(message.type)),
|
||||
authoritativeOrphanRemoved,
|
||||
nonAuthoritativeOrphanPreserved,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,56 @@ const out = await page.evaluate(async () => {
|
||||
await card.updateComplete;
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
};
|
||||
const settleElement = async (element) => {
|
||||
await element.updateComplete;
|
||||
const shell = element.querySelector('hp-dialog');
|
||||
if (shell) await shell.updateComplete;
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
};
|
||||
const deepActiveElement = () => {
|
||||
let active = document.activeElement;
|
||||
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
||||
return active;
|
||||
};
|
||||
const directConfirm = async (kind, action, key) => {
|
||||
const element = document.createElement('hp-confirm');
|
||||
element.token = Math.floor(Math.random() * 1_000_000) + 1;
|
||||
element.request = {
|
||||
key,
|
||||
kind,
|
||||
title: kind === 'destructive' ? 'Delete?' : 'Unlock?',
|
||||
message: kind === 'destructive'
|
||||
? 'The plan and all its rooms will be deleted.'
|
||||
: 'House Plan will send an unlock command.',
|
||||
confirmLabel: kind === 'destructive' ? 'Delete' : 'Unlock',
|
||||
cancelLabel: 'Cancel',
|
||||
};
|
||||
const decision = new Promise((resolve) => element.addEventListener(
|
||||
'hp-confirm-decision', (event) => resolve(event.detail.accepted), { once: true },
|
||||
));
|
||||
document.body.append(element);
|
||||
await settleElement(element);
|
||||
const shell = element.querySelector('hp-dialog');
|
||||
const native = shell?.shadowRoot?.querySelector('dialog');
|
||||
const body = element.querySelector('.danger-confirm-body');
|
||||
const cancel = element.querySelector('button[autofocus]');
|
||||
const semantics = native?.getAttribute('role') === 'alertdialog'
|
||||
&& native.getAttribute('aria-describedby') === body?.id
|
||||
&& !!body?.textContent?.trim();
|
||||
const focused = deepActiveElement() === cancel;
|
||||
const stayedNative = !!native && !shell?.shadowRoot?.querySelector('ha-dialog');
|
||||
if (action === 'escape') {
|
||||
cancel?.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: 'Escape', bubbles: true, composed: true, cancelable: true,
|
||||
}));
|
||||
} else {
|
||||
cancel?.click();
|
||||
}
|
||||
const accepted = await decision;
|
||||
element.remove();
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
return { semantics, focused, stayedNative, cancelled: accepted === false };
|
||||
};
|
||||
const dialogs = () => root().querySelectorAll('hp-confirm').length;
|
||||
const request = (key) => ({
|
||||
key,
|
||||
@@ -44,6 +94,16 @@ const out = await page.evaluate(async () => {
|
||||
|
||||
await settle();
|
||||
|
||||
result.standaloneStartsWithoutHaDialog = !customElements.get('ha-dialog');
|
||||
const noHaDestructive = await directConfirm('destructive', 'click', 'no-ha-delete');
|
||||
result.noHaDestructiveIsDescribedAlert = noHaDestructive.semantics;
|
||||
result.noHaDestructiveFocusesCancel = noHaDestructive.focused;
|
||||
result.noHaDestructiveCancelResolvesFalse = noHaDestructive.cancelled;
|
||||
const noHaWarning = await directConfirm('warning', 'escape', 'no-ha-unlock');
|
||||
result.noHaWarningIsDescribedAlert = noHaWarning.semantics;
|
||||
result.noHaWarningFocusesCancel = noHaWarning.focused;
|
||||
result.noHaWarningEscapeResolvesFalse = noHaWarning.cancelled;
|
||||
|
||||
// Основная ветка: ровно один диалог, а не два (риск двойного рендера при
|
||||
// выносе — если бы блок остался и в ветке, и в обёртке).
|
||||
const single = card._confirmDanger(request('single'));
|
||||
@@ -133,8 +193,68 @@ const out = await page.evaluate(async () => {
|
||||
await settle();
|
||||
result.notReadyCardRefusesInsteadOfHanging = (await refused) === false;
|
||||
|
||||
// Match the public surface of HA's pinned ha-dialog closely enough to prove
|
||||
// branch selection and ARIA forwarding without depending on private shadow
|
||||
// DOM. Alert confirmations must still avoid it: the pinned component does
|
||||
// not forward its reflected `type` to the actual dialog role.
|
||||
class HaDialogStub extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' }).innerHTML = '<slot name="headerTitle"></slot>'
|
||||
+ '<slot></slot><slot name="footer"></slot>';
|
||||
}
|
||||
connectedCallback() {
|
||||
queueMicrotask(() => this.dispatchEvent(new Event('opened')));
|
||||
}
|
||||
}
|
||||
customElements.define('ha-dialog', HaDialogStub);
|
||||
|
||||
const ordinary = document.createElement('hp-dialog');
|
||||
ordinary.title = 'Ordinary device editor';
|
||||
ordinary.describedBy = 'ordinary-description';
|
||||
ordinary.innerHTML = '<p id="ordinary-description">Ordinary dialog description</p>';
|
||||
document.body.append(ordinary);
|
||||
await ordinary.updateComplete;
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
const haShell = ordinary.shadowRoot.querySelector('ha-dialog');
|
||||
result.ordinaryDialogUsesHaBranch = !!haShell
|
||||
&& !ordinary.shadowRoot.querySelector('dialog');
|
||||
result.ordinaryDialogForwardsHaAria = haShell?.ariaLabelledBy?.startsWith('hp-dialog-title-')
|
||||
&& haShell?.ariaDescribedBy === 'ordinary-description';
|
||||
ordinary.remove();
|
||||
|
||||
const haDestructive = await directConfirm('destructive', 'click', 'ha-delete');
|
||||
result.haDestructiveStaysNativeAlert = haDestructive.semantics
|
||||
&& haDestructive.stayedNative;
|
||||
result.haDestructiveFocusesCancel = haDestructive.focused;
|
||||
result.haDestructiveCancelResolvesFalse = haDestructive.cancelled;
|
||||
const haWarning = await directConfirm('warning', 'escape', 'ha-unlock');
|
||||
result.haWarningStaysNativeAlert = haWarning.semantics && haWarning.stayedNative;
|
||||
result.haWarningFocusesCancel = haWarning.focused;
|
||||
result.haWarningEscapeResolvesFalse = haWarning.cancelled;
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
await page.evaluate(async () => {
|
||||
const element = document.createElement('hp-confirm');
|
||||
element.token = 4_060_000;
|
||||
element.request = {
|
||||
key: 'accessibility-probe', kind: 'destructive', title: 'Accessibility probe',
|
||||
message: 'All rooms in this plan will be permanently deleted.',
|
||||
confirmLabel: 'Delete', cancelLabel: 'Cancel',
|
||||
};
|
||||
document.body.append(element);
|
||||
await element.updateComplete;
|
||||
await element.querySelector('hp-dialog')?.updateComplete;
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
});
|
||||
const accessibilitySnapshot = await page.getByRole('alertdialog', {
|
||||
name: 'Accessibility probe',
|
||||
}).ariaSnapshot();
|
||||
out.realAccessibilityTreeIncludesConsequence = accessibilitySnapshot
|
||||
.includes('All rooms in this plan will be permanently deleted.');
|
||||
await page.evaluate(() => document.querySelector('hp-confirm')?.remove());
|
||||
|
||||
checkAll(out);
|
||||
await finish(browser, out);
|
||||
|
||||
Vendored
+58
-58
@@ -1,125 +1,125 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "bd405d5ce3531f7484e3d780d84bf9a6120e8bf7226acbfbcd15eb29ef573e40",
|
||||
"fingerprint": "53446fa7a809213ad1e999195c01161c132792fa83995d46ad61a91ada09a6bf",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-cmbllkdu.js",
|
||||
"houseplan-assets/houseplan-card-D7UbumXf.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 287575,
|
||||
"initialViewGzipBytes": 287320,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-D66Ner2L.js",
|
||||
"houseplan-assets/de-C9HvYpJv.js",
|
||||
"houseplan-assets/editor-CyKPZvNS.js",
|
||||
"houseplan-assets/fr-BjAKtQwB.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js"
|
||||
"houseplan-assets/backdrop-pick-BCqduHv0.js",
|
||||
"houseplan-assets/de-AtSCoayi.js",
|
||||
"houseplan-assets/editor-IBy3vVVQ.js",
|
||||
"houseplan-assets/fr-ByO3R-ts.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BNEf5mnq.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-IojorVEt.js"
|
||||
],
|
||||
"lazyGzipBytes": 199808,
|
||||
"lazyGzipBytes": 199360,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-D66Ner2L.js",
|
||||
"houseplan-assets/editor-CyKPZvNS.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js"
|
||||
"houseplan-assets/backdrop-pick-BCqduHv0.js",
|
||||
"houseplan-assets/editor-IBy3vVVQ.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BNEf5mnq.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 144676,
|
||||
"lazyEditorGzipBytes": 144683,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-D66Ner2L.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js"
|
||||
"houseplan-assets/backdrop-pick-BCqduHv0.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-IojorVEt.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 14024,
|
||||
"lazyOnboardingGzipBytes": 14028,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-C9HvYpJv.js",
|
||||
"houseplan-assets/fr-BjAKtQwB.js"
|
||||
"houseplan-assets/de-AtSCoayi.js",
|
||||
"houseplan-assets/fr-ByO3R-ts.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 48176,
|
||||
"lazyLocaleGzipBytes": 47720,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-D66Ner2L.js",
|
||||
"sha256": "6f8e893fe459b025f2f89ef3cf48afd07be82699a34489d446e6d2bbea1e0f44",
|
||||
"path": "houseplan-assets/backdrop-pick-BCqduHv0.js",
|
||||
"sha256": "683fb618bc084cf2c8b373ad56d1d113b73c9a78aa9bb513005c686184a1bb09",
|
||||
"rawBytes": 20636,
|
||||
"gzipBytes": 7068,
|
||||
"gzipBytes": 7071,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-cmbllkdu.js"
|
||||
"houseplan-assets/houseplan-card-D7UbumXf.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-C9HvYpJv.js",
|
||||
"sha256": "c48bad47855be263551852c9e4350e6148c3bc0206ddd63136982d4414110c48",
|
||||
"rawBytes": 81096,
|
||||
"gzipBytes": 24342,
|
||||
"path": "houseplan-assets/de-AtSCoayi.js",
|
||||
"sha256": "b7daaaec1d63a75a919fa577f284c8a7d5a3cc9430398a462e654d46075f21a7",
|
||||
"rawBytes": 79660,
|
||||
"gzipBytes": 24110,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-CyKPZvNS.js",
|
||||
"sha256": "f285f0141083e5ea3bb7069126474c1e9cfb7e9073f58cb8fed8963cc131848a",
|
||||
"path": "houseplan-assets/editor-IBy3vVVQ.js",
|
||||
"sha256": "2a7fadc3f67dc1c69b6a3168b47393d18eb1aafd81ba6d85d41522cd12df032e",
|
||||
"rawBytes": 3826,
|
||||
"gzipBytes": 1578,
|
||||
"gzipBytes": 1581,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-cmbllkdu.js"
|
||||
"houseplan-assets/houseplan-card-D7UbumXf.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-BjAKtQwB.js",
|
||||
"sha256": "b443ec61052a852b1f7d1982b402ee3cf85f9600382dd317b55caa0543da39e2",
|
||||
"rawBytes": 83283,
|
||||
"gzipBytes": 23834,
|
||||
"path": "houseplan-assets/fr-ByO3R-ts.js",
|
||||
"sha256": "ca6c58cb0bb9537d038cf00626ec9c8d1e0759edda76923ac6375e6a7b559ea6",
|
||||
"rawBytes": 81807,
|
||||
"gzipBytes": 23610,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-cmbllkdu.js",
|
||||
"sha256": "1ac69d06e6d8717d0daa9634bb0ce911e4fded918904b06c34ac8290b3842d75",
|
||||
"rawBytes": 1019911,
|
||||
"gzipBytes": 286781,
|
||||
"path": "houseplan-assets/houseplan-card-D7UbumXf.js",
|
||||
"sha256": "2a074b9627f0f8a81a23f97686e194030059efd2d64ebcaaa14fc887f9433561",
|
||||
"rawBytes": 1017381,
|
||||
"gzipBytes": 286525,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-C9HvYpJv.js",
|
||||
"houseplan-assets/editor-CyKPZvNS.js",
|
||||
"houseplan-assets/fr-BjAKtQwB.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js"
|
||||
"houseplan-assets/de-AtSCoayi.js",
|
||||
"houseplan-assets/editor-IBy3vVVQ.js",
|
||||
"houseplan-assets/fr-ByO3R-ts.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-BNEf5mnq.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-IojorVEt.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-bwdFd5UE.js",
|
||||
"sha256": "01529edd319c84ce373d6086db952d6bdcad27d303c96c0dadb717bf16f9306e",
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-BNEf5mnq.js",
|
||||
"sha256": "2fcdbacd2b2d043790efce384ae8c43438208787388cd1bbd21b5cf667d04318",
|
||||
"rawBytes": 527184,
|
||||
"gzipBytes": 136030,
|
||||
"gzipBytes": 136031,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-D66Ner2L.js",
|
||||
"houseplan-assets/houseplan-card-cmbllkdu.js"
|
||||
"houseplan-assets/backdrop-pick-BCqduHv0.js",
|
||||
"houseplan-assets/houseplan-card-D7UbumXf.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-B7KlxNo_.js",
|
||||
"sha256": "67bbf22592e7fcb3060012f104c0ff3ebe2b6ea29fa771e650d2fae8b4085ccd",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-IojorVEt.js",
|
||||
"sha256": "e85b4995d8609b561e35ad93f3f7dd2bdd975428c25348916c82dcdb32dd1a8a",
|
||||
"rawBytes": 28088,
|
||||
"gzipBytes": 6956,
|
||||
"gzipBytes": 6957,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-D66Ner2L.js",
|
||||
"houseplan-assets/houseplan-card-cmbllkdu.js"
|
||||
"houseplan-assets/backdrop-pick-BCqduHv0.js",
|
||||
"houseplan-assets/houseplan-card-D7UbumXf.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "46e873cfe52720c02c77556c3e39ec7f96b7171f7be332c59116216b49009691",
|
||||
"sha256": "7d9bdcd89732ba3d226b0306f48d44c72ae8efa1778947e8b25399659dfbcb0f",
|
||||
"rawBytes": 1183,
|
||||
"gzipBytes": 794,
|
||||
"gzipBytes": 795,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-cmbllkdu.js"
|
||||
"houseplan-assets/houseplan-card-D7UbumXf.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="bd405d5ce3531f7484e3d780d84bf9a6120e8bf7226acbfbcd15eb29ef573e40";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-cmbllkdu.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__="53446fa7a809213ad1e999195c01161c132792fa83995d46ad61a91ada09a6bf";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-D7UbumXf.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
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+13
-9
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+28
-28
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="bd405d5ce3531f7484e3d780d84bf9a6120e8bf7226acbfbcd15eb29ef573e40";try{await import("./houseplan-assets/houseplan-card-cmbllkdu.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__="53446fa7a809213ad1e999195c01161c132792fa83995d46ad61a91ada09a6bf";try{await import("./houseplan-assets/houseplan-card-D7UbumXf.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)}
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Delete and unlock confirmations now use House Plan's native dialog shell and
|
||||
expose an alert dialog together with its consequence text to screen readers;
|
||||
ordinary dialogs continue to use Home Assistant's dialog chrome
|
||||
([#406](https://github.com/Matysh/houseplan-card/issues/406)).
|
||||
|
||||
- A failed config write while following a device to another Home Assistant
|
||||
Area no longer silently discards its manually placed position: House Plan
|
||||
restores the point before retrying, or marks the marker for attention if the
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- Подтверждения удаления и разблокировки теперь используют нативную оболочку
|
||||
House Plan и передаются скринридеру как срочный диалог вместе с текстом
|
||||
последствий; обычные окна по-прежнему используют оболочку Home Assistant
|
||||
([#406](https://github.com/Matysh/houseplan-card/issues/406)).
|
||||
|
||||
- Отказ записи конфига при переносе устройства в другую HA-зону больше не
|
||||
стирает молча поставленную вручную позицию: House Plan восстанавливает точку
|
||||
перед повтором, а если не удалось и это — отмечает маркер для внимания.
|
||||
|
||||
@@ -128,6 +128,42 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: '',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'i18n-dead-key-returns',
|
||||
guard: 'node --test test/i18n-dead-keys.test.mjs',
|
||||
because: 'a translation key without any literal, dynamic-family or derived consumer must '
|
||||
+ 'not silently return to all shipped dictionaries (#406)',
|
||||
patches: [{
|
||||
file: 'src/i18n/en.json',
|
||||
find: ' "confirm.unlock_title": "Unlock?",',
|
||||
replace: ' "confirm.unlock": "Unlock {name}?",\n'
|
||||
+ ' "confirm.unlock_title": "Unlock?",',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'confirm-dialog-loses-alertdialog',
|
||||
guard: 'node demo/smoke_danger_confirm_branches.mjs',
|
||||
because: 'delete and unlock confirmations must expose the real dialog as an alertdialog '
|
||||
+ 'with its consequence text in both standalone and HA environments (#406)',
|
||||
patches: [{
|
||||
file: 'src/hp-dialog.ts',
|
||||
find: " role=${this.alert ? 'alertdialog' : 'dialog'}",
|
||||
replace: ' role="dialog"',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'area-snapshot-cleanup-ignores-authority',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="non-authoritative registry preserves orphan snapshots" '
|
||||
+ 'test/device-area-relocation.test.mjs',
|
||||
because: 'temporary registry absence must not erase device Area provenance before the '
|
||||
+ 'registry is authoritative (#406)',
|
||||
patches: [{
|
||||
file: 'src/device-area-relocation.ts',
|
||||
find: ' if (!options.authoritative) return { decisions, relocateIds };',
|
||||
replace: ' if (false && !options.authoritative) return { decisions, relocateIds };',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'settings-help-party1-placement-removed',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
|
||||
@@ -65,7 +65,7 @@ export function markerAreaSnapshotOf(value: unknown): MarkerAreaSnapshot {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
||||
const result: MarkerAreaSnapshot = {};
|
||||
for (const [id, raw] of Object.entries(value as Record<string, unknown>)
|
||||
.slice(0, MARKER_AREA_SNAPSHOT_LIMIT)) {
|
||||
.slice(-MARKER_AREA_SNAPSHOT_LIMIT)) {
|
||||
if (!validText(id) || !raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
|
||||
const binding = (raw as { binding?: unknown }).binding;
|
||||
const area = (raw as { area?: unknown }).area;
|
||||
@@ -142,6 +142,22 @@ export function resolveDeviceAreaRelocations(
|
||||
if (!options.authoritative) return { decisions, relocateIds };
|
||||
|
||||
const snapshot = markerAreaSnapshotOf(options.snapshot);
|
||||
const liveIds = new Set<string>();
|
||||
const liveBindings = new Set<MarkerAreaBinding>();
|
||||
for (const device of options.devices) {
|
||||
if (validText(device.id)) liveIds.add(device.id);
|
||||
if (validText(device.marker?.id)) liveIds.add(device.marker.id);
|
||||
if ((device.bindingKind === 'device' || device.bindingKind === 'entity')
|
||||
&& validText(device.bindingRef)) {
|
||||
liveBindings.add(`${device.bindingKind}:${device.bindingRef}`);
|
||||
}
|
||||
if (validBinding(device.marker?.binding)) liveBindings.add(device.marker.binding);
|
||||
}
|
||||
for (const [id, entry] of Object.entries(snapshot)) {
|
||||
if (!liveIds.has(id) && !liveBindings.has(entry.binding)) decisions.push({
|
||||
...unresolved(id, 'registry-unverified'), removeSnapshot: true,
|
||||
});
|
||||
}
|
||||
const scale = Number.isFinite(options.coordinateScale) && Number(options.coordinateScale) > 0
|
||||
? Number(options.coordinateScale) : 1000;
|
||||
const targets = new Map<string, Array<{ space: SpaceModel; room: RoomCfg }>>();
|
||||
|
||||
+4
-1
@@ -33,14 +33,17 @@ export class HpConfirm extends LitElement {
|
||||
const request = this.request;
|
||||
if (!request) return null;
|
||||
const destructive = request.kind === 'destructive';
|
||||
const descriptionId = `hp-confirm-description-${this.token}`;
|
||||
return html`<hp-dialog class="danger-confirm-dialog"
|
||||
.hass=${this.hass}
|
||||
.title=${request.title}
|
||||
.alert=${true}
|
||||
.describedBy=${descriptionId}
|
||||
.icon=${request.icon || (destructive
|
||||
? 'mdi:alert-outline' : 'mdi:lock-open-alert-outline')}
|
||||
dismiss-on-scrim
|
||||
@hp-close=${() => this._decide(false)}>
|
||||
<div class="body danger-confirm-body" data-confirm-key=${request.key}>
|
||||
<div id=${descriptionId} class="body danger-confirm-body" data-confirm-key=${request.key}>
|
||||
${request.objectName
|
||||
? html`<strong class="danger-confirm-object">${request.objectName}</strong>`
|
||||
: null}
|
||||
|
||||
+19
-8
@@ -24,9 +24,10 @@ let dialogSequence = 0;
|
||||
/**
|
||||
* Shared modal shell for Houseplan.
|
||||
*
|
||||
* Home Assistant provides the visual surface and focus trap through ha-dialog.
|
||||
* The native dialog branch keeps the standalone demo usable without mocking HA
|
||||
* frontend internals. Both branches expose the same `hp-close` contract and
|
||||
* Home Assistant provides the visual surface and focus trap through ha-dialog
|
||||
* for ordinary dialogs. Alert confirmations deliberately use the native branch:
|
||||
* the pinned HA component exposes an alert type but does not forward its role
|
||||
* to the actual dialog. Both branches expose the same `hp-close` contract and
|
||||
* restore focus to the control that opened the dialog.
|
||||
*/
|
||||
export class HpDialog extends LitElement {
|
||||
@@ -34,6 +35,8 @@ export class HpDialog extends LitElement {
|
||||
title: { type: String },
|
||||
icon: { type: String },
|
||||
wide: { type: Boolean, reflect: true },
|
||||
alert: { type: Boolean, reflect: true },
|
||||
describedBy: { type: String, attribute: 'described-by' },
|
||||
dismissOnScrim: { type: Boolean, attribute: 'dismiss-on-scrim' },
|
||||
hass: { attribute: false },
|
||||
};
|
||||
@@ -218,6 +221,8 @@ export class HpDialog extends LitElement {
|
||||
title = '';
|
||||
icon = '';
|
||||
wide = false;
|
||||
alert = false;
|
||||
describedBy = '';
|
||||
dismissOnScrim = false;
|
||||
hass: any = null;
|
||||
|
||||
@@ -228,6 +233,10 @@ export class HpDialog extends LitElement {
|
||||
private _overlays: OverlayEntry[] = [];
|
||||
private readonly _titleId = `hp-dialog-title-${++dialogSequence}`;
|
||||
|
||||
private _usesHaDialog(): boolean {
|
||||
return this._useHaDialog && !this.alert;
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._opener = this._deepActiveElement();
|
||||
@@ -274,7 +283,7 @@ export class HpDialog extends LitElement {
|
||||
|
||||
protected firstUpdated(changed: PropertyValues): void {
|
||||
super.firstUpdated(changed);
|
||||
if (!this._useHaDialog) {
|
||||
if (!this._usesHaDialog()) {
|
||||
const dialog = this.renderRoot.querySelector('dialog');
|
||||
if (dialog && !dialog.open) dialog.showModal();
|
||||
}
|
||||
@@ -340,7 +349,7 @@ export class HpDialog extends LitElement {
|
||||
const focusable = this._focusableElements();
|
||||
const autofocus = focusable.find((el) => el.hasAttribute('autofocus'));
|
||||
const target = autofocus || focusable[0]
|
||||
|| (!this._useHaDialog ? this.renderRoot.querySelector<HTMLElement>('.close') : null)
|
||||
|| (!this._usesHaDialog() ? this.renderRoot.querySelector<HTMLElement>('.close') : null)
|
||||
|| this.renderRoot.querySelector<HTMLElement>('.surface')
|
||||
|| this.renderRoot.querySelector<HTMLElement>('ha-dialog');
|
||||
target?.focus({ preventScroll: true });
|
||||
@@ -405,7 +414,7 @@ export class HpDialog extends LitElement {
|
||||
this._requestClose();
|
||||
return;
|
||||
}
|
||||
if (event.key !== 'Tab' || this._useHaDialog) return;
|
||||
if (event.key !== 'Tab' || this._usesHaDialog()) return;
|
||||
const close = this.renderRoot.querySelector<HTMLElement>('.close');
|
||||
const focusable = close ? [close, ...this._focusableElements()] : this._focusableElements();
|
||||
if (!focusable.length) {
|
||||
@@ -440,13 +449,14 @@ export class HpDialog extends LitElement {
|
||||
<span class="title-text">${this.title}</span>
|
||||
</span>`;
|
||||
|
||||
if (this._useHaDialog) {
|
||||
if (this._usesHaDialog()) {
|
||||
return html`<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${true}
|
||||
width=${this.wide ? 'medium' : 'small'}
|
||||
.preventScrimClose=${!this.dismissOnScrim}
|
||||
.ariaLabelledBy=${this._titleId}
|
||||
.ariaDescribedBy=${this.describedBy || undefined}
|
||||
@opened=${this._focusInitial}
|
||||
@closed=${this._requestClose}
|
||||
>
|
||||
@@ -457,9 +467,10 @@ export class HpDialog extends LitElement {
|
||||
}
|
||||
|
||||
return html`<dialog
|
||||
role="dialog"
|
||||
role=${this.alert ? 'alertdialog' : 'dialog'}
|
||||
aria-modal="true"
|
||||
aria-labelledby=${this._titleId}
|
||||
aria-describedby=${this.describedBy || nothing}
|
||||
@cancel=${this._onFallbackCancel}
|
||||
@click=${this._onFallbackClick}
|
||||
>
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"history.draft_segment": "Raum-Entwurfsegment hinzufügen",
|
||||
"history.draft_merge": "Unvollständige Raumumrisse verbinden",
|
||||
"history.draft_segment_delete": "Raum-Entwurfssegment entfernen",
|
||||
"history.partition_add": "Trennwand hinzufügen",
|
||||
"history.column_add": "Säule hinzufügen",
|
||||
"history.physical_edit": "Physisches Objekt ändern",
|
||||
"history.physical_delete": "Physisches Objekt löschen",
|
||||
@@ -37,8 +36,6 @@
|
||||
"history.wall_chain_finish": "Wandkette fertigstellen",
|
||||
"history.wall_face_batch": "Räume aus Wänden erstellen",
|
||||
"toast.column_duplicate": "Eine Säule mit demselben Zentrum und äußerer Größe existiert bereits",
|
||||
"confirm.delete_draft": "Sofort den gesamten unvollständigen Raumumriss löschen?",
|
||||
"confirm.delete_draft_segment": "Dieses Entwurfssegment löschen? Der verbleibende Umriss könnte sich in zwei teilen.",
|
||||
"confirm.delete_draft_title": "Unvollständigen Umriss löschen?",
|
||||
"confirm.delete_draft_body": "Der gesamte unvollständige Raumumriss wird gelöscht. Beim Abbrechen wird nichts geändert.",
|
||||
"confirm.delete_draft_segment_title": "Umrisssegment löschen?",
|
||||
@@ -76,7 +73,6 @@
|
||||
"title.zoom_in": "Zoomen",
|
||||
"title.zoom_out": "Herauszoomen",
|
||||
"title.zoom_fit": "Alles anpassen",
|
||||
"title.markup": "Raum-Markierung: Gitter, Linien, Konturen",
|
||||
"title.configure_space": "Bereich konfigurieren",
|
||||
"title.add_space": "Bereich hinzufügen",
|
||||
"title.markup_add": "Wände: eine durchgehende Kette zeichnen; Shift sperrt auf 45°. Werkzeugwechsel oder Verlassen des Editors beendet die Kette und wandelt sie in unabhängige Wände",
|
||||
@@ -121,7 +117,6 @@
|
||||
"history.edit_opening": "Öffnung bearbeiten",
|
||||
"history.move_opening": "Öffnung verschieben",
|
||||
"history.delete_opening": "Öffnung löschen",
|
||||
"history.delete_room": "Raum löschen",
|
||||
"history.delete_room_keep_walls": "Raum löschen, Wände behalten",
|
||||
"history.delete_room_with_walls": "Raum und Wände löschen",
|
||||
"history.decor_add": "Dekorobjekt hinzufügen",
|
||||
@@ -163,7 +158,6 @@
|
||||
"confirm.delete_partition_openings_title": "Wand und Öffnungen löschen?",
|
||||
"confirm.delete_partition_openings_body": "Diese Wand enthält {count} Öffnung(en). Sie werden zusammen gelöscht.",
|
||||
"confirm.delete_partition_openings_item": "• {type}, {length}",
|
||||
"markup.delete": "Löschen",
|
||||
"markup.hint_points": "Punkte: {n} · Shift — 45° Schritte · Esc — Kette beenden · Ctrl+Z — Punkt rückgängig machen · Schließen eines Bereichs erstellt einen Raum",
|
||||
"markup.hint_start": "Klicken Sie auf einen Rasterpunkt, um eine Wandkette zu starten",
|
||||
"tip.lqi": "durchschnittliches Zigbee-Signal:",
|
||||
@@ -192,9 +186,7 @@
|
||||
"display.badge": "Symbol + Zustand",
|
||||
"display.icon_ripple": "Symbol + Zustand und Aktivität",
|
||||
"display.static_icon": "Immer statisches Symbol",
|
||||
"marker.display_hint": "Symbol + Zustand ändert die Platte für die Zustände Arbeit, geöffnet, Alarm und nicht verfügbar. Symbol + Zustand und Aktivität zeigt zusätzlich einen kurzen Impuls für Ereignisse und einen kontinuierlichen Impuls für laufende Arbeit, Bewegung oder Präsenz. Wert + Zustand ersetzt das Symbol durch einen eindeutigen HA-Wert. Ein statisches Symbol reagiert niemals auf Gerätezustände.",
|
||||
"marker.display_hint_badge": "Das Symbol und die dynamische Platte zeigen den Gerätezustand ohne den üblichen Aktivitätspuls. Rote Alarme bleiben sichtbar.",
|
||||
"marker.display_hint_icon": "Das Symbol und die dynamische Platte zeigen den Gerätezustand ohne den üblichen Aktivitätspuls. Rote Alarme bleiben sichtbar.",
|
||||
"marker.display_hint_icon_ripple": "Das Symbol, die dynamische Platte und der Puls zeigen einen kurzen Puls für Ereignisse und einen kontinuierlichen Puls für Arbeit, Bewegung oder Anwesenheit. Rote Alarme haben eine separate Priorität.",
|
||||
"marker.display_hint_value": "Ein ausgewählter oder automatisch ermittelter Home Assistant-Wert ersetzt das Symbol, während die Platte weiterhin den Zustand anzeigt. Rote Alarme bleiben sichtbar.",
|
||||
"marker.display_hint_static_icon": "Die themenabhängige Hülle und das Symbol bleiben immer gleich. Zustand, Aktivität, Nichtverfügbarkeit und Alarme verändern nicht das Erscheinungsbild.",
|
||||
@@ -293,14 +285,11 @@
|
||||
"device.light_group": "Lichtgruppe",
|
||||
"device.fallback": "Gerät",
|
||||
"device.virtual": "Virtuelles Gerät",
|
||||
"confirm.delete_room": "Raum \"{name}\" löschen?",
|
||||
"confirm.delete_room_title": "Raum \"{name}\" löschen?",
|
||||
"confirm.delete_room_body": "Festlegen, ob die ausschließlich zu diesem Raum gehörenden physischen Wände erhalten bleiben. Mit einem anderen Raum geteilte Wände bleiben immer erhalten.",
|
||||
"confirm.remove_marker": "„{name}“ aus dem Plan löschen? Das Gerät verschwindet vollständig und trägt nicht mehr zu den Plandaten bei. Es kann später erneut hinzugefügt werden.",
|
||||
"confirm.remove_marker_title": "Gerät aus dem Plan löschen?",
|
||||
"confirm.remove_marker_body": "Das Gerät verschwindet vollständig und trägt nicht mehr zu den Plandaten bei. Es kann später erneut hinzugefügt werden.",
|
||||
"confirm.erase_decor": "Das Objekt „{kind}“ löschen? Diese Aktion kann im Editor rückgängig gemacht werden.",
|
||||
"confirm.delete_space": "Bereich „{title}“ mit allen Räumen und Markierungen löschen?",
|
||||
"confirm.delete_space_title": "Bereich löschen?",
|
||||
"confirm.delete_space_body": "Der Bereich, alle seine Räume und die gesamte Planmarkierung werden gelöscht.",
|
||||
"space.delete_blocked": "Dieser Bereich wird noch von {n} Gerät(en) verwendet. Die Geräte zuerst in einen anderen Bereich verschieben oder löschen.",
|
||||
@@ -737,7 +726,6 @@
|
||||
"marker.control_passive": "passive Quelle",
|
||||
"toast.marker_control_cycle": "Dieser Link würde eine kreisförmige Kette von Lichtsteuerungen erzeugen.",
|
||||
"toast.marker_binding_required": "Wählen Sie ein Home Assistant-Gerät, bevor Sie eine andere Lichtquelle verlinken.",
|
||||
"confirm.unlock": "„{name}“ entsperren?",
|
||||
"confirm.unlock_title": "Schloss öffnen?",
|
||||
"confirm.unlock_body": "House Plan sendet einen Befehl zum Öffnen an dieses Schloss.",
|
||||
"toast.files_migrate_failed": "Anhänge konnten nicht zur neuen Bindung verschoben werden, Links zeigen weiterhin auf die alten Dateien: {err}",
|
||||
@@ -748,7 +736,6 @@
|
||||
"space.used_by": "in Benutzung: {list}",
|
||||
"space.in_use": "Ein Bereich verwendet diesen Plan noch — zuerst die Verknüpfung lösen",
|
||||
"btn.use": "Verwenden",
|
||||
"confirm.delete_plan": "Plandatei „{name}“ vom Server löschen? Dies kann nicht rückgängig gemacht werden.",
|
||||
"confirm.delete_plan_title": "Plandatei löschen?",
|
||||
"confirm.delete_plan_body": "Die Datei wird vom Server gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden.",
|
||||
"toast.plans_list_failed": "Konnte die gespeicherten Pläne nicht auflisten: {err}",
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"history.draft_segment": "Add room-draft segment",
|
||||
"history.draft_merge": "Join unfinished room outlines",
|
||||
"history.draft_segment_delete": "Remove room-draft segment",
|
||||
"history.partition_add": "Add partition",
|
||||
"history.column_add": "Add column",
|
||||
"history.physical_edit": "Change physical object",
|
||||
"history.physical_delete": "Delete physical object",
|
||||
@@ -37,8 +36,6 @@
|
||||
"history.wall_chain_finish": "Finish wall chain",
|
||||
"history.wall_face_batch": "Create rooms from walls",
|
||||
"toast.column_duplicate": "A column with the same centre and outer size already exists",
|
||||
"confirm.delete_draft": "Delete the whole unfinished room contour?",
|
||||
"confirm.delete_draft_segment": "Delete this draft segment? The remaining contour may split in two.",
|
||||
"confirm.delete_draft_title": "Delete unfinished contour?",
|
||||
"confirm.delete_draft_body": "The entire unfinished room contour will be deleted. Cancelling makes no changes.",
|
||||
"confirm.delete_draft_segment_title": "Delete contour segment?",
|
||||
@@ -76,7 +73,6 @@
|
||||
"title.zoom_in": "Zoom in",
|
||||
"title.zoom_out": "Zoom out",
|
||||
"title.zoom_fit": "Fit all",
|
||||
"title.markup": "Room markup: grid, lines, outlines",
|
||||
"title.configure_space": "Configure space",
|
||||
"title.add_space": "Add space",
|
||||
"title.markup_add": "Walls: draw a continuous chain; Shift locks to 45°. Changing tool or leaving the editor finishes it as independent walls",
|
||||
@@ -121,7 +117,6 @@
|
||||
"history.edit_opening": "Edit opening",
|
||||
"history.move_opening": "Move opening",
|
||||
"history.delete_opening": "Delete opening",
|
||||
"history.delete_room": "Delete room",
|
||||
"history.delete_room_keep_walls": "Delete room, keep walls",
|
||||
"history.delete_room_with_walls": "Delete room and walls",
|
||||
"history.decor_add": "Add decor object",
|
||||
@@ -163,7 +158,6 @@
|
||||
"confirm.delete_partition_openings_title": "Delete wall and openings?",
|
||||
"confirm.delete_partition_openings_body": "This wall contains {count} opening(s). They will be deleted together.",
|
||||
"confirm.delete_partition_openings_item": "• {type}, {length}",
|
||||
"markup.delete": "Delete",
|
||||
"markup.hint_points": "points: {n} · Shift — 45° steps · Esc — finish chain · Ctrl+Z — undo a point · closing an area offers a room",
|
||||
"markup.hint_start": "click a grid dot to start a wall chain",
|
||||
"tip.lqi": "average zigbee signal:",
|
||||
@@ -192,9 +186,7 @@
|
||||
"display.badge": "Icon + state",
|
||||
"display.icon_ripple": "Icon + state and activity",
|
||||
"display.static_icon": "Always static icon",
|
||||
"marker.display_hint": "Icon + state changes the plate for work, open, alarm and unavailable states. Icon + state and activity additionally shows a short pulse for events and a continuous pulse for ongoing work, movement or presence. Value + state replaces the icon with one unambiguous HA value. A static icon never reacts to device states.",
|
||||
"marker.display_hint_badge": "The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.",
|
||||
"marker.display_hint_icon": "The icon and dynamic plate show device state without the ordinary activity pulse. Red alarms remain visible.",
|
||||
"marker.display_hint_icon_ripple": "The icon, dynamic plate and pulse show a short pulse for events and a continuous pulse for work, motion or presence. Red alarms have separate priority.",
|
||||
"marker.display_hint_value": "A selected or automatically resolved Home Assistant value replaces the icon while the plate continues to show state. Red alarms remain visible.",
|
||||
"marker.display_hint_static_icon": "The theme-aware shell and icon always stay the same. State, activity, unavailability and alarms do not change the face.",
|
||||
@@ -293,14 +285,11 @@
|
||||
"device.light_group": "light group",
|
||||
"device.fallback": "device",
|
||||
"device.virtual": "virtual device",
|
||||
"confirm.delete_room": "Delete room \"{name}\"?",
|
||||
"confirm.delete_room_title": "Delete room \"{name}\"?",
|
||||
"confirm.delete_room_body": "Choose whether the room's exclusive physical walls should remain. Walls shared with another room are always kept.",
|
||||
"confirm.remove_marker": "Delete \"{name}\" from the plan? The device will disappear completely and stop contributing to plan data. You can add it again later.",
|
||||
"confirm.remove_marker_title": "Delete device from the plan?",
|
||||
"confirm.remove_marker_body": "The device will disappear completely and stop contributing to plan data. You can add it again later.",
|
||||
"confirm.erase_decor": "Erase the {kind} object? You can undo this action from the editor history.",
|
||||
"confirm.delete_space": "Delete space \"{title}\" with all its rooms and markup?",
|
||||
"confirm.delete_space_title": "Delete space?",
|
||||
"confirm.delete_space_body": "The space, all its rooms and all its plan markup will be deleted.",
|
||||
"space.delete_blocked": "This space is still used by {n} device(s). Move them to another space or delete them first.",
|
||||
@@ -737,7 +726,6 @@
|
||||
"marker.control_passive": "passive source",
|
||||
"toast.marker_control_cycle": "This link would create a circular chain of light controls.",
|
||||
"toast.marker_binding_required": "Choose a Home Assistant device before linking another light source.",
|
||||
"confirm.unlock": "Unlock “{name}”?",
|
||||
"confirm.unlock_title": "Unlock?",
|
||||
"confirm.unlock_body": "House Plan will send an unlock command to this lock.",
|
||||
"toast.files_migrate_failed": "Attachments could not be moved to the new binding, links keep pointing at the old files: {err}",
|
||||
@@ -748,7 +736,6 @@
|
||||
"space.used_by": "in use: {list}",
|
||||
"space.in_use": "A space still uses this plan — detach it first",
|
||||
"btn.use": "Use",
|
||||
"confirm.delete_plan": "Delete the plan file \"{name}\" from the server? This cannot be undone.",
|
||||
"confirm.delete_plan_title": "Delete plan file?",
|
||||
"confirm.delete_plan_body": "The file will be deleted from the server. This action cannot be undone.",
|
||||
"toast.plans_list_failed": "Could not list the stored plans: {err}",
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"history.draft_segment": "Ajouter un segment de brouillon de pièce",
|
||||
"history.draft_merge": "Relier des contours de pièce inachevés",
|
||||
"history.draft_segment_delete": "Supprimer un segment de brouillon de pièce",
|
||||
"history.partition_add": "Ajouter une cloison",
|
||||
"history.column_add": "Ajouter une colonne",
|
||||
"history.physical_edit": "Modifier l’objet physique",
|
||||
"history.physical_delete": "Supprimer l’objet physique",
|
||||
@@ -37,8 +36,6 @@
|
||||
"history.wall_chain_finish": "Terminer la chaîne de murs",
|
||||
"history.wall_face_batch": "Créer des pièces à partir des murs",
|
||||
"toast.column_duplicate": "Une colonne avec le même centre et la même dimension extérieure existe déjà",
|
||||
"confirm.delete_draft": "Supprimer tout le contour de pièce inachevé ?",
|
||||
"confirm.delete_draft_segment": "Supprimer ce segment de brouillon ? Le contour restant peut se scinder en deux.",
|
||||
"confirm.delete_draft_title": "Supprimer le contour inachevé ?",
|
||||
"confirm.delete_draft_body": "Tout le contour de pièce inachevé sera supprimé. L’annulation ne modifie rien.",
|
||||
"confirm.delete_draft_segment_title": "Supprimer le segment de contour ?",
|
||||
@@ -76,7 +73,6 @@
|
||||
"title.zoom_in": "Zoomer",
|
||||
"title.zoom_out": "Dézoomer",
|
||||
"title.zoom_fit": "Tout ajuster",
|
||||
"title.markup": "Traçage des pièces : grille, lignes, contours",
|
||||
"title.configure_space": "Configurer l’espace",
|
||||
"title.add_space": "Ajouter un espace",
|
||||
"title.markup_add": "Murs : tracez une chaîne continue ; Maj verrouille à 45°. Changer d’outil ou quitter l’éditeur termine la chaîne en murs indépendants",
|
||||
@@ -121,7 +117,6 @@
|
||||
"history.edit_opening": "Modifier l’ouverture",
|
||||
"history.move_opening": "Déplacer l’ouverture",
|
||||
"history.delete_opening": "Supprimer l’ouverture",
|
||||
"history.delete_room": "Supprimer la pièce",
|
||||
"history.delete_room_keep_walls": "Supprimer la pièce, conserver les murs",
|
||||
"history.delete_room_with_walls": "Supprimer la pièce et les murs",
|
||||
"history.decor_add": "Ajouter un objet décoratif",
|
||||
@@ -163,7 +158,6 @@
|
||||
"confirm.delete_partition_openings_title": "Supprimer le mur et les ouvertures ?",
|
||||
"confirm.delete_partition_openings_body": "Ce mur contient {count} ouverture(s). Elles seront supprimées avec lui.",
|
||||
"confirm.delete_partition_openings_item": "• {type}, {length}",
|
||||
"markup.delete": "Supprimer",
|
||||
"markup.hint_points": "points : {n} · Maj — pas de 45° · Échap — terminer la chaîne · Ctrl+Z — annuler un point · fermer une zone propose de créer une pièce",
|
||||
"markup.hint_start": "cliquez sur un point de la grille pour commencer une chaîne de murs",
|
||||
"tip.lqi": "signal Zigbee moyen :",
|
||||
@@ -192,9 +186,7 @@
|
||||
"display.badge": "Icône + état",
|
||||
"display.icon_ripple": "Icône + état et activité",
|
||||
"display.static_icon": "Icône toujours statique",
|
||||
"marker.display_hint": "Icône + état modifie le fond pour les états en fonctionnement, ouvert, alarme et indisponible. Icône + état et activité ajoute une brève pulsation pour les événements et une pulsation continue pour une activité, un mouvement ou une présence en cours. Valeur + état remplace l’icône par une valeur HA non ambiguë. Une icône statique ne réagit jamais aux états de l’appareil.",
|
||||
"marker.display_hint_badge": "L’icône et le fond dynamique affichent l’état de l’appareil sans la pulsation d’activité habituelle. Les alarmes rouges restent visibles.",
|
||||
"marker.display_hint_icon": "L’icône et le fond dynamique affichent l’état de l’appareil sans la pulsation d’activité habituelle. Les alarmes rouges restent visibles.",
|
||||
"marker.display_hint_icon_ripple": "L’icône, le fond dynamique et la pulsation affichent une brève animation pour les événements et une animation continue pour le fonctionnement, le mouvement ou la présence. Les alarmes rouges restent prioritaires.",
|
||||
"marker.display_hint_value": "Une valeur Home Assistant sélectionnée ou résolue automatiquement remplace l’icône tandis que le fond continue d’indiquer l’état. Les alarmes rouges restent visibles.",
|
||||
"marker.display_hint_static_icon": "Le contour adapté au thème et l’icône restent toujours identiques. L’état, l’activité, l’indisponibilité et les alarmes ne modifient pas l’apparence.",
|
||||
@@ -293,14 +285,11 @@
|
||||
"device.light_group": "groupe de lumières",
|
||||
"device.fallback": "appareil",
|
||||
"device.virtual": "appareil virtuel",
|
||||
"confirm.delete_room": "Supprimer la pièce « {name} » ?",
|
||||
"confirm.delete_room_title": "Supprimer la pièce « {name} » ?",
|
||||
"confirm.delete_room_body": "Choisissez si les murs physiques propres à la pièce doivent être conservés. Les murs partagés avec une autre pièce sont toujours conservés.",
|
||||
"confirm.remove_marker": "Supprimer « {name} » du plan ? L’appareil disparaîtra complètement et ne contribuera plus aux données du plan. Vous pourrez l’ajouter de nouveau plus tard.",
|
||||
"confirm.remove_marker_title": "Supprimer l’appareil du plan ?",
|
||||
"confirm.remove_marker_body": "L’appareil disparaîtra complètement et ne contribuera plus aux données du plan. Vous pourrez l’ajouter de nouveau plus tard.",
|
||||
"confirm.erase_decor": "Effacer l’objet {kind} ? Vous pouvez annuler cette action depuis l’historique de l’éditeur.",
|
||||
"confirm.delete_space": "Supprimer l’espace « {title} » avec toutes ses pièces et son tracé ?",
|
||||
"confirm.delete_space_title": "Supprimer l’espace ?",
|
||||
"confirm.delete_space_body": "L’espace, toutes ses pièces et tout le tracé du plan seront supprimés.",
|
||||
"space.delete_blocked": "Cet espace est encore utilisé par {n} appareil(s). Déplacez-les vers un autre espace ou supprimez-les d’abord.",
|
||||
@@ -737,7 +726,6 @@
|
||||
"marker.control_passive": "source passive",
|
||||
"toast.marker_control_cycle": "Ce lien créerait une chaîne circulaire de contrôles de lumière.",
|
||||
"toast.marker_binding_required": "Choisissez un appareil Home Assistant avant de lier une autre source lumineuse.",
|
||||
"confirm.unlock": "Déverrouiller « {name} » ?",
|
||||
"confirm.unlock_title": "Déverrouiller ?",
|
||||
"confirm.unlock_body": "House Plan enverra une commande de déverrouillage à cette serrure.",
|
||||
"toast.files_migrate_failed": "Les pièces jointes n’ont pas pu être déplacées vers la nouvelle association ; les liens continuent de pointer vers les anciens fichiers : {err}",
|
||||
@@ -748,7 +736,6 @@
|
||||
"space.used_by": "utilisé par : {list}",
|
||||
"space.in_use": "Un espace utilise encore ce plan — dissociez-le d’abord",
|
||||
"btn.use": "Utiliser",
|
||||
"confirm.delete_plan": "Supprimer le fichier de plan « {name} » du serveur ? Cette action est irréversible.",
|
||||
"confirm.delete_plan_title": "Supprimer le fichier du plan ?",
|
||||
"confirm.delete_plan_body": "Le fichier sera supprimé du serveur. Cette action est irréversible.",
|
||||
"toast.plans_list_failed": "Impossible de lister les plans stockés : {err}",
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"history.draft_segment": "Добавление сегмента черновика комнаты",
|
||||
"history.draft_merge": "Соединение незавершённых контуров комнаты",
|
||||
"history.draft_segment_delete": "Удаление сегмента черновика комнаты",
|
||||
"history.partition_add": "Добавление перегородки",
|
||||
"history.column_add": "Добавление колонны",
|
||||
"history.physical_edit": "Изменение физического объекта",
|
||||
"history.physical_delete": "Удаление физического объекта",
|
||||
@@ -37,8 +36,6 @@
|
||||
"history.wall_chain_finish": "Завершение цепочки стен",
|
||||
"history.wall_face_batch": "Создание комнат из стен",
|
||||
"toast.column_duplicate": "Колонна с тем же центром и внешним размером уже существует",
|
||||
"confirm.delete_draft": "Удалить весь незавершённый контур комнаты?",
|
||||
"confirm.delete_draft_segment": "Удалить этот сегмент черновика? Оставшийся контур может разделиться на два.",
|
||||
"confirm.delete_draft_title": "Удалить незавершённый контур?",
|
||||
"confirm.delete_draft_body": "Весь незавершённый контур комнаты будет удалён. При отмене ничего не изменится.",
|
||||
"confirm.delete_draft_segment_title": "Удалить сегмент контура?",
|
||||
@@ -76,7 +73,6 @@
|
||||
"title.zoom_in": "Приблизить",
|
||||
"title.zoom_out": "Отдалить",
|
||||
"title.zoom_fit": "Вписать всё",
|
||||
"title.markup": "Разметка комнат: сетка, линии, контуры",
|
||||
"title.configure_space": "Настроить пространство",
|
||||
"title.add_space": "Добавить пространство",
|
||||
"title.markup_add": "Стены: рисуйте непрерывную цепочку; Shift фиксирует 45°. Смена инструмента или выход завершает её как независимые стены",
|
||||
@@ -121,7 +117,6 @@
|
||||
"history.edit_opening": "Изменение проёма",
|
||||
"history.move_opening": "Перемещение проёма",
|
||||
"history.delete_opening": "Удаление проёма",
|
||||
"history.delete_room": "Удаление комнаты",
|
||||
"history.delete_room_keep_walls": "Удаление комнаты с сохранением стен",
|
||||
"history.delete_room_with_walls": "Удаление комнаты и её стен",
|
||||
"history.decor_add": "Добавление объекта декора",
|
||||
@@ -163,7 +158,6 @@
|
||||
"confirm.delete_partition_openings_title": "Удалить стену и проёмы?",
|
||||
"confirm.delete_partition_openings_body": "В стене есть проёмы: {count}. Они будут удалены вместе со стеной.",
|
||||
"confirm.delete_partition_openings_item": "• {type}, {length}",
|
||||
"markup.delete": "Удалить",
|
||||
"markup.hint_points": "точек: {n} · Shift — шаг 45° · Esc — завершить цепочку · Ctrl+Z — убрать точку · при замыкании будет предложена комната",
|
||||
"markup.hint_start": "кликните точку сетки, чтобы начать цепочку стен",
|
||||
"tip.lqi": "средний сигнал zigbee:",
|
||||
@@ -192,9 +186,7 @@
|
||||
"display.badge": "Значок + состояние",
|
||||
"display.icon_ripple": "Значок + состояние и активность",
|
||||
"display.static_icon": "Всегда статичный значок",
|
||||
"marker.display_hint": "Значок + состояние меняет подложку при работе, открытии, тревоге и недоступности. Значок + состояние и активность дополнительно показывает короткую пульсацию для событий и постоянную — для работы, движения или присутствия. Значение + состояние заменяет значок одним однозначным значением HA. Статичный значок не реагирует на состояния устройства.",
|
||||
"marker.display_hint_badge": "Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.",
|
||||
"marker.display_hint_icon": "Значок и динамическая подложка показывают состояние устройства без обычной пульсации активности. Красная тревога сохраняется.",
|
||||
"marker.display_hint_icon_ripple": "Значок, динамическая подложка и пульсация: короткая — для событий, постоянная — для работы, движения или присутствия. Красная тревога имеет отдельный приоритет.",
|
||||
"marker.display_hint_value": "Значок заменяется выбранным или автоматически найденным значением Home Assistant; подложка продолжает показывать состояние. Красная тревога сохраняется.",
|
||||
"marker.display_hint_static_icon": "Подложка в цветах темы и значок всегда остаются одинаковыми. Состояния, активность, недоступность и тревоги не меняют отображение.",
|
||||
@@ -293,14 +285,11 @@
|
||||
"device.light_group": "группа света",
|
||||
"device.fallback": "устройство",
|
||||
"device.virtual": "виртуальное устройство",
|
||||
"confirm.delete_room": "Удалить комнату «{name}»?",
|
||||
"confirm.delete_room_title": "Удалить комнату «{name}»?",
|
||||
"confirm.delete_room_body": "Выберите, нужно ли оставить физические стены, принадлежащие только этой комнате. Общие с другой комнатой стены сохраняются всегда.",
|
||||
"confirm.remove_marker": "Удалить «{name}» с плана? Устройство исчезнет полностью и перестанет участвовать в данных плана. Позже его можно будет добавить заново.",
|
||||
"confirm.remove_marker_title": "Удалить устройство с плана?",
|
||||
"confirm.remove_marker_body": "Устройство исчезнет полностью и перестанет участвовать в данных плана. Позже его можно будет добавить заново.",
|
||||
"confirm.erase_decor": "Стереть объект «{kind}»? Действие можно отменить из истории редактора.",
|
||||
"confirm.delete_space": "Удалить пространство «{title}» со всеми комнатами и разметкой?",
|
||||
"confirm.delete_space_title": "Удалить пространство?",
|
||||
"confirm.delete_space_body": "Пространство, все его комнаты и вся разметка плана будут удалены.",
|
||||
"space.delete_blocked": "Это пространство всё ещё используется устройствами: {n}. Сначала перенесите их в другое пространство или удалите.",
|
||||
@@ -737,7 +726,6 @@
|
||||
"marker.control_passive": "пассивный источник",
|
||||
"toast.marker_control_cycle": "Такая связь создаст циклическую цепочку управления светом.",
|
||||
"toast.marker_binding_required": "Сначала выберите устройство Home Assistant, затем добавьте управляемый источник света.",
|
||||
"confirm.unlock": "Открыть замок «{name}»?",
|
||||
"confirm.unlock_title": "Открыть замок?",
|
||||
"confirm.unlock_body": "House Plan отправит этому замку команду открытия.",
|
||||
"toast.files_migrate_failed": "Не удалось перенести вложения к новой привязке, ссылки остались на старые файлы: {err}",
|
||||
@@ -748,7 +736,6 @@
|
||||
"space.used_by": "используется: {list}",
|
||||
"space.in_use": "План используется пространством — сначала отцепите его",
|
||||
"btn.use": "Выбрать",
|
||||
"confirm.delete_plan": "Удалить файл плана «{name}» с сервера? Действие необратимо.",
|
||||
"confirm.delete_plan_title": "Удалить файл плана?",
|
||||
"confirm.delete_plan_body": "Файл будет удалён с сервера. Это действие нельзя отменить.",
|
||||
"toast.plans_list_failed": "Не удалось получить список планов: {err}",
|
||||
|
||||
@@ -2,6 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
applyAreaRelocationResolution,
|
||||
MARKER_AREA_SNAPSHOT_LIMIT,
|
||||
markerAreaSnapshotOf,
|
||||
removeMarkerAreaSnapshots,
|
||||
registryFollowingBinding,
|
||||
@@ -166,6 +167,37 @@ test('unresolved duplicate Area and non-authoritative registry are no-ops', () =
|
||||
assert.deepEqual(resolve({ authoritative: false }), { decisions: [], relocateIds: new Set() });
|
||||
});
|
||||
|
||||
test('authoritative registry removes orphan snapshots but preserves live marker ids', () => {
|
||||
const snapshot = {
|
||||
orphan: { binding: 'device:orphan', area: 'area-a' },
|
||||
'marker-id': { binding: 'device:marker-id', area: 'area-a' },
|
||||
'canonical-device-id': { binding: 'device:device-row', area: 'area-a' },
|
||||
};
|
||||
const result = resolve({
|
||||
devices: [device({
|
||||
id: 'device-row',
|
||||
bindingRef: 'device-row',
|
||||
marker: { id: 'marker-id', binding: 'device:marker-id', removed: true },
|
||||
})],
|
||||
snapshot,
|
||||
});
|
||||
const orphan = result.decisions.find((decision) => decision.id === 'orphan');
|
||||
assert.equal(orphan?.reason, 'registry-unverified');
|
||||
assert.equal(orphan?.removeSnapshot, true);
|
||||
assert.equal(result.decisions.some((decision) => decision.id === 'marker-id'), false);
|
||||
assert.deepEqual(applyAreaRelocationResolution(snapshot, result), {
|
||||
'marker-id': snapshot['marker-id'],
|
||||
'canonical-device-id': snapshot['canonical-device-id'],
|
||||
});
|
||||
});
|
||||
|
||||
test('non-authoritative registry preserves orphan snapshots', () => {
|
||||
const snapshot = { orphan: { binding: 'device:orphan', area: 'area-a' } };
|
||||
const result = resolve({ devices: [], snapshot, authoritative: false });
|
||||
assert.deepEqual(result, { decisions: [], relocateIds: new Set() });
|
||||
assert.deepEqual(applyAreaRelocationResolution(snapshot, result), snapshot);
|
||||
});
|
||||
|
||||
test('failed delete does not advance relocation provenance', () => {
|
||||
const previous = { 'device-1': { binding: 'device:device-1', area: 'area-a' } };
|
||||
const result = resolve({ snapshot: previous });
|
||||
@@ -180,6 +212,21 @@ test('defensive snapshot reader drops malformed entries without poisoning valid
|
||||
}), { good: { binding: 'entity:sensor.good', area: 'kitchen' } });
|
||||
});
|
||||
|
||||
test('defensive snapshot reader keeps the newest entries when over its limit', () => {
|
||||
const source = Object.fromEntries(Array.from(
|
||||
{ length: MARKER_AREA_SNAPSHOT_LIMIT + 2 },
|
||||
(_, index) => [`entry-${index}`, { binding: `device:${index}`, area: 'area-a' }],
|
||||
));
|
||||
const snapshot = markerAreaSnapshotOf(source);
|
||||
assert.equal(Object.keys(snapshot).length, MARKER_AREA_SNAPSHOT_LIMIT);
|
||||
assert.equal(Object.hasOwn(snapshot, 'entry-0'), false);
|
||||
assert.equal(Object.hasOwn(snapshot, 'entry-1'), false);
|
||||
assert.deepEqual(snapshot['entry-2'], { binding: 'device:2', area: 'area-a' });
|
||||
assert.deepEqual(snapshot[`entry-${MARKER_AREA_SNAPSHOT_LIMIT + 1}`], {
|
||||
binding: `device:${MARKER_AREA_SNAPSHOT_LIMIT + 1}`, area: 'area-a',
|
||||
});
|
||||
});
|
||||
|
||||
test('marker deletion and rebind cleanup remove only their own lifecycle entries', () => {
|
||||
const source = {
|
||||
keep: { binding: 'device:keep', area: 'living' },
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import test from 'node:test';
|
||||
import ts from 'typescript';
|
||||
|
||||
const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const dictionary = JSON.parse(readFileSync(join(repoRoot, 'src/i18n/en.json'), 'utf8'));
|
||||
const dictionaryKeys = Object.keys(dictionary);
|
||||
|
||||
const sourceFiles = [];
|
||||
const visitDirectory = (directory) => {
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
const path = join(directory, entry.name);
|
||||
if (entry.isDirectory()) visitDirectory(path);
|
||||
else if (entry.isFile() && entry.name.endsWith('.ts')) sourceFiles.push(path);
|
||||
}
|
||||
};
|
||||
visitDirectory(join(repoRoot, 'src'));
|
||||
|
||||
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
/** Convert a string-producing expression into a dictionary-key matcher. */
|
||||
const expressionPattern = (node) => {
|
||||
if (ts.isStringLiteralLike(node)) return { source: escapeRegExp(node.text), dynamic: false };
|
||||
if (ts.isParenthesizedExpression(node)) return expressionPattern(node.expression);
|
||||
if (ts.isTemplateExpression(node)) {
|
||||
let source = escapeRegExp(node.head.text);
|
||||
for (const span of node.templateSpans) {
|
||||
source += '.+' + escapeRegExp(span.literal.text);
|
||||
}
|
||||
return { source, dynamic: true };
|
||||
}
|
||||
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
||||
const left = expressionPattern(node.left);
|
||||
const right = expressionPattern(node.right);
|
||||
if (!left && !right) return null;
|
||||
return {
|
||||
source: (left?.source || '.+') + (right?.source || '.+'),
|
||||
dynamic: (left?.dynamic ?? true) || (right?.dynamic ?? true),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const literalConsumers = new Set();
|
||||
const dynamicConsumers = [];
|
||||
const derivedHelpAria = new Set();
|
||||
|
||||
for (const path of sourceFiles) {
|
||||
const source = ts.createSourceFile(
|
||||
path,
|
||||
readFileSync(path, 'utf8'),
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TS,
|
||||
);
|
||||
const visit = (node) => {
|
||||
if (ts.isStringLiteralLike(node)) literalConsumers.add(node.text);
|
||||
|
||||
if (ts.isTemplateExpression(node)
|
||||
|| (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken)) {
|
||||
const pattern = expressionPattern(node);
|
||||
if (pattern?.dynamic && /[A-Za-z]/.test(pattern.source)) {
|
||||
dynamicConsumers.push(new RegExp(`^${pattern.source}$`));
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isCallExpression(node) && node.arguments.length) {
|
||||
const callee = ts.isPropertyAccessExpression(node.expression)
|
||||
? node.expression.name.text
|
||||
: ts.isIdentifier(node.expression) ? node.expression.text : '';
|
||||
const key = node.arguments[0];
|
||||
if (callee === '_help' && ts.isStringLiteralLike(key) && key.text.endsWith('.help')) {
|
||||
derivedHelpAria.add(`${key.text}.aria`);
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(source);
|
||||
}
|
||||
|
||||
test('every i18n key has a literal, dynamic-family or derived help consumer', () => {
|
||||
const unused = dictionaryKeys.filter((key) => !literalConsumers.has(key)
|
||||
&& !derivedHelpAria.has(key)
|
||||
&& !dynamicConsumers.some((pattern) => pattern.test(key)));
|
||||
assert.deepEqual(unused, [], unused.length
|
||||
? `Unused i18n keys: ${unused.join(', ')}. Use each key from src/ or delete it from every locale.`
|
||||
: undefined);
|
||||
});
|
||||
|
||||
test('help accessibility copy is derived from every literal help consumer', () => {
|
||||
assert.equal(derivedHelpAria.size, 19, 'the current settings surface has 19 help descriptions');
|
||||
for (const key of derivedHelpAria) {
|
||||
assert.equal(typeof dictionary[key], 'string', `${key} must accompany its .help consumer`);
|
||||
}
|
||||
});
|
||||
@@ -29,7 +29,6 @@ test('only old tool copy is removed while persisted partition copy remains', ()
|
||||
for (const key of [
|
||||
'markup.partition',
|
||||
'physical.partition_properties',
|
||||
'history.partition_add',
|
||||
]) assert.equal(typeof locale[key], 'string', key);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user