Fix support preview and retry races

Issue: #418
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-09-02 17:10:05 +03:00
parent e3073f6840
commit 2b978d63ef
25 changed files with 453 additions and 214 deletions
@@ -1,125 +1,125 @@
{
"schema": 1,
"fingerprint": "c3f3b28b3149752a2b63584dbe161b2a247db77770fd5bd84bd87a14f303b3a6",
"fingerprint": "86bca8107c5d9e57b05a46242a839b6a888240ff8560cc296275e39dbc668668",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-DoTfrXZx.js",
"houseplan-assets/houseplan-card-tbclbSeu.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 290566,
"initialViewGzipBytes": 290568,
"lazyFiles": [
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/de-Bja91KNY.js",
"houseplan-assets/editor-CMVCfgT3.js",
"houseplan-assets/fr-ClzwnAMY.js",
"houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js",
"houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js"
"houseplan-assets/backdrop-pick-B0-0LPJ7.js",
"houseplan-assets/de-Co7h1A4k.js",
"houseplan-assets/editor-kfbSO1xJ.js",
"houseplan-assets/fr-BOp29Mo4.js",
"houseplan-assets/houseplan-editor-runtime-Dy3ho_PX.js",
"houseplan-assets/houseplan-onboarding-runtime-BuI5u78M.js"
],
"lazyGzipBytes": 205825,
"lazyGzipBytes": 206073,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/editor-CMVCfgT3.js",
"houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js"
"houseplan-assets/backdrop-pick-B0-0LPJ7.js",
"houseplan-assets/editor-kfbSO1xJ.js",
"houseplan-assets/houseplan-editor-runtime-Dy3ho_PX.js"
],
"lazyEditorGzipBytes": 148718,
"lazyEditorGzipBytes": 148958,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js"
"houseplan-assets/backdrop-pick-B0-0LPJ7.js",
"houseplan-assets/houseplan-onboarding-runtime-BuI5u78M.js"
],
"lazyOnboardingGzipBytes": 14032,
"lazyOnboardingGzipBytes": 14030,
"lazyLocaleFiles": [
"houseplan-assets/de-Bja91KNY.js",
"houseplan-assets/fr-ClzwnAMY.js"
"houseplan-assets/de-Co7h1A4k.js",
"houseplan-assets/fr-BOp29Mo4.js"
],
"lazyLocaleGzipBytes": 50147,
"lazyLocaleGzipBytes": 50156,
"files": [
{
"path": "houseplan-assets/backdrop-pick-CrHBqjf1.js",
"sha256": "5dfc1f1a537f2026f2b2e0628bde0dde81e62278f9fd07647b748a334af220ac",
"path": "houseplan-assets/backdrop-pick-B0-0LPJ7.js",
"sha256": "d686cf21b2714cb7a0b6a6992ae111135b5ec6f344aba7045ce92f865614917d",
"rawBytes": 20636,
"gzipBytes": 7072,
"gzipBytes": 7071,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DoTfrXZx.js"
"houseplan-assets/houseplan-card-tbclbSeu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-Bja91KNY.js",
"sha256": "62fc9d40d1de845df4c83a527d1426b927c7b87b2a83da8271e14485edebca51",
"path": "houseplan-assets/de-Co7h1A4k.js",
"sha256": "7d508bfc2ac62d6b554a17455710be751291867a02f158d7af33868c57b8ea9a",
"rawBytes": 83981,
"gzipBytes": 25344,
"gzipBytes": 25349,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-CMVCfgT3.js",
"sha256": "408f5bc440896e6b29eaf3ab59d30960974215e3d0341161675a3e88a8f2700e",
"path": "houseplan-assets/editor-kfbSO1xJ.js",
"sha256": "a515e5da0e979ac670a06aeff4eda8645b5508484666b902deb21cc91eab2eaa",
"rawBytes": 3826,
"gzipBytes": 1583,
"gzipBytes": 1580,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DoTfrXZx.js"
"houseplan-assets/houseplan-card-tbclbSeu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-ClzwnAMY.js",
"sha256": "976d7e2294ffccbd4223e2ffba4b5c43697edbb8e1baa37d6b785f6a98c69bcc",
"path": "houseplan-assets/fr-BOp29Mo4.js",
"sha256": "925c012768c6fff20143c0dc23e28272a9237eb2cfe316c0a845a9ef836163d6",
"rawBytes": 86212,
"gzipBytes": 24803,
"gzipBytes": 24807,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DoTfrXZx.js",
"sha256": "40cc6422072c6017550b0cbf73a772ae4839e172d1187510f1eb92dd78ccb1d5",
"path": "houseplan-assets/houseplan-card-tbclbSeu.js",
"sha256": "5a2ef33b7b2fee90a86909f9112ed4426634bb92539c36fb6492415e5971984d",
"rawBytes": 1032986,
"gzipBytes": 289769,
"gzipBytes": 289772,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-Bja91KNY.js",
"houseplan-assets/editor-CMVCfgT3.js",
"houseplan-assets/fr-ClzwnAMY.js",
"houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js",
"houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js"
"houseplan-assets/de-Co7h1A4k.js",
"houseplan-assets/editor-kfbSO1xJ.js",
"houseplan-assets/fr-BOp29Mo4.js",
"houseplan-assets/houseplan-editor-runtime-Dy3ho_PX.js",
"houseplan-assets/houseplan-onboarding-runtime-BuI5u78M.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js",
"sha256": "9d86b76630217ad0b95a8f9f5b3ee8c04776f3450f911b3d24eb5c0d6170e10b",
"rawBytes": 543265,
"gzipBytes": 140063,
"path": "houseplan-assets/houseplan-editor-runtime-Dy3ho_PX.js",
"sha256": "b7924f294dab05b23b3cccb6201ed69a48498eb603e965493c13fdaf96c5b9fa",
"rawBytes": 544110,
"gzipBytes": 140307,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/houseplan-card-DoTfrXZx.js"
"houseplan-assets/backdrop-pick-B0-0LPJ7.js",
"houseplan-assets/houseplan-card-tbclbSeu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js",
"sha256": "9e5fb7f22b8a9fba1afd1e5930b8da2e1250a55aac7610d60061d9cb80aa9091",
"path": "houseplan-assets/houseplan-onboarding-runtime-BuI5u78M.js",
"sha256": "a7e139c1110cbfba8df39dd8010508fd5120cb734fd33873e15264b031149882",
"rawBytes": 28088,
"gzipBytes": 6960,
"gzipBytes": 6959,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/houseplan-card-DoTfrXZx.js"
"houseplan-assets/backdrop-pick-B0-0LPJ7.js",
"houseplan-assets/houseplan-card-tbclbSeu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "9e0f7a84c95cc7aec1c4e3e20e3a8663e12f418a089e3897dcac67fa1be94ae0",
"sha256": "5aeb63b7e05cd86436a5e97e91ca00e27b943fcc2475dee490b21902f5ff23bb",
"rawBytes": 1183,
"gzipBytes": 797,
"gzipBytes": 796,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-DoTfrXZx.js"
"houseplan-assets/houseplan-card-tbclbSeu.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c3f3b28b3149752a2b63584dbe161b2a247db77770fd5bd84bd87a14f303b3a6";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-DoTfrXZx.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__="86bca8107c5d9e57b05a46242a839b6a888240ff8560cc296275e39dbc668668";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-tbclbSeu.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
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c3f3b28b3149752a2b63584dbe161b2a247db77770fd5bd84bd87a14f303b3a6";try{await import("./houseplan-assets/houseplan-card-DoTfrXZx.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__="86bca8107c5d9e57b05a46242a839b6a888240ff8560cc296275e39dbc668668";try{await import("./houseplan-assets/houseplan-card-tbclbSeu.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)}
+124 -10
View File
@@ -46,6 +46,19 @@ const result = await page.evaluate(async ({ version, previewText, previewSha })
const originalCallWS = card.hass.callWS.bind(card.hass);
const calls = [];
let submitMode = 'success';
let previewToken = 'a';
let deferPreviews = false;
const pendingPreviews = [];
const previewPayload = (tokenChar = previewToken) => ({
text: previewText,
size: new TextEncoder().encode(previewText).byteLength,
expires_in: 300,
spaces: 2,
token: tokenChar.repeat(48),
sha256: previewSha,
version: 1,
format: 'houseplan-support-package',
});
card.hass = {
...card.hass,
callWS: async (message) => {
@@ -54,16 +67,10 @@ const result = await page.evaluate(async ({ version, previewText, previewSha })
}
calls.push(structuredClone(message));
if (message.type === 'houseplan/support/preview') {
return {
text: previewText,
size: new TextEncoder().encode(previewText).byteLength,
expires_in: 300,
spaces: 2,
token: 'a'.repeat(48),
sha256: previewSha,
version: 1,
format: 'houseplan-support-package',
};
if (deferPreviews) {
return new Promise((resolve, reject) => pendingPreviews.push({ resolve, reject }));
}
return previewPayload();
}
if (message.type === 'houseplan/support/submit') {
if (submitMode === 'rate') throw { code: 'support_rate_limited' };
@@ -244,6 +251,113 @@ const result = await page.evaluate(async ({ version, previewText, previewSha })
&& retryRequests.every((call) => call.idempotency_key === retryKey)
&& root().querySelector('#support-receipt')?.textContent.includes('HP-43-SMOKE') === true;
await close();
await open();
deferPreviews = true;
const canceledBuild = card._editorRuntime._setSupportAttachment(true);
await until(() => pendingPreviews.length === 1
&& card._supportDialog?.status === 'building', 'deferred preview building');
await update();
const disabledWhileBuilding = root().querySelector('.supportattach input')?.disabled === true;
const canceledRequest = pendingPreviews.shift();
await card._editorRuntime._setSupportAttachment(false);
await update();
const hiddenImmediately = card._supportDialog?.attach === false
&& card._supportDialog?.status === 'idle'
&& card._supportDialog?.preview === null
&& !root().querySelector('.supportpreview');
canceledRequest.resolve(previewPayload('b'));
await canceledBuild;
await update();
const discardedB = calls.filter((call) => call.type === 'houseplan/support/preview/discard'
&& call.token === 'b'.repeat(48)).length;
out.latePreviewConsentStaysRevoked = disabledWhileBuilding && hiddenImmediately
&& card._supportDialog?.attach === false
&& card._supportDialog?.status === 'idle'
&& card._supportDialog?.preview === null
&& !root().querySelector('.supportpreview')
&& discardedB === 1;
const oldBuild = card._editorRuntime._setSupportAttachment(true);
await until(() => pendingPreviews.length === 1, 'old preview queued');
const oldRequest = pendingPreviews.shift();
await card._editorRuntime._setSupportAttachment(false);
const latestBuild = card._editorRuntime._setSupportAttachment(true);
await until(() => pendingPreviews.length === 1, 'latest preview queued');
const latestRequest = pendingPreviews.shift();
latestRequest.resolve(previewPayload('c'));
await latestBuild;
oldRequest.resolve(previewPayload('d'));
await oldBuild;
await update();
out.latestPreviewGenerationWins = card._supportDialog?.attach === true
&& card._supportDialog?.status === 'ready'
&& card._supportDialog?.preview?.token === 'c'.repeat(48)
&& calls.filter((call) => call.type === 'houseplan/support/preview/discard'
&& call.token === 'd'.repeat(48)).length === 1;
const rejectedBuild = card._editorRuntime._refreshSupportPreview();
await until(() => pendingPreviews.length === 1
&& card._supportDialog?.status === 'building', 'rejected stale preview queued');
const rejectedRequest = pendingPreviews.shift();
await card._editorRuntime._setSupportAttachment(false);
rejectedRequest.reject({ code: 'support_rejected' });
await rejectedBuild;
await update();
out.stalePreviewErrorIsIgnored = card._supportDialog?.attach === false
&& card._supportDialog?.status === 'idle'
&& card._supportDialog?.errorCode === ''
&& card._supportDialog?.preview === null
&& !root().querySelector('.supportpreview');
deferPreviews = false;
await close();
await open();
const validationKey = card._supportDialog.idempotencyKey;
const submitsBeforeEmptyValidation = calls.filter(
(call) => call.type === 'houseplan/support/submit',
).length;
await card._editorRuntime._submitSupport();
await update();
out.validationDoesNotRotateKey = card._supportDialog?.idempotencyKey === validationKey
&& card._supportDialog?.submissionFingerprint === ''
&& calls.filter((call) => call.type === 'houseplan/support/submit').length
=== submitsBeforeEmptyValidation;
const failSubmit = async () => {
await card._editorRuntime._submitSupport();
await until(() => card._supportDialog?.status === 'error', 'payload submit failure');
await update();
return calls.findLast((call) => call.type === 'houseplan/support/submit');
};
submitMode = 'rate';
await setInput('#support-message', ' First payload ');
const firstPayloadRequest = await failSubmit();
await setInput('#support-message', 'First payload ');
const trimOnlyRequest = await failSubmit();
await setInput('#support-message', 'Second payload');
const changedMessageRequest = await failSubmit();
await setInput('#support-contact', 'owner@example.test');
const changedContactRequest = await failSubmit();
previewToken = 'e';
await card._editorRuntime._setSupportAttachment(true);
const attachedRequest = await failSubmit();
await card._editorRuntime._setSupportAttachment(false);
previewToken = 'f';
await card._editorRuntime._setSupportAttachment(true);
const changedPreviewRequest = await failSubmit();
out.idempotencyKeyFollowsEffectivePayload = firstPayloadRequest?.idempotency_key === validationKey
&& trimOnlyRequest?.idempotency_key === firstPayloadRequest?.idempotency_key
&& changedMessageRequest?.idempotency_key !== trimOnlyRequest?.idempotency_key
&& changedContactRequest?.idempotency_key !== changedMessageRequest?.idempotency_key
&& attachedRequest?.idempotency_key !== changedContactRequest?.idempotency_key
&& attachedRequest?.preview_token === 'e'.repeat(48)
&& changedPreviewRequest?.idempotency_key !== attachedRequest?.idempotency_key
&& changedPreviewRequest?.preview_token === 'f'.repeat(48)
&& firstPayloadRequest?.message === 'First payload'
&& firstPayloadRequest?.contact === '';
await close();
return out;
}, { version: VERSION, previewText: PREVIEW_TEXT, previewSha: PREVIEW_SHA });
+56 -56
View File
@@ -1,125 +1,125 @@
{
"schema": 1,
"fingerprint": "c3f3b28b3149752a2b63584dbe161b2a247db77770fd5bd84bd87a14f303b3a6",
"fingerprint": "86bca8107c5d9e57b05a46242a839b6a888240ff8560cc296275e39dbc668668",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-DoTfrXZx.js",
"houseplan-assets/houseplan-card-tbclbSeu.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 290566,
"initialViewGzipBytes": 290568,
"lazyFiles": [
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/de-Bja91KNY.js",
"houseplan-assets/editor-CMVCfgT3.js",
"houseplan-assets/fr-ClzwnAMY.js",
"houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js",
"houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js"
"houseplan-assets/backdrop-pick-B0-0LPJ7.js",
"houseplan-assets/de-Co7h1A4k.js",
"houseplan-assets/editor-kfbSO1xJ.js",
"houseplan-assets/fr-BOp29Mo4.js",
"houseplan-assets/houseplan-editor-runtime-Dy3ho_PX.js",
"houseplan-assets/houseplan-onboarding-runtime-BuI5u78M.js"
],
"lazyGzipBytes": 205825,
"lazyGzipBytes": 206073,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/editor-CMVCfgT3.js",
"houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js"
"houseplan-assets/backdrop-pick-B0-0LPJ7.js",
"houseplan-assets/editor-kfbSO1xJ.js",
"houseplan-assets/houseplan-editor-runtime-Dy3ho_PX.js"
],
"lazyEditorGzipBytes": 148718,
"lazyEditorGzipBytes": 148958,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js"
"houseplan-assets/backdrop-pick-B0-0LPJ7.js",
"houseplan-assets/houseplan-onboarding-runtime-BuI5u78M.js"
],
"lazyOnboardingGzipBytes": 14032,
"lazyOnboardingGzipBytes": 14030,
"lazyLocaleFiles": [
"houseplan-assets/de-Bja91KNY.js",
"houseplan-assets/fr-ClzwnAMY.js"
"houseplan-assets/de-Co7h1A4k.js",
"houseplan-assets/fr-BOp29Mo4.js"
],
"lazyLocaleGzipBytes": 50147,
"lazyLocaleGzipBytes": 50156,
"files": [
{
"path": "houseplan-assets/backdrop-pick-CrHBqjf1.js",
"sha256": "5dfc1f1a537f2026f2b2e0628bde0dde81e62278f9fd07647b748a334af220ac",
"path": "houseplan-assets/backdrop-pick-B0-0LPJ7.js",
"sha256": "d686cf21b2714cb7a0b6a6992ae111135b5ec6f344aba7045ce92f865614917d",
"rawBytes": 20636,
"gzipBytes": 7072,
"gzipBytes": 7071,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DoTfrXZx.js"
"houseplan-assets/houseplan-card-tbclbSeu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-Bja91KNY.js",
"sha256": "62fc9d40d1de845df4c83a527d1426b927c7b87b2a83da8271e14485edebca51",
"path": "houseplan-assets/de-Co7h1A4k.js",
"sha256": "7d508bfc2ac62d6b554a17455710be751291867a02f158d7af33868c57b8ea9a",
"rawBytes": 83981,
"gzipBytes": 25344,
"gzipBytes": 25349,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-CMVCfgT3.js",
"sha256": "408f5bc440896e6b29eaf3ab59d30960974215e3d0341161675a3e88a8f2700e",
"path": "houseplan-assets/editor-kfbSO1xJ.js",
"sha256": "a515e5da0e979ac670a06aeff4eda8645b5508484666b902deb21cc91eab2eaa",
"rawBytes": 3826,
"gzipBytes": 1583,
"gzipBytes": 1580,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DoTfrXZx.js"
"houseplan-assets/houseplan-card-tbclbSeu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-ClzwnAMY.js",
"sha256": "976d7e2294ffccbd4223e2ffba4b5c43697edbb8e1baa37d6b785f6a98c69bcc",
"path": "houseplan-assets/fr-BOp29Mo4.js",
"sha256": "925c012768c6fff20143c0dc23e28272a9237eb2cfe316c0a845a9ef836163d6",
"rawBytes": 86212,
"gzipBytes": 24803,
"gzipBytes": 24807,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-DoTfrXZx.js",
"sha256": "40cc6422072c6017550b0cbf73a772ae4839e172d1187510f1eb92dd78ccb1d5",
"path": "houseplan-assets/houseplan-card-tbclbSeu.js",
"sha256": "5a2ef33b7b2fee90a86909f9112ed4426634bb92539c36fb6492415e5971984d",
"rawBytes": 1032986,
"gzipBytes": 289769,
"gzipBytes": 289772,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-Bja91KNY.js",
"houseplan-assets/editor-CMVCfgT3.js",
"houseplan-assets/fr-ClzwnAMY.js",
"houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js",
"houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js"
"houseplan-assets/de-Co7h1A4k.js",
"houseplan-assets/editor-kfbSO1xJ.js",
"houseplan-assets/fr-BOp29Mo4.js",
"houseplan-assets/houseplan-editor-runtime-Dy3ho_PX.js",
"houseplan-assets/houseplan-onboarding-runtime-BuI5u78M.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-BSYUK6nC.js",
"sha256": "9d86b76630217ad0b95a8f9f5b3ee8c04776f3450f911b3d24eb5c0d6170e10b",
"rawBytes": 543265,
"gzipBytes": 140063,
"path": "houseplan-assets/houseplan-editor-runtime-Dy3ho_PX.js",
"sha256": "b7924f294dab05b23b3cccb6201ed69a48498eb603e965493c13fdaf96c5b9fa",
"rawBytes": 544110,
"gzipBytes": 140307,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/houseplan-card-DoTfrXZx.js"
"houseplan-assets/backdrop-pick-B0-0LPJ7.js",
"houseplan-assets/houseplan-card-tbclbSeu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-B9wPxaO1.js",
"sha256": "9e5fb7f22b8a9fba1afd1e5930b8da2e1250a55aac7610d60061d9cb80aa9091",
"path": "houseplan-assets/houseplan-onboarding-runtime-BuI5u78M.js",
"sha256": "a7e139c1110cbfba8df39dd8010508fd5120cb734fd33873e15264b031149882",
"rawBytes": 28088,
"gzipBytes": 6960,
"gzipBytes": 6959,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CrHBqjf1.js",
"houseplan-assets/houseplan-card-DoTfrXZx.js"
"houseplan-assets/backdrop-pick-B0-0LPJ7.js",
"houseplan-assets/houseplan-card-tbclbSeu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "9e0f7a84c95cc7aec1c4e3e20e3a8663e12f418a089e3897dcac67fa1be94ae0",
"sha256": "5aeb63b7e05cd86436a5e97e91ca00e27b943fcc2475dee490b21902f5ff23bb",
"rawBytes": 1183,
"gzipBytes": 797,
"gzipBytes": 796,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-DoTfrXZx.js"
"houseplan-assets/houseplan-card-tbclbSeu.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,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c3f3b28b3149752a2b63584dbe161b2a247db77770fd5bd84bd87a14f303b3a6";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-DoTfrXZx.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__="86bca8107c5d9e57b05a46242a839b6a888240ff8560cc296275e39dbc668668";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-tbclbSeu.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
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="c3f3b28b3149752a2b63584dbe161b2a247db77770fd5bd84bd87a14f303b3a6";try{await import("./houseplan-assets/houseplan-card-DoTfrXZx.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__="86bca8107c5d9e57b05a46242a839b6a888240ff8560cc296275e39dbc668668";try{await import("./houseplan-assets/houseplan-card-tbclbSeu.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 keeps attachment consent authoritative when preview
responses arrive late, and edited retries receive a fresh idempotency key so
the updated report reaches support
([#418](https://github.com/Matysh/houseplan-card/issues/418)).
- Dangerous-action confirmation no longer hangs if the active space disappears
or a request arrives while a lazy language is switching; House Plan now
refuses safely when no confirmation dialog can be rendered
+4
View File
@@ -8,6 +8,10 @@
## Не выпущено
- **«Помощь и обратная связь»** теперь учитывает отказ от вложения даже при
позднем ответе превью, а изменённая повторная отправка получает новый ключ
идемпотентности, чтобы исправленный репорт дошёл до поддержки
([#418](https://github.com/Matysh/houseplan-card/issues/418)).
- Подтверждение опасного действия больше не зависает, если активное пространство
исчезло или запрос пришёл во время переключения лениво загружаемого языка:
когда показать диалог невозможно, House Plan безопасно отвечает отказом
+27
View File
@@ -4433,6 +4433,33 @@ const MUTANT_DEFINITIONS = [
replace: ' this._devicePositionHistory.clear();',
}],
},
{
id: 'support-stale-preview-response-revives-consent',
guard: 'node demo/smoke_support_feedback.mjs',
because: 'a late preview response must not restore exact plan geometry after attachment '
+ 'consent was revoked or a newer preview generation won (#418)',
patches: [{
file: 'src/houseplan-editor-runtime.ts',
find: " return generation === this._supportPreviewGeneration\n"
+ ' && current?.draftId === draftId\n'
+ ' && current.attach;',
replace: ' return true;',
}],
},
{
id: 'support-edited-retry-reuses-old-idempotency-key',
guard: 'node demo/smoke_support_feedback.mjs',
because: 'a retry with changed outbound message, contact or preview must receive a new '
+ 'idempotency key instead of resolving to the first payload at the relay (#418)',
patches: [{
file: 'src/support-feedback.ts',
find: ' idempotencyKey: state.submissionFingerprint\n'
+ ' && state.submissionFingerprint !== fingerprint\n'
+ " ? randomId('report')\n"
+ ' : state.idempotencyKey,',
replace: ' idempotencyKey: state.idempotencyKey,',
}],
},
{
id: 'support-timeout-claims-success',
guard: 'node demo/smoke_support_feedback.mjs',
+30 -6
View File
@@ -252,6 +252,7 @@ import {
supportErrorCode,
supportRuntimeFacts,
supportSizeKiB,
supportSubmissionIdentity,
type SupportDialogState,
type SupportPreview,
} from './support-feedback';
@@ -1189,6 +1190,7 @@ export class HouseplanEditorRuntime {
spaceId: string; fingerprint: string; violations: JunctionLimitViolation[];
}>();
private _supportExpiryTimer?: number;
private _supportPreviewGeneration = 0;
public constructor(public readonly host: HouseplanEditorHostPort) {
host._editorSecondary = new EditorSecondaryController({
@@ -9078,6 +9080,7 @@ public _openSupportDialog = (): void => {
if (!this.host._norm || !this.host._canEdit) return;
clearTimeout(this._supportExpiryTimer);
this._supportExpiryTimer = undefined;
this._supportPreviewGeneration += 1;
this.host._supportDialog = newSupportDialogState();
};
@@ -9190,10 +9193,18 @@ private _supportFacts(): ReturnType<typeof supportRuntimeFacts> {
});
}
private _supportPreviewRequestIsCurrent(draftId: string, generation: number): boolean {
const current = this.host._supportDialog;
return generation === this._supportPreviewGeneration
&& current?.draftId === draftId
&& current.attach;
}
private async _buildSupportPreview(draftId: string): Promise<void> {
const current = this.host._supportDialog;
if (!current || current.draftId !== draftId || !current.attach
|| this.host._haIntegrationVersion !== CARD_VERSION) return;
const generation = ++this._supportPreviewGeneration;
this._supportPatch(draftId, { status: 'building', errorCode: '' });
try {
const response: unknown = await this.host.hass.callWS({
@@ -9234,6 +9245,10 @@ private async _buildSupportPreview(draftId: string): Promise<void> {
text,
preparedAt: now,
};
if (!this._supportPreviewRequestIsCurrent(draftId, generation)) {
void this._discardSupportPreview(token);
return;
}
if (!this._supportPatch(draftId, {
status: 'ready',
preview,
@@ -9245,6 +9260,7 @@ private async _buildSupportPreview(draftId: string): Promise<void> {
}
this._scheduleSupportExpiry(draftId, preview);
} catch (error: unknown) {
if (!this._supportPreviewRequestIsCurrent(draftId, generation)) return;
if (!this._supportPatch(draftId, {
status: 'error',
errorCode: supportErrorCode(error),
@@ -9258,6 +9274,7 @@ public async _setSupportAttachment(attach: boolean): Promise<void> {
if (!current || current.status === 'sending' || current.status === 'success') return;
const token = current.preview?.token || '';
if (!attach) {
this._supportPreviewGeneration += 1;
clearTimeout(this._supportExpiryTimer);
this._supportExpiryTimer = undefined;
this._supportPatch(current.draftId, {
@@ -9317,14 +9334,21 @@ public async _submitSupport(): Promise<void> {
return;
}
if (!supportCanSubmit(current)) return;
this._supportPatch(current.draftId, { status: 'sending', errorCode: '' });
const submission = supportSubmissionIdentity(current);
const sending = this._supportPatch(current.draftId, {
status: 'sending',
errorCode: '',
idempotencyKey: submission.idempotencyKey,
submissionFingerprint: submission.fingerprint,
});
if (!sending) return;
try {
const response: unknown = await this.host.hass.callWS({
type: 'houseplan/support/submit',
message: current.message.trim(),
contact: current.contact.trim(),
...(current.attach && current.preview ? { preview_token: current.preview.token } : {}),
idempotency_key: current.idempotencyKey,
message: sending.message.trim(),
contact: sending.contact.trim(),
...(sending.attach && sending.preview ? { preview_token: sending.preview.token } : {}),
idempotency_key: sending.idempotencyKey,
});
const reportId = response && typeof response === 'object' && 'report_id' in response
? String((response as { report_id?: unknown }).report_id || '') : '';
@@ -9433,7 +9457,7 @@ public _renderSupportDialog(): TemplateResult {
${state.status === 'building' ? html`<div class="supportstatus" role="status" aria-live="polite">
<ha-icon icon="mdi:progress-clock"></ha-icon>${this.host._t('support.building')}
</div>` : nothing}
${state.preview ? html`
${state.attach && state.preview ? html`
<div class="supportpreview">
<div class="supportsummary">${this.host._t('support.preview_summary', {
version: state.preview.version,
+23
View File
@@ -26,6 +26,7 @@ export type SupportDialogStatus =
export interface SupportDialogState {
draftId: string;
idempotencyKey: string;
submissionFingerprint: string;
contact: string;
message: string;
attach: boolean;
@@ -52,10 +53,32 @@ function randomId(prefix: string): string {
return `${prefix}-${id}`;
}
export function supportSubmissionFingerprint(state: SupportDialogState): string {
return JSON.stringify({
message: state.message.trim(),
contact: state.contact.trim(),
previewToken: state.attach && state.preview ? state.preview.token : '',
});
}
export function supportSubmissionIdentity(
state: SupportDialogState,
): { idempotencyKey: string; fingerprint: string } {
const fingerprint = supportSubmissionFingerprint(state);
return {
idempotencyKey: state.submissionFingerprint
&& state.submissionFingerprint !== fingerprint
? randomId('report')
: state.idempotencyKey,
fingerprint,
};
}
export function newSupportDialogState(): SupportDialogState {
return {
draftId: randomId('draft'),
idempotencyKey: randomId('report'),
submissionFingerprint: '',
contact: '',
message: '',
attach: false,
+43
View File
@@ -8,6 +8,8 @@ import {
supportDraftError,
supportErrorCode,
supportRuntimeFacts,
supportSubmissionFingerprint,
supportSubmissionIdentity,
} from '../test-build/support-feedback.js';
test('a fresh dialog never opts into exact plan geometry', () => {
@@ -39,6 +41,47 @@ test('attachment submit requires the exact non-expired preview', () => {
assert.equal(supportDraftError({ ...base, preview }, now + 1), 'preview_expired');
});
test('submission identity follows the effective trimmed payload', () => {
const initial = {
...newSupportDialogState(),
message: ' first message ',
contact: ' user@example.test ',
};
const first = supportSubmissionIdentity(initial);
assert.equal(first.idempotencyKey, initial.idempotencyKey);
assert.equal(
first.fingerprint,
supportSubmissionFingerprint({ ...initial, message: 'first message', contact: 'user@example.test' }),
);
const attempted = { ...initial, submissionFingerprint: first.fingerprint };
assert.equal(
supportSubmissionIdentity({ ...attempted, message: 'first message ' }).idempotencyKey,
initial.idempotencyKey,
);
assert.notEqual(
supportSubmissionIdentity({ ...attempted, message: 'different message' }).idempotencyKey,
initial.idempotencyKey,
);
assert.notEqual(
supportSubmissionIdentity({ ...attempted, contact: 'other@example.test' }).idempotencyKey,
initial.idempotencyKey,
);
const preview = {
token: 'a'.repeat(48), expiresAt: Date.now() + 1_000, size: 2, sha256: 'b'.repeat(64),
spaces: 1, format: 'houseplan-support-package', version: 1, text: '{}\n', preparedAt: Date.now(),
};
assert.notEqual(
supportSubmissionIdentity({ ...attempted, attach: true, preview }).idempotencyKey,
initial.idempotencyKey,
);
assert.equal(
supportSubmissionFingerprint({ ...attempted, attach: false, preview }),
first.fingerprint,
);
});
test('runtime facts are bounded enums without a raw user agent', () => {
const facts = supportRuntimeFacts({
userAgent: 'Mozilla/5.0 Chrome/140.0.0.0 Safari/537.36 private-suffix',