From d5da256341afe3b1d22e91810683bd74dcab15a7 Mon Sep 17 00:00:00 2001 From: Home Assistant Version Control Date: Thu, 6 Aug 2026 15:24:19 +0000 Subject: [PATCH] 93 files --- .../maintenance_supporter/const.py | 6 + .../__tests__/complete-dialog.test.ts | 71 + .../__tests__/panel-deeplink.test.ts | 15 +- .../components/battery-fleet-section.ts | 30 +- .../components/complete-dialog.ts | 60 + .../frontend-src/esbuild.mjs | 15 +- .../frontend-src/helpers/storage-keys.ts | 1 + .../frontend-src/locales/cs.json | 4 +- .../frontend-src/locales/da.json | 4 +- .../frontend-src/locales/de.json | 4 +- .../frontend-src/locales/en.json | 4 +- .../frontend-src/locales/es.json | 4 +- .../frontend-src/locales/fi.json | 4 +- .../frontend-src/locales/fr.json | 4 +- .../frontend-src/locales/hi.json | 4 +- .../frontend-src/locales/hu.json | 4 +- .../frontend-src/locales/it.json | 4 +- .../frontend-src/locales/ja.json | 4 +- .../frontend-src/locales/ko.json | 4 +- .../frontend-src/locales/nb.json | 4 +- .../frontend-src/locales/nl.json | 4 +- .../frontend-src/locales/pl.json | 4 +- .../frontend-src/locales/pt-br.json | 4 +- .../frontend-src/locales/pt.json | 4 +- .../frontend-src/locales/ru.json | 4 +- .../frontend-src/locales/sv.json | 4 +- .../frontend-src/locales/tr.json | 4 +- .../frontend-src/locales/uk.json | 4 +- .../frontend-src/locales/zh.json | 4 +- .../frontend-src/maintenance-card.ts | 17 +- .../frontend-src/maintenance-panel.ts | 156 +- .../frontend-src/types.ts | 4 + .../frontend/__init__.py | 12 + .../frontend/locales/cs.json | 4 +- .../frontend/locales/da.json | 4 +- .../frontend/locales/de.json | 4 +- .../frontend/locales/en.json | 4 +- .../frontend/locales/es.json | 4 +- .../frontend/locales/fi.json | 4 +- .../frontend/locales/fr.json | 4 +- .../frontend/locales/hi.json | 4 +- .../frontend/locales/hu.json | 4 +- .../frontend/locales/it.json | 4 +- .../frontend/locales/ja.json | 4 +- .../frontend/locales/ko.json | 4 +- .../frontend/locales/nb.json | 4 +- .../frontend/locales/nl.json | 4 +- .../frontend/locales/pl.json | 4 +- .../frontend/locales/pt-br.json | 4 +- .../frontend/locales/pt.json | 4 +- .../frontend/locales/ru.json | 4 +- .../frontend/locales/sv.json | 4 +- .../frontend/locales/tr.json | 4 +- .../frontend/locales/uk.json | 4 +- .../frontend/locales/zh.json | 4 +- .../frontend/maintenance-calendar-card.js | 4 +- .../frontend/maintenance-card.js | 174 +- .../frontend/maintenance-panel.js | 6527 +++-------------- .../frontend/maintenance-strategy-shim.js | 4 +- .../adopt-problem-sensors-dialog-EEQGF7EF.js | 222 + .../frontend/panel-chunks/chunk-3FYWLAW5.js | 2 + .../frontend/panel-chunks/chunk-4AV2K4W7.js | 2 + .../chunk-4MVSRJ3Y.js} | 16 +- .../frontend/panel-chunks/chunk-6CM3ZIHM.js | 54 + .../frontend/panel-chunks/chunk-JBZVB6GP.js | 2 + .../frontend/panel-chunks/chunk-MEKM6THN.js | 2 + .../frontend/panel-chunks/chunk-URNT5464.js | 2 + .../panel-chunks/complete-dialog-KGRPNNX2.js | 292 + .../panel-chunks/object-dialog-IU7V7TDN.js | 143 + .../panel-chunks/qr-dialog-U2M7KFS3.js | 213 + .../panel-chunks/settings-view-PUJN7ASF.js | 1141 +++ .../suggested-setups-dialog-VXF7GBII.js | 146 + .../panel-chunks/task-dialog-5VUS7JEH.js | 983 +++ ...EV3.js => budget-section-card-HV65MOOG.js} | 4 +- .../chunks/budget-section-card-OBF3DYBH.js | 125 - .../{chunk-D4IFN5R3.js => chunk-5HT73ALV.js} | 2 +- .../strategy/chunks/chunk-7IBGRLM5.js | 60 - .../{chunk-V27MBWM3.js => chunk-CRNPDKEQ.js} | 4 +- .../{chunk-C6VY6OOC.js => chunk-HC6XJZMT.js} | 4 +- .../strategy/chunks/chunk-JSL2OXKU.js | 2 - .../strategy/chunks/dialog-mount-JGWDXBYE.js | 2435 ------ ...t-G4OSJIHX.js => dialog-mount-ZDXPVJWH.js} | 460 +- .../chunks/groups-section-card-BPWR5VFS.js | 137 - ...4JJ.js => groups-section-card-XIGNRWQ6.js} | 4 +- ...C.js => vacation-section-card-CRAL2J5R.js} | 4 +- .../chunks/vacation-section-card-CTBYKBDH.js | 122 - .../maintenance-dashboard-strategy.js | 6 +- .../helpers/battery_fleet.py | 23 +- .../maintenance_supporter/manifest.json | 2 +- .../websocket/__init__.py | 42 +- .../websocket/dashboard.py | 7 +- .../websocket/objects.py | 14 +- zigbee2mqtt/state.json | 40 +- 93 files changed, 5199 insertions(+), 8790 deletions(-) create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/adopt-problem-sensors-dialog-EEQGF7EF.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-3FYWLAW5.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-4AV2K4W7.js rename custom_components/maintenance_supporter/frontend/{strategy/chunks/chunk-DM35MPY4.js => panel-chunks/chunk-4MVSRJ3Y.js} (69%) create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-6CM3ZIHM.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-JBZVB6GP.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-MEKM6THN.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-URNT5464.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/complete-dialog-KGRPNNX2.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/object-dialog-IU7V7TDN.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/qr-dialog-U2M7KFS3.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/settings-view-PUJN7ASF.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/suggested-setups-dialog-VXF7GBII.js create mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/task-dialog-5VUS7JEH.js rename custom_components/maintenance_supporter/frontend/strategy/chunks/{budget-section-card-HV3ADEV3.js => budget-section-card-HV65MOOG.js} (97%) delete mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/budget-section-card-OBF3DYBH.js rename custom_components/maintenance_supporter/frontend/strategy/chunks/{chunk-D4IFN5R3.js => chunk-5HT73ALV.js} (94%) delete mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-7IBGRLM5.js rename custom_components/maintenance_supporter/frontend/strategy/chunks/{chunk-V27MBWM3.js => chunk-CRNPDKEQ.js} (95%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{chunk-C6VY6OOC.js => chunk-HC6XJZMT.js} (95%) delete mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-JSL2OXKU.js delete mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/dialog-mount-JGWDXBYE.js rename custom_components/maintenance_supporter/frontend/strategy/chunks/{dialog-mount-G4OSJIHX.js => dialog-mount-ZDXPVJWH.js} (84%) delete mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/groups-section-card-BPWR5VFS.js rename custom_components/maintenance_supporter/frontend/strategy/chunks/{groups-section-card-5Q5NH4JJ.js => groups-section-card-XIGNRWQ6.js} (97%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{vacation-section-card-BRJM72JC.js => vacation-section-card-CRAL2J5R.js} (97%) delete mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/vacation-section-card-CTBYKBDH.js diff --git a/custom_components/maintenance_supporter/const.py b/custom_components/maintenance_supporter/const.py index ee0e52b..a9f8cd2 100644 --- a/custom_components/maintenance_supporter/const.py +++ b/custom_components/maintenance_supporter/const.py @@ -287,6 +287,12 @@ STRATEGY_CHUNKS_URL = f"{STRATEGY_DIR_URL}/chunks" # STRATEGY_DIR_URL. The shim has no relative imports, so a flat URL is fine. STRATEGY_SHIM_URL = "/maintenance_supporter_strategy_shim.js" CALENDAR_CARD_URL = "/maintenance_supporter_calendar_card" +# Panel code-split chunks (perf wave 2, item 5). The panel entry is served +# from a versioned FILE url (PANEL_URL + content hash, see panel.py), which +# gives relative imports nothing to resolve against — so esbuild rewrites +# its chunk imports to ABSOLUTE urls under this prefix (publicPath in +# esbuild.mjs; the two values must stay in sync). +PANEL_CHUNKS_URL = "/maintenance_supporter_panelfiles/panel-chunks" # Runtime-loaded UI translations (frontend/locales/.json), served as a # directory so the panel/card fetch the active language on demand. Mirrors # LOCALES_BASE in frontend-src/styles.ts: only EN is bundled into the JS (as the diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/complete-dialog.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/complete-dialog.test.ts index 9048e23..bbebc6b 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/complete-dialog.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/complete-dialog.test.ts @@ -167,3 +167,74 @@ describe("complete-dialog", () => { expect(completedEvent).to.be.false; }); }); + +describe("complete-dialog cost suggestion from parts (#104 follow-up)", () => { + async function mountWithParts(over: Partial = {}) { + const { hass } = createMockHass({}); + const el = await fixture(html` + + `); + Object.assign(el, over); + el.open(); + await el.updateComplete; + return el; + } + + const chip = (el: MaintenanceCompleteDialog) => + el.shadowRoot!.querySelector(".cost-suggestion"); + + it("sums selected consumed parts (qty x unit cost) and fills on click", async () => { + const el = await mountWithParts({ + parts: [ + { id: "p1", name: "Filter", cost: 12.5 }, + { id: "p2", name: "O-Ring", cost: 2.25 }, + { id: "p3", name: "Unpriced", cost: null }, + ] as never, + consumesParts: [ + { part_id: "p1", quantity: 1 }, + { part_id: "p2", quantity: 2 }, + { part_id: "p3", quantity: 1 }, + ] as never, + currencySymbol: "€", + }); + const c = chip(el)!; + expect(c, "suggestion chip rendered").to.exist; + expect(c.textContent).to.include("17.00"); + expect(c.textContent).to.include("€"); + c.click(); + await el.updateComplete; + const cost = [...el.shadowRoot!.querySelectorAll(".field-input")][1]; + expect(cost.value).to.equal("17.00"); + expect(chip(el), "chip hides once cost is set").to.equal(null); + }); + + it("buy task: restock qty x unit cost, follows the qty field", async () => { + const el = await mountWithParts({ restockDefault: 2, restockUnitCost: 4.5 }); + expect(chip(el)!.textContent).to.include("9.00"); + }); + + it("no chip when no involved part carries a price", async () => { + const el = await mountWithParts({ + parts: [{ id: "p1", name: "Filter", cost: null }] as never, + consumesParts: [{ part_id: "p1", quantity: 1 }] as never, + }); + expect(chip(el)).to.equal(null); + }); + + it("no chip once the user typed a cost themselves", async () => { + const el = await mountWithParts({ + parts: [{ id: "p1", name: "Filter", cost: 5 }] as never, + consumesParts: [{ part_id: "p1", quantity: 1 }] as never, + }); + expect(chip(el)).to.exist; + setInput(el, 1, "3.10"); + await el.updateComplete; + expect(chip(el)).to.equal(null); + }); +}); diff --git a/custom_components/maintenance_supporter/frontend-src/__tests__/panel-deeplink.test.ts b/custom_components/maintenance_supporter/frontend-src/__tests__/panel-deeplink.test.ts index 4554e69..61595b2 100644 --- a/custom_components/maintenance_supporter/frontend-src/__tests__/panel-deeplink.test.ts +++ b/custom_components/maintenance_supporter/frontend-src/__tests__/panel-deeplink.test.ts @@ -20,7 +20,9 @@ function setDeepLink(query: string) { } async function settleRaf(el: { updateComplete: Promise }) { - // Deep-link dialog opens behind a requestAnimationFrame. + // Deep-link dialog opens behind a requestAnimationFrame — and since the + // dialogs became lazy code-split chunks, behind their dynamic import too. + await customElements.whenDefined("maintenance-complete-dialog"); await new Promise((r) => requestAnimationFrame(() => r(null))); await new Promise((r) => setTimeout(r, 20)); await (el as HTMLElement & { updateComplete: Promise }).updateComplete; @@ -30,6 +32,15 @@ function completeDialog(el: HTMLElement): MaintenanceCompleteDialog | null { return sr(el).querySelector("maintenance-complete-dialog"); } +/** The dialogs are lazy code-split chunks — the open lands whenever the + * whole lazy-UI group has loaded, so poll instead of guessing a delay. */ +async function waitForOpenCompleteDialog(el: HTMLElement): Promise { + for (let i = 0; i < 100; i++) { + if (completeDialog(el)?.shadowRoot?.querySelector("ha-dialog")) return; + await new Promise((r) => setTimeout(r, 20)); + } +} + describe("panel deep links (QR scan routing)", () => { beforeEach(() => { resetTaskSeq(); @@ -52,6 +63,7 @@ describe("panel deep links (QR scan routing)", () => { expect(sr(el).querySelector(".task-header"), "task detail rendered").to.exist; expect(sr(el).querySelector(".task-name-breadcrumb")!.textContent).to.include("Scan Me"); // …with the complete dialog open and targeted at the scanned task. + await waitForOpenCompleteDialog(el); const dlg = completeDialog(el)!; expect(dlg.shadowRoot!.querySelector("ha-dialog"), "complete dialog open").to.exist; expect(dlg.entryId).to.equal("e1"); @@ -94,6 +106,7 @@ describe("panel deep links (QR scan routing)", () => { expect( sent.filter((m) => m.type === "maintenance_supporter/task/quick_complete").length, ).to.equal(1); + await waitForOpenCompleteDialog(el); const dlg = completeDialog(el)!; expect(dlg.shadowRoot!.querySelector("ha-dialog"), "fallback dialog open").to.exist; expect(dlg.taskName).to.equal("No Defaults"); diff --git a/custom_components/maintenance_supporter/frontend-src/components/battery-fleet-section.ts b/custom_components/maintenance_supporter/frontend-src/components/battery-fleet-section.ts index 88162cc..113a714 100644 --- a/custom_components/maintenance_supporter/frontend-src/components/battery-fleet-section.ts +++ b/custom_components/maintenance_supporter/frontend-src/components/battery-fleet-section.ts @@ -29,6 +29,10 @@ interface BatteryRow { * the fleet floor, whichever is higher) — the level bar colors against * it, not against a fixed 20 %. */ low_threshold?: number; + /** B1: the predicted date has passed while the battery still reports + * healthy. Never escalates to low/task — the row just shows the + * discrepancy (usual cause: an unrecorded swap). */ + forecast_overdue?: boolean; } interface RosterRow extends BatteryRow { status: "low" | "soon" | "ok"; @@ -466,13 +470,15 @@ export class MaintenanceBatteryFleetSection extends LitElement { })()} ${b.days_until != null ? html`~${this._predictedDate(b.days_until)}${b.forecast_overdue ? html`` : nothing}~${this._predictedDate(b.days_until)}` : nothing} `; + } + private _close(): void { this._open = false; } @@ -317,6 +363,7 @@ export class MaintenanceCompleteDialog extends LitElement { (this._cost = (e.target as HTMLInputElement).value)} /> + ${this._renderCostSuggestion(L)}