Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Bottein a3a43d0d1e Show the event type in the logbook for event entities 2026-06-25 15:00:35 +02:00
23 changed files with 228 additions and 991 deletions
-1
View File
@@ -240,7 +240,6 @@ gulp.task("rspack-dev-server-e2e-test-app", () =>
),
contentBase: paths.e2eTestApp_output_root,
port: 8095,
open: false,
})
);
+5 -6
View File
@@ -28,7 +28,6 @@
"test:e2e:show-report": "yarn playwright show-report test/e2e/reports/combined",
"test:e2e:demo": "playwright test --config test/e2e/playwright.demo.config.ts",
"test:e2e:app": "playwright test --config test/e2e/playwright.app.config.ts",
"test:e2e:app:dev": "test/e2e/app/script/develop_app",
"test:e2e:gallery": "playwright test --config test/e2e/playwright.gallery.config.ts"
},
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
@@ -82,7 +81,6 @@
"@tsparticles/engine": "4.2.1",
"@tsparticles/preset-links": "4.2.1",
"@vibrant/color": "4.0.4",
"@vvo/tzdb": "6.198.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"barcode-detector": "3.2.0",
@@ -99,6 +97,7 @@
"echarts": "6.1.0",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.4.2",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.16",
"home-assistant-js-websocket": "9.6.0",
@@ -173,7 +172,7 @@
"eslint": "10.5.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.11",
"eslint-plugin-import-x": "4.17.0",
"eslint-plugin-import-x": "4.16.2",
"eslint-plugin-lit": "2.3.1",
"eslint-plugin-lit-a11y": "5.1.1",
"eslint-plugin-unused-imports": "4.4.1",
@@ -182,7 +181,7 @@
"fs-extra": "11.3.5",
"generate-license-file": "4.2.1",
"glob": "13.0.6",
"globals": "17.7.0",
"globals": "17.6.0",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0",
@@ -207,7 +206,7 @@
"terser-webpack-plugin": "5.6.1",
"ts-lit-plugin": "2.0.2",
"typescript": "6.0.3",
"typescript-eslint": "8.62.0",
"typescript-eslint": "8.61.1",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.9",
"webpack-stats-plugin": "1.1.3",
@@ -220,7 +219,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.2",
"@fullcalendar/daygrid": "6.1.21",
"globals": "17.7.0",
"globals": "17.6.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"glob@^10.2.2": "^10.5.0"
+2 -2
View File
@@ -1,4 +1,4 @@
import { timeZonesNames } from "@vvo/tzdb";
import timezones from "google-timezones-json";
import { TimeZone } from "../../data/translation";
const RESOLVED_RAW = Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
@@ -10,7 +10,7 @@ const RESOLVED_TIME_ZONE =
RESOLVED_RAW &&
(RESOLVED_RAW === "UTC" ||
RESOLVED_RAW === "Etc/UTC" ||
timeZonesNames.includes(RESOLVED_RAW))
RESOLVED_RAW in timezones)
? RESOLVED_RAW
: undefined;
+23 -25
View File
@@ -1,4 +1,4 @@
import { getTimeZones, timeZonesNames } from "@vvo/tzdb";
import timezones from "google-timezones-json";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -13,40 +13,38 @@ const SEARCH_KEYS = [
{ name: "secondary", weight: 8 },
];
// @vvo/tzdb is missing the bare "UTC" zone, even though it is a valid IANA
// identifier and a common server default. Add UTC back so a
// "UTC" configuration can be selected.
// google-timezones-json is missing the bare "UTC" and "Etc/UTC" zones, even
// though both are valid IANA identifiers and common server defaults. Without
// them a "UTC" configuration shows up as an unknown time zone. Add them back.
const ADDITIONAL_TIMEZONES: PickerComboBoxItem[] = [
{ id: "UTC", primary: "+00:00 UTC", secondary: "UTC" },
{ id: "UTC", primary: "(GMT+00:00) UTC", secondary: "UTC" },
{ id: "Etc/UTC", primary: "(GMT+00:00) UTC", secondary: "Etc/UTC" },
];
export const getTimezoneOptions = (): PickerComboBoxItem[] => {
const options: PickerComboBoxItem[] = Array.from(
new Map(
getTimeZones({ includeUtc: true })
.flatMap((timezone) => {
const groupArray = Array.isArray(timezone.group)
? timezone.group
: [timezone.group];
const filteredGroup = groupArray.filter((gName) =>
timeZonesNames.includes(gName)
);
// google-timezones-json also ships an invalid IANA identifier. Correct it so
// the zone can be selected (the backend rejects the invalid id).
const TIMEZONE_ID_CORRECTIONS: Record<string, string> = {
"Asia/Yuzhno-Sakhalinsk": "Asia/Sakhalin",
};
return [timezone.name, ...filteredGroup].map((nameString) => ({
id: nameString,
primary: timezone.rawFormat,
secondary: nameString,
}));
})
.map((item) => [item.id, item])
).values()
);
export const getTimezoneOptions = (): PickerComboBoxItem[] => {
const options: PickerComboBoxItem[] = Object.entries(
timezones as Record<string, string>
).map(([key, value]) => {
const id = TIMEZONE_ID_CORRECTIONS[key] ?? key;
return {
id,
primary: value,
secondary: id,
};
});
for (const timezone of ADDITIONAL_TIMEZONES) {
if (!options.some((option) => option.id === timezone.id)) {
options.push(timezone);
}
}
return options;
};
@@ -74,7 +74,7 @@ export interface MediaPlayerItemId {
media_content_type?: string | undefined;
}
const MANUAL_ITEM_BASE: Omit<MediaPlayerItem, "title"> = {
const MANUAL_ITEM: MediaPlayerItem = {
can_expand: true,
can_play: false,
can_search: false,
@@ -83,6 +83,7 @@ const MANUAL_ITEM_BASE: Omit<MediaPlayerItem, "title"> = {
media_content_id: MANUAL_MEDIA_SOURCE_PREFIX,
media_content_type: "",
iconPath: mdiKeyboard,
title: "Manual entry",
};
@customElement("ha-media-player-browse")
@@ -239,7 +240,7 @@ export class HaMediaPlayerBrowse extends LitElement {
currentId.media_content_id &&
isManualMediaSourceContentId(currentId.media_content_id)
) {
this._currentItem = this._manualItem();
this._currentItem = MANUAL_ITEM;
fireEvent(this, "media-browsed", {
ids: navigateIds,
current: this._currentItem,
@@ -800,21 +801,12 @@ export class HaMediaPlayerBrowse extends LitElement {
return prom.then((item) => {
if (!mediaContentId && this.action === "pick") {
item.children = item.children || [];
item.children.push(this._manualItem());
item.children.push(MANUAL_ITEM);
}
return item;
});
}
private _manualItem(): MediaPlayerItem {
return {
...MANUAL_ITEM_BASE,
title: this.hass.localize(
"ui.components.selectors.selector.types.manual"
),
};
}
private _measureCard(): void {
this.narrow = (this.dialog ? window.innerWidth : this.offsetWidth) < 450;
}
+16 -6
View File
@@ -27,6 +27,7 @@ export interface LogbookEntry {
source?: string; // The trigger source (English phrase, parsed for the cause)
domain?: string;
state?: string; // The state of the entity
attributes?: { event_type?: string }; // Selected attributes the backend surfaces
// Context data
context_id?: string;
context_user_id?: string;
@@ -241,13 +242,13 @@ export const parseTriggerSource = (source: string): ParsedTriggerSource => {
};
// Short label shown instead of the bare timestamp for each timestamp-state
// domain. Typed to TIMESTAMP_STATE_DOMAINS minus datetime (a real value), so a
// new timestamp domain won't compile until it gets a label here.
// domain. Typed to TIMESTAMP_STATE_DOMAINS minus datetime (a real value) and
// event (handled separately via its event type), so a new timestamp domain
// won't compile until it gets a label here.
type LogbookActionMessage =
| "pressed"
| "activated"
| "scanned"
| "detected_event_no_type"
| "updated"
| "sent"
| "detected"
@@ -258,14 +259,13 @@ type LogbookActionMessage =
| "command_sent";
const STATE_ACTION_MESSAGES: Record<
Exclude<TimestampStateDomain, "datetime">,
Exclude<TimestampStateDomain, "datetime" | "event">,
LogbookActionMessage
> = {
button: "pressed",
input_button: "pressed",
scene: "activated",
tag: "scanned",
event: "detected_event_no_type",
image: "updated",
notify: "sent",
wake_word: "detected",
@@ -281,8 +281,18 @@ export const localizeStateMessage = (
hass: HomeAssistant,
state: string,
stateObj: HassEntity,
domain: string
domain: string,
attributes?: LogbookEntry["attributes"]
): string => {
// Events show the triggered event type, falling back to a generic label when
// the type is unknown (the timestamp state is meaningless on its own).
if (domain === "event") {
const eventType = attributes?.event_type;
if (eventType != null) {
return hass.formatEntityAttributeValue(stateObj, "event_type", eventType);
}
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event_no_type`);
}
const actionKey: LogbookActionMessage | undefined =
STATE_ACTION_MESSAGES[domain as keyof typeof STATE_ACTION_MESSAGES];
if (actionKey) {
+1 -1
View File
@@ -159,7 +159,7 @@ class DialogEditSidebar extends DirtyStateProviderMixin<SidebarState>()(
value: panel.url_path,
label:
(getPanelTitle(this.hass, panel) || panel.url_path) +
`${defaultPanel === panel.url_path ? ` (${this.hass.localize("ui.sidebar.default")})` : ""}`,
`${defaultPanel === panel.url_path ? " (default)" : ""}`,
icon: getPanelIcon(panel),
iconPath: getPanelIconPath(panel),
disableHiding: panel.url_path === defaultPanel,
@@ -41,9 +41,7 @@ export class DialogSupportPackage extends LitElement {
<ha-dialog
.open=${this._open}
width="full"
.headerTitle=${this.hass.localize(
"ui.panel.config.cloud.account.download_support_package"
)}
header-title="Download support package"
@closed=${this._dialogClosed}
>
${this._supportPackage
@@ -54,16 +52,13 @@ export class DialogSupportPackage extends LitElement {
: html`
<div class="progress-container">
<ha-spinner></ha-spinner>
${this.hass.localize(
"ui.panel.config.cloud.account.support_package_generating_preview"
)}...
Generating preview...
</div>
`}
<div slot="footer" class="footer">
<ha-alert>
${this.hass.localize(
"ui.panel.config.cloud.account.support_package_privacy_warning"
)}
This file may contain personal data about your home. Avoid sharing
them with unverified or untrusted parties.
</ha-alert>
<hr />
<ha-dialog-footer>
@@ -72,10 +67,10 @@ export class DialogSupportPackage extends LitElement {
appearance="plain"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.close")}
Close
</ha-button>
<ha-button slot="primaryAction" @click=${this._download}>
${this.hass.localize("ui.common.download")}
Download
</ha-button>
</ha-dialog-footer>
</div>
+1 -4
View File
@@ -109,10 +109,7 @@ export class SystemLogCard extends LitElement {
`
: html`
<div class="header">
<h1 class="card-header">
${this.header ||
this.hass.localize("ui.panel.config.logs.caption")}
</h1>
<h1 class="card-header">${this.header || "Logs"}</h1>
<div class="header-buttons">
<ha-icon-button
.path=${mdiDownload}
+1 -3
View File
@@ -197,9 +197,7 @@ export class HaScriptTrace extends LitElement {
</div>
${this._traces === undefined
? html`<div class="container">
${this.hass.localize("ui.panel.config.script.trace.loading")}
</div>`
? html`<div class="container">Loading…</div>`
: this._traces.length === 0
? html`<div class="container">
${this.hass!.localize(
+7 -1
View File
@@ -266,7 +266,13 @@ const computeLogbookValue = (
if (item.entity_id && item.state) {
return {
text: stateObj
? localizeStateMessage(hass, item.state, stateObj, domain!)
? localizeStateMessage(
hass,
item.state,
stateObj,
domain!,
item.attributes
)
: item.state,
type: "state",
};
@@ -58,9 +58,7 @@ export class HuiErrorBadge extends LitElement implements LovelaceBadge {
class="error"
@click=${this._viewDetail}
type="button"
.label=${this.hass?.localize(
"ui.panel.lovelace.editor.error_section.title"
) ?? ""}
label="Error"
>
<ha-svg-icon slot="icon" .path=${mdiAlertCircle}></ha-svg-icon>
<div class="content">${this._config.error}</div>
@@ -135,11 +135,7 @@ class HuiHistoryChartCardFeature
if (this._coordinates && !this._coordinates.length) {
return html`
<div class="container">
<div class="info">
${this.hass!.localize(
"ui.components.history_charts.no_history_found"
)}
</div>
<div class="info">No state history found.</div>
</div>
`;
}
@@ -122,11 +122,7 @@ export class HuiGraphHeaderFooter
if (this._coordinates && !this._coordinates.length) {
return html`
<div class="container">
<div class="info">
${this.hass!.localize(
"ui.components.history_charts.no_history_found"
)}
</div>
<div class="info">No state history found.</div>
</div>
`;
}
@@ -47,9 +47,7 @@ export class HuiErrorSection
// Todo improve
return html`
<h1>
${this.hass!.localize("ui.panel.lovelace.editor.error_section.title")}
</h1>
<h1>Error</h1>
<p>${this._config.error}</p>
`;
}
@@ -331,28 +331,6 @@ export function computeBarycenter(
return totalWeight > 0 ? weightedSum / totalWeight : fallback;
}
// Index of the single highest-weight neighbor present in the reference
// section (ties on weight broken by the earliest edge). Used only as a
// barycenter tie-break so a node stays beside its dominant neighbor's group
// instead of falling back to a stale seed index. Falls back to the node's own
// index when it has no resolvable neighbor, matching computeBarycenter.
export function dominantNeighborIndex(
neighbors: WeightedNeighbor[],
referenceIdIndexMap: Map<string, number>,
fallback: number
): number {
let bestIdx = fallback;
let bestWeight = -Infinity;
neighbors.forEach(({ id, weight }) => {
const idx = referenceIdIndexMap.get(id);
if (idx !== undefined && weight > bestWeight) {
bestWeight = weight;
bestIdx = idx;
}
});
return bestIdx;
}
function buildIdIndexMap(section: Node[]): Map<string, number> {
const map = new Map<string, number>();
section.forEach((node, index) => map.set(node.id, index));
@@ -364,20 +342,12 @@ function sortSectionByBarycenter(
referenceMap: Map<string, number>,
getNeighbors: (node: Node) => WeightedNeighbor[]
): { sorted: Node[]; changed: boolean } {
const decorated = section.map((node, index) => {
const neighbors = getNeighbors(node);
return {
node,
index,
barycenter: computeBarycenter(neighbors, referenceMap, index),
// Tie-break that keeps a node next to its dominant neighbor's group.
anchor: dominantNeighborIndex(neighbors, referenceMap, index),
};
});
decorated.sort(
(a, b) =>
a.barycenter - b.barycenter || a.anchor - b.anchor || a.index - b.index
);
const decorated = section.map((node, index) => ({
node,
index,
barycenter: computeBarycenter(getNeighbors(node), referenceMap, index),
}));
decorated.sort((a, b) => a.barycenter - b.barycenter || a.index - b.index);
const sorted = decorated.map((d) => d.node);
const changed = sorted.some((n, idx) => n !== section[idx]);
return { sorted, changed };
@@ -482,55 +452,6 @@ function crossingsAdjacentTo(
return total;
}
function countAllCrossings(
sections: Node[][],
sectionMaps: Map<string, number>[],
depths: number[],
edges: GraphEdge[]
): number {
let total = 0;
for (let i = 0; i < sections.length - 1; i++) {
total += countCrossings(
getEdgeSegmentsBetween(
depths[i],
depths[i + 1],
depths,
edges,
sectionMaps[i],
sectionMaps[i + 1]
)
);
}
return total;
}
// A section is "multi-parent" when at least one of its real nodes draws from
// two or more distinct parents in the previous section. In the energy/power/
// water cards only the source/home layers do this (grid/solar/battery feed
// home + battery_in + grid_return); every later section (floors, areas,
// devices) is a pure single-parent tree level. Pass-throughs are always
// single-parent (one source/target chain) and never make a section multi-parent.
function sectionHasMultipleParents(
section: Node[],
prevDepth: number,
depths: number[]
): boolean {
return section.some((node) => {
if (isPassThroughNode(node)) {
return false;
}
const parentIds = new Set(
getNeighborIds(node, "source", prevDepth, depths).map((n) => n.id)
);
return parentIds.size > 1;
});
}
// The head barycenter sweep (STEP 1) only touches the multi-parent head
// sections (the single-parent tree below is placed deterministically in
// STEP 2), so a single forward+backward pass converges in practice and the loop
// early-exits on the first no-change sweep. The cap is just headroom for unusual
// topologies with several interacting head sections.
const MAX_SORT_ITERATIONS = 4;
export function sortNodesInSections(
@@ -539,30 +460,13 @@ export function sortNodesInSections(
edges: GraphEdge[]
): Record<number, Node[]> {
const sections: Node[][] = depths.map((d) => [...(nodesPerSection[d] || [])]);
// Id→index lookup per section, kept in sync with sections.
// Id→index lookup per section, kept in sync with sections. Rebuilt only when
// a section's order actually changes (inside tryReplace).
const sectionMaps: Map<string, number>[] = sections.map(buildIdIndexMap);
// Classify each section past the root. Multi-parent sections (the
// intentionally-ordered source/home layers) are minimized by barycenter in
// PASS 2; the rest are single-parent tree levels placed deterministically in
// PASS 1. Classification reads only the graph, so it is stable across passes.
const multiParent = depths.map(
(_d, i) =>
i >= 1 && sectionHasMultipleParents(sections[i], depths[i - 1], depths)
);
// Best (fewest-crossing) head ordering seen so far, seeded from the original
// input so the head is provably never worse than the seed.
const snapshot = (): Node[][] => sections.map((s) => s.slice());
let liveCrossings = countAllCrossings(sections, sectionMaps, depths, edges);
let bestCrossings = liveCrossings;
let bestSections = snapshot();
// Replace a multi-parent section with a candidate ordering when crossings on
// its adjacent boundaries do not increase. Accepting equal-crossing
// ("plateau") moves lets the sweep escape local optima; the best snapshot
// (captured only on a strict global decrease) is what seeds the head order,
// so the result is deterministic and never worse than the seed.
// Replace a section with a candidate ordering only when crossings strictly
// drop on either side. This keeps user-intended ordering intact when
// barycenter would shuffle nodes without improving the layout.
const tryReplace = (i: number, candidate: Node[]): boolean => {
const before = crossingsAdjacentTo(i, sections, sectionMaps, depths, edges);
const sectionSnapshot = sections[i];
@@ -570,12 +474,7 @@ export function sortNodesInSections(
sections[i] = candidate;
sectionMaps[i] = buildIdIndexMap(candidate);
const after = crossingsAdjacentTo(i, sections, sectionMaps, depths, edges);
if (after <= before) {
liveCrossings += after - before;
if (liveCrossings < bestCrossings) {
bestCrossings = liveCrossings;
bestSections = snapshot();
}
if (after < before) {
return true;
}
sections[i] = sectionSnapshot;
@@ -583,97 +482,39 @@ export function sortNodesInSections(
return false;
};
// STEP 1 — settle the multi-parent head sections (sources → home/battery_in/
// grid_return) by barycenter. These layers have nodes with several parents and
// so no single parent position to inherit; we minimize their crossings by the
// weighted average of neighbour positions, iterating forward then backward
// until the order is stable. The backward sweep is restricted to multi-parent
// neighbours, so the head order never depends on its single-parent children —
// that is what keeps the whole result idempotent, because STEP 2 re-derives
// those children purely from the settled head. The root section (index 0) is
// never reordered.
if (multiParent.some(Boolean)) {
for (let iter = 0; iter < MAX_SORT_ITERATIONS; iter++) {
let changed = false;
for (let iter = 0; iter < MAX_SORT_ITERATIONS; iter++) {
let changed = false;
for (let i = 1; i < sections.length; i++) {
if (!multiParent[i]) {
continue;
}
const prevDepth = depths[i - 1];
const result = sortSectionByBarycenter(
sections[i],
sectionMaps[i - 1],
(node) => getNeighborIds(node, "source", prevDepth, depths)
);
if (result.changed && tryReplace(i, result.sorted)) {
changed = true;
}
for (let i = 1; i < sections.length; i++) {
const prevDepth = depths[i - 1];
const result = sortSectionByBarycenter(
sections[i],
sectionMaps[i - 1],
(node) => getNeighborIds(node, "source", prevDepth, depths)
);
if (result.changed && tryReplace(i, result.sorted)) {
changed = true;
}
}
for (let i = sections.length - 2; i >= 1; i--) {
if (!multiParent[i] || !multiParent[i + 1]) {
continue;
}
const nextDepth = depths[i + 1];
const result = sortSectionByBarycenter(
sections[i],
sectionMaps[i + 1],
(node) => getNeighborIds(node, "target", nextDepth, depths)
);
if (result.changed && tryReplace(i, result.sorted)) {
changed = true;
}
for (let i = sections.length - 2; i >= 0; i--) {
const nextDepth = depths[i + 1];
const result = sortSectionByBarycenter(
sections[i],
sectionMaps[i + 1],
(node) => getNeighborIds(node, "target", nextDepth, depths)
);
if (result.changed && tryReplace(i, result.sorted)) {
changed = true;
}
if (!changed || bestCrossings === 0) break;
}
}
// STEP 2 — deterministic hierarchy placement. Starting from the best head
// ordering, walk left to right and order every single-parent section by the
// position of each node's single parent in the already-final previous section
// (sortSectionByBarycenter with one parent reduces to a stable sort by that
// parent's index). This is the classic layered-tree drawing: each parent's
// children stay contiguous, every single-parent boundary is crossing-free,
// pass-throughs travel along their chain, and the user-configured floor/area
// order is preserved because same-parent siblings keep their seed index.
// It runs unconditionally — grouping a parent's children under it must win
// even on a crossing-neutral plateau (the #52852 fix) — and because it is a
// pure function of the settled head, re-running yields the identical layout.
const finalSections = bestSections.map((s) => s.slice());
const finalMaps = finalSections.map(buildIdIndexMap);
for (let i = 1; i < finalSections.length; i++) {
if (multiParent[i]) {
continue;
}
const prevDepth = depths[i - 1];
const { sorted } = sortSectionByBarycenter(
finalSections[i],
finalMaps[i - 1],
(node) => getNeighborIds(node, "source", prevDepth, depths)
);
finalSections[i] = sorted;
finalMaps[i] = buildIdIndexMap(sorted);
if (!changed) break;
}
// Hierarchy placement makes every single-parent boundary crossing-free, so on
// the energy/water cards (multi-parent only at the head) it can only lower the
// total. Guard the general graph: if regrouping somehow raised crossings (only
// possible for a multi-parent section sitting *below* single-parent ones,
// which the cards never produce), fall back to the gated best head so the
// never-worse-than-seed guarantee always holds. Ties keep the grouped layout.
const finalCrossings = countAllCrossings(
finalSections,
finalMaps,
depths,
edges
);
const chosen = finalCrossings <= bestCrossings ? finalSections : bestSections;
const sortedSections: Record<number, Node[]> = {};
depths.forEach((depth, i) => {
sortedSections[depth] = chosen[i];
sortedSections[depth] = sections[i];
});
return sortedSections;
}
+1 -8
View File
@@ -2503,7 +2503,6 @@
"edit_sidebar": "Edit sidebar",
"edit_subtitle": "Synced on all devices",
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device.",
"default": "default",
"reset_to_defaults": "Reset to defaults",
"reset_confirmation": "Are you sure you want to reset the sidebar to its default configuration? This will restore the original order and visibility of all panels."
},
@@ -6195,8 +6194,7 @@
"paste_invalid_config": "Pasted script is not editable in the visual editor"
},
"trace": {
"edit_script": "Edit script",
"loading": "Loading"
"edit_script": "Edit script"
}
},
"scene": {
@@ -6344,8 +6342,6 @@
},
"account": {
"download_support_package": "Download support package",
"support_package_generating_preview": "Generating preview",
"support_package_privacy_warning": "This file may contain personal data about your home. Avoid sharing them with unverified or untrusted parties.",
"reset_cloud_data": "Reset cloud data",
"reset_data_confirm_title": "Reset cloud data?",
"reset_data_confirm_text": "This will reset all your cloud settings. This includes your remote connection, Google Assistant, and Amazon Alexa integrations. This action cannot be undone.",
@@ -9222,9 +9218,6 @@
"title": "Delete section",
"text": "This section and all its cards will be deleted."
},
"error_section": {
"title": "Error"
},
"edit_section": {
"header": "Edit section",
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_view::tab_visibility%]",
+23 -102
View File
@@ -5,62 +5,8 @@
* yarn test:e2e:app
*/
import { test, expect, type Page } from "@playwright/test";
import type { MoreInfoView } from "../../src/dialogs/more-info/const";
import { PANEL_TIMEOUT, QUICK_TIMEOUT, SHELL_TIMEOUT } from "./helpers";
/**
* Each More info view renders one root element inside the dialog, plus one or
* more characteristic descendants that prove the view actually populated rather
* than rendering an empty shell. `text`, when set, asserts the element's text
* instead of just its presence.
*/
const MORE_INFO_VIEW_ELEMENTS: {
view: MoreInfoView;
element: string;
content: { selector: string; text?: string }[];
}[] = [
{
view: "info",
element: "ha-more-info-info",
content: [
{ selector: "more-info-light" },
{ selector: "span.title", text: "Test Light" },
],
},
{
view: "history",
element: "ha-more-info-history-and-logbook",
// The demo loads the history component but not logbook.
content: [{ selector: "ha-more-info-history" }],
},
{
view: "settings",
element: "ha-more-info-settings",
// The scenario mocks config/entity_registry/get, so the real registry
// panel renders instead of the "no unique ID" warning.
content: [{ selector: "entity-registry-settings" }],
},
{
view: "related",
element: "ha-related-items",
// search/related is mocked to return no relations, so the empty list
// renders.
content: [{ selector: "ha-related-items >> ha-list" }],
},
{
view: "add_to",
element: "ha-more-info-add-to",
// Admin users get the default add-to action list.
content: [{ selector: "ha-add-to-action-list" }],
},
{
view: "details",
element: "ha-more-info-details",
// The details view renders the state and attributes cards.
content: [{ selector: "ha-card" }],
},
];
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@@ -210,56 +156,31 @@ test.describe("Lovelace dashboard", () => {
// ---------------------------------------------------------------------------
test.describe("Light more-info dialog", () => {
for (const { view, element, content } of MORE_INFO_VIEW_ELEMENTS) {
test(`opens more-info ${view} view for a light entity`, async ({
page,
}) => {
// The light-more-info scenario seeds light.test_light synchronously.
await goToPanel(page, "/?scenario=light-more-info#/lovelace");
test("opens more-info dialog for a light entity", async ({ page }) => {
// The light-more-info scenario seeds light.test_light synchronously.
await goToPanel(page, "/?scenario=light-more-info#/lovelace");
const dialog = page.locator("ha-more-info-dialog");
// Fire the standard hass-more-info event from the app root with an
// explicit view. The HA shell opens ha-more-info-dialog on the requested
// view directly, so the test does not depend on the admin/demo-hidden
// header controls.
//
// The event is one-shot: if it lands before the shell's hass-more-info
// listener is attached it is silently dropped. Re-dispatching is
// idempotent (showDialog just resets the dialog to the requested view),
// so poll the dispatch until the requested view actually renders.
await expect(async () => {
await page.evaluate((v) => {
const el = document.querySelector("ha-test");
el?.dispatchEvent(
new CustomEvent("hass-more-info", {
detail: { entityId: "light.test_light", view: v },
bubbles: true,
composed: true,
})
);
}, view);
await expect(dialog).toBeAttached({ timeout: QUICK_TIMEOUT });
await expect(dialog.locator(element)).toBeAttached({
timeout: QUICK_TIMEOUT,
});
}).toPass({ timeout: SHELL_TIMEOUT });
// Each view should render its own characteristic content, not just an
// empty shell.
for (const { selector, text } of content) {
const locator = dialog.locator(selector).first();
if (text) {
// eslint-disable-next-line no-await-in-loop
await expect(locator).toContainText(text, { timeout: QUICK_TIMEOUT });
} else {
// eslint-disable-next-line no-await-in-loop
await expect(locator).toBeAttached({ timeout: QUICK_TIMEOUT });
}
}
// Fire the standard hass-more-info event from the app root. The HA shell
// listens for this and opens ha-more-info-dialog via its dialog manager.
await page.evaluate(() => {
const el = document.querySelector("ha-test");
el?.dispatchEvent(
new CustomEvent("hass-more-info", {
detail: { entityId: "light.test_light" },
bubbles: true,
composed: true,
})
);
});
}
const dialog = page.locator("ha-more-info-dialog");
await expect(dialog).toBeAttached({ timeout: SHELL_TIMEOUT });
// Confirm it actually rendered our entity, not a generic empty dialog.
await expect(dialog.locator("span.title")).toContainText("Test Light", {
timeout: QUICK_TIMEOUT,
});
});
});
// ---------------------------------------------------------------------------
-9
View File
@@ -1,9 +0,0 @@
#!/bin/sh
# Develop the e2e test app
# Stop on errors
set -e
cd "$(dirname "$0")/../../../.."
./node_modules/.bin/gulp develop-e2e-test-app
-2
View File
@@ -25,7 +25,6 @@ import { mockLovelace } from "../../../../demo/src/stubs/lovelace";
import { mockMediaPlayer } from "../../../../demo/src/stubs/media_player";
import { mockPersistentNotification } from "../../../../demo/src/stubs/persistent_notification";
import { mockRecorder } from "../../../../demo/src/stubs/recorder";
import { mockSearch } from "../../../../demo/src/stubs/search";
import { mockSensor } from "../../../../demo/src/stubs/sensor";
import { mockSystemLog } from "../../../../demo/src/stubs/system_log";
import { mockTemplate } from "../../../../demo/src/stubs/template";
@@ -101,7 +100,6 @@ export class HaTest extends HomeAssistantAppEl {
mockConfigEntries(hass);
mockIcons(hass);
mockPersistentNotification(hass);
mockSearch(hass);
// Load default entities from the sections config
hass.addEntities(energyEntities());
-31
View File
@@ -1,4 +1,3 @@
import type { ExtEntityRegistryEntry } from "../../../../../src/data/entity/entity_registry";
import type { MockHomeAssistant } from "../../../../../src/fake_data/provide_hass";
export type Scenario = (hass: MockHomeAssistant) => Promise<void> | void;
@@ -57,36 +56,6 @@ const lightMoreInfoScenario: Scenario = async (hass) => {
},
},
]);
// The base entity registry stub only mocks the list/get_entries commands, so
// the more-info settings view falls back to its "no unique ID" warning. Mock
// the single-entry lookup (config/entity_registry/get) so the settings view
// renders the real entity-registry-settings panel.
const registryEntry: ExtEntityRegistryEntry = {
created_at: 0,
modified_at: 0,
id: "test_light",
entity_id: "light.test_light",
unique_id: "test_light_unique_id",
name: null,
icon: null,
platform: "demo",
config_entry_id: null,
config_subentry_id: null,
device_id: null,
area_id: null,
labels: [],
disabled_by: null,
hidden_by: null,
entity_category: null,
has_entity_name: false,
original_name: "Test Light",
options: null,
categories: {},
capabilities: {},
aliases: [],
};
hass.mockWS("config/entity_registry/get", () => registryEntry);
};
// ── Registry ──────────────────────────────────────────────────────────────
@@ -12,7 +12,6 @@ import {
getPassThroughSections,
createPassThroughNode,
computeBarycenter,
dominantNeighborIndex,
sortNodesInSections,
} from "../../../../../src/resources/echarts/components/sankey/sankey-layout";
@@ -757,470 +756,5 @@ describe("Sankey Layout Functions", () => {
});
expectIdentityPreserved(result, input);
});
it("untangles a plateau-trapped subtree to remove an avoidable crossing (#52852)", () => {
// Realistic consumption tree: home → floors → areas → devices, plus two
// devices that attach higher up — one on a floor with no area
// (dev_floor_outside) and one straight on home (dev_home) — which the
// engine threads through with pass-throughs. The seed splits
// floor_outside's subtree: its pass-through child sits *after*
// floor_foundation's area. Pulling it back trades a crossing from the
// (1,2) boundary to the (2,3) boundary — a net-zero "plateau" the old
// strict gate refused, leaving the crossing. The plateau-escape must now
// take that step and let the device section follow, reaching 0 crossings.
const e = {
homeFo: { source: "home", target: "floor_outside", value: 1 },
homeFf: { source: "home", target: "floor_foundation", value: 1 },
foHvac: { source: "floor_outside", target: "area_hvac", value: 1 },
ffParking: {
source: "floor_foundation",
target: "area_parking",
value: 1,
},
hvacDev: { source: "area_hvac", target: "dev_hvac", value: 1 },
parkingDev: { source: "area_parking", target: "dev_parking", value: 1 },
foDev: {
source: "floor_outside",
target: "dev_floor_outside",
value: 1,
},
homeDev: { source: "home", target: "dev_home", value: 1 },
};
const testNodes: Record<string, TestNode> = {
home: {
id: "home",
depth: 0,
value: 4,
inEdges: [],
outEdges: [e.homeFo, e.homeFf, e.homeDev],
},
floor_outside: {
id: "floor_outside",
depth: 1,
value: 2,
inEdges: [e.homeFo],
outEdges: [e.foHvac, e.foDev],
},
floor_foundation: {
id: "floor_foundation",
depth: 1,
value: 1,
inEdges: [e.homeFf],
outEdges: [e.ffParking],
},
area_hvac: {
id: "area_hvac",
depth: 2,
value: 1,
inEdges: [e.foHvac],
outEdges: [e.hvacDev],
},
area_parking: {
id: "area_parking",
depth: 2,
value: 1,
inEdges: [e.ffParking],
outEdges: [e.parkingDev],
},
dev_hvac: {
id: "dev_hvac",
depth: 3,
value: 1,
inEdges: [e.hvacDev],
outEdges: [],
},
dev_parking: {
id: "dev_parking",
depth: 3,
value: 1,
inEdges: [e.parkingDev],
outEdges: [],
},
dev_floor_outside: {
id: "dev_floor_outside",
depth: 3,
value: 1,
inEdges: [e.foDev],
outEdges: [],
},
dev_home: {
id: "dev_home",
depth: 3,
value: 1,
inEdges: [e.homeDev],
outEdges: [],
},
};
const { nodes: graph, edges } = buildGraph(testNodes);
const ptHome1 = createPassThroughNode("home", "dev_home", 1, 1);
const ptFo2 = createPassThroughNode(
"floor_outside",
"dev_floor_outside",
2,
1
);
const ptHome2 = createPassThroughNode("home", "dev_home", 2, 1);
// Seed order from ha-sankey-chart: pass-throughs appended after the real
// children, so floor_outside's subtree is broken across the section.
const input = {
0: [graph.home],
1: [graph.floor_outside, graph.floor_foundation, ptHome1],
2: [graph.area_hvac, graph.area_parking, ptFo2, ptHome2],
3: [
graph.dev_hvac,
graph.dev_parking,
graph.dev_floor_outside,
graph.dev_home,
],
};
const result = sortNodesInSections(input, [0, 1, 2, 3], edges);
// floor_outside's children (area_hvac and its pass-through) are now
// contiguous, ahead of floor_foundation's; the layout is crossing-free.
expect(sectionIds(result)).toEqual({
0: ["home"],
1: ["floor_outside", "floor_foundation", "home-dev_home-1"],
2: [
"area_hvac",
"floor_outside-dev_floor_outside-2",
"area_parking",
"home-dev_home-2",
],
3: ["dev_hvac", "dev_floor_outside", "dev_parking", "dev_home"],
});
expectIdentityPreserved(result, input);
// Re-running on the result must not drift: plateau churn is discarded and
// the best snapshot is returned, so the order is stable (idempotent).
const again = sortNodesInSections(result, [0, 1, 2, 3], edges);
expect(sectionIds(again)).toEqual(sectionIds(result));
});
it("groups single-parent siblings under their parent and keeps configured sibling order", () => {
// Two floors, two areas each, fed to the engine interleaved (not grouped
// by floor). The deterministic hierarchy pass must regroup areas under
// their floor, and within a floor preserve the configured (seed) order:
// a1 before a2, b1 before b2.
const e = {
hFa: { source: "home", target: "floor_a", value: 2 },
hFb: { source: "home", target: "floor_b", value: 2 },
faA1: { source: "floor_a", target: "a1", value: 1 },
faA2: { source: "floor_a", target: "a2", value: 1 },
fbB1: { source: "floor_b", target: "b1", value: 1 },
fbB2: { source: "floor_b", target: "b2", value: 1 },
};
const testNodes: Record<string, TestNode> = {
home: {
id: "home",
depth: 0,
value: 4,
inEdges: [],
outEdges: [e.hFa, e.hFb],
},
floor_a: {
id: "floor_a",
depth: 1,
value: 2,
inEdges: [e.hFa],
outEdges: [e.faA1, e.faA2],
},
floor_b: {
id: "floor_b",
depth: 1,
value: 2,
inEdges: [e.hFb],
outEdges: [e.fbB1, e.fbB2],
},
a1: { id: "a1", depth: 2, value: 1, inEdges: [e.faA1], outEdges: [] },
a2: { id: "a2", depth: 2, value: 1, inEdges: [e.faA2], outEdges: [] },
b1: { id: "b1", depth: 2, value: 1, inEdges: [e.fbB1], outEdges: [] },
b2: { id: "b2", depth: 2, value: 1, inEdges: [e.fbB2], outEdges: [] },
};
const { nodes: graph, edges } = buildGraph(testNodes);
const input = {
0: [graph.home],
1: [graph.floor_a, graph.floor_b],
2: [graph.a1, graph.b1, graph.a2, graph.b2], // interleaved
};
const result = sortNodesInSections(input, [0, 1, 2], edges);
expect(sectionIds(result)).toEqual({
0: ["home"],
1: ["floor_a", "floor_b"],
2: ["a1", "a2", "b1", "b2"],
});
expectIdentityPreserved(result, input);
});
it("orders a single-parent section by parent position, ignoring flow magnitude", () => {
// childB carries a far larger flow than childA, but a single-parent
// section is ordered by parent position (hierarchy), never by value.
const edgeACa = { source: "A", target: "childA", value: 1 };
const edgeBCb = { source: "B", target: "childB", value: 100 };
const testNodes: Record<string, TestNode> = {
A: { id: "A", depth: 0, value: 1, inEdges: [], outEdges: [edgeACa] },
B: { id: "B", depth: 0, value: 100, inEdges: [], outEdges: [edgeBCb] },
childA: {
id: "childA",
depth: 1,
value: 1,
inEdges: [edgeACa],
outEdges: [],
},
childB: {
id: "childB",
depth: 1,
value: 100,
inEdges: [edgeBCb],
outEdges: [],
},
};
const { nodes: graph, edges } = buildGraph(testNodes);
const input = { 0: [graph.A, graph.B], 1: [graph.childB, graph.childA] };
const result = sortNodesInSections(input, [0, 1], edges);
expect(sectionIds(result)).toEqual({
0: ["A", "B"],
1: ["childA", "childB"],
});
expectIdentityPreserved(result, input);
});
it("keeps the single-parent tree hierarchical below a multi-parent source layer", () => {
// grid + solar feed home (a genuine multi-parent section that stays under
// the barycenter sweep); the floor/area tree below is single-parent and
// must regroup by parent regardless of the multi-parent head.
const e = {
gH: { source: "grid", target: "home", value: 2 },
sH: { source: "solar", target: "home", value: 2 },
hFa: { source: "home", target: "floor_a", value: 2 },
hFb: { source: "home", target: "floor_b", value: 2 },
faA: { source: "floor_a", target: "area_a", value: 2 },
fbB: { source: "floor_b", target: "area_b", value: 2 },
};
const testNodes: Record<string, TestNode> = {
grid: { id: "grid", depth: 0, value: 2, inEdges: [], outEdges: [e.gH] },
solar: {
id: "solar",
depth: 0,
value: 2,
inEdges: [],
outEdges: [e.sH],
},
home: {
id: "home",
depth: 1,
value: 4,
inEdges: [e.gH, e.sH],
outEdges: [e.hFa, e.hFb],
},
floor_a: {
id: "floor_a",
depth: 2,
value: 2,
inEdges: [e.hFa],
outEdges: [e.faA],
},
floor_b: {
id: "floor_b",
depth: 2,
value: 2,
inEdges: [e.hFb],
outEdges: [e.fbB],
},
area_a: {
id: "area_a",
depth: 3,
value: 2,
inEdges: [e.faA],
outEdges: [],
},
area_b: {
id: "area_b",
depth: 3,
value: 2,
inEdges: [e.fbB],
outEdges: [],
},
};
const { nodes: graph, edges } = buildGraph(testNodes);
const input = {
0: [graph.grid, graph.solar],
1: [graph.home],
2: [graph.floor_a, graph.floor_b],
3: [graph.area_b, graph.area_a], // reversed; must regroup under floors
};
const result = sortNodesInSections(input, [0, 1, 2, 3], edges);
expect(sectionIds(result)).toEqual({
0: ["grid", "solar"],
1: ["home"],
2: ["floor_a", "floor_b"],
3: ["area_a", "area_b"],
});
expectIdentityPreserved(result, input);
});
it("regroups single-parent children after a multi-parent head is reordered (idempotent)", () => {
// A multi-parent head section [H0,H1,H2] (only H1 draws from two sources)
// gets reordered by the barycenter sweep. Its single-parent children must
// then be regrouped under the *settled* head — H1's children before H2's
// child — and the result must be idempotent. Before the head/tree passes
// were ordered correctly the children stayed grouped against the head's
// SEED order, which both mis-grouped them and broke f(f(x)) === f(x).
const e = {
s0h0: { source: "S0", target: "H0", value: 6 },
s0h1: { source: "S0", target: "H1", value: 7 },
s1h1: { source: "S1", target: "H1", value: 5 },
s2h2: { source: "S2", target: "H2", value: 6 },
h1d0: { source: "H1", target: "D0", value: 7 },
h1d2: { source: "H1", target: "D2", value: 9 },
h2d1: { source: "H2", target: "D1", value: 7 },
};
const testNodes: Record<string, TestNode> = {
S0: {
id: "S0",
depth: 0,
value: 13,
inEdges: [],
outEdges: [e.s0h0, e.s0h1],
},
S1: { id: "S1", depth: 0, value: 5, inEdges: [], outEdges: [e.s1h1] },
S2: { id: "S2", depth: 0, value: 6, inEdges: [], outEdges: [e.s2h2] },
H0: { id: "H0", depth: 1, value: 6, inEdges: [e.s0h0], outEdges: [] },
H1: {
id: "H1",
depth: 1,
value: 12,
inEdges: [e.s1h1, e.s0h1],
outEdges: [e.h1d0, e.h1d2],
},
H2: {
id: "H2",
depth: 1,
value: 6,
inEdges: [e.s2h2],
outEdges: [e.h2d1],
},
D0: { id: "D0", depth: 2, value: 7, inEdges: [e.h1d0], outEdges: [] },
D1: { id: "D1", depth: 2, value: 7, inEdges: [e.h2d1], outEdges: [] },
D2: { id: "D2", depth: 2, value: 9, inEdges: [e.h1d2], outEdges: [] },
};
const { nodes: graph, edges } = buildGraph(testNodes);
const input = {
0: [graph.S0, graph.S1, graph.S2],
1: [graph.H2, graph.H1, graph.H0],
2: [graph.D1, graph.D2, graph.D0],
};
const result = sortNodesInSections(input, [0, 1, 2], edges);
// Head settles to barycenter order [H0,H1,H2]; children regroup under it:
// H1's children (D2,D0) precede H2's child (D1).
expect(sectionIds(result)).toEqual({
0: ["S0", "S1", "S2"],
1: ["H0", "H1", "H2"],
2: ["D2", "D0", "D1"],
});
expectIdentityPreserved(result, input);
// Idempotent: re-feeding the output yields the identical order.
const again = sortNodesInSections(result, [0, 1, 2], edges);
expect(sectionIds(again)).toEqual(sectionIds(result));
});
it("lets single-parent children follow their reordered multi-parent parent to reach 0 crossings", () => {
// root [a,b,c,d]; section 1 [m1,m2] is multi-parent with a clean split
// (c,d -> m1 ; a,b -> m2); section 2 [x<-m1, y<-m2] single-parent. The
// optimum needs BOTH the parent order swapped (m2,m1) AND the children to
// follow (y,x) — placing the tree after the head settles reaches it.
const e = {
am2: { source: "a", target: "m2", value: 1 },
bm2: { source: "b", target: "m2", value: 1 },
cm1: { source: "c", target: "m1", value: 1 },
dm1: { source: "d", target: "m1", value: 1 },
m1x: { source: "m1", target: "x", value: 2 },
m2y: { source: "m2", target: "y", value: 2 },
};
const testNodes: Record<string, TestNode> = {
a: { id: "a", depth: 0, value: 1, inEdges: [], outEdges: [e.am2] },
b: { id: "b", depth: 0, value: 1, inEdges: [], outEdges: [e.bm2] },
c: { id: "c", depth: 0, value: 1, inEdges: [], outEdges: [e.cm1] },
d: { id: "d", depth: 0, value: 1, inEdges: [], outEdges: [e.dm1] },
m1: {
id: "m1",
depth: 1,
value: 2,
inEdges: [e.cm1, e.dm1],
outEdges: [e.m1x],
},
m2: {
id: "m2",
depth: 1,
value: 2,
inEdges: [e.am2, e.bm2],
outEdges: [e.m2y],
},
x: { id: "x", depth: 2, value: 2, inEdges: [e.m1x], outEdges: [] },
y: { id: "y", depth: 2, value: 2, inEdges: [e.m2y], outEdges: [] },
};
const { nodes: graph, edges } = buildGraph(testNodes);
const input = {
0: [graph.a, graph.b, graph.c, graph.d],
1: [graph.m1, graph.m2],
2: [graph.x, graph.y],
};
const result = sortNodesInSections(input, [0, 1, 2], edges);
expect(sectionIds(result)).toEqual({
0: ["a", "b", "c", "d"],
1: ["m2", "m1"],
2: ["y", "x"],
});
expectIdentityPreserved(result, input);
});
});
describe("dominantNeighborIndex", () => {
it("returns the index of the single heaviest neighbor", () => {
const map = new Map([
["light", 0],
["heavy", 3],
]);
const result = dominantNeighborIndex(
[
{ id: "light", weight: 1 },
{ id: "heavy", weight: 5 },
],
map,
9
);
expect(result).toBe(3);
});
it("breaks weight ties by the earliest edge", () => {
const map = new Map([
["a", 2],
["b", 4],
]);
const result = dominantNeighborIndex(
[
{ id: "a", weight: 1 },
{ id: "b", weight: 1 },
],
map,
9
);
expect(result).toBe(2);
});
it("falls back when no neighbor is in the reference section", () => {
const result = dominantNeighborIndex(
[{ id: "missing", weight: 1 }],
new Map(),
7
);
expect(result).toBe(7);
});
});
});
+99 -91
View File
@@ -4187,6 +4187,13 @@ __metadata:
languageName: node
linkType: hard
"@package-json/types@npm:^0.0.12":
version: 0.0.12
resolution: "@package-json/types@npm:0.0.12"
checksum: 10/435d921b3ccc817ebe282c6eaac50282691d3be4dafb461d01c03b89fb89cda14ba7b5e20d01503bc1996ab93f98ae8e71601c3be7119b4ea76193b550523fcd
languageName: node
linkType: hard
"@playwright/test@npm:1.61.0":
version: 1.61.0
resolution: "@playwright/test@npm:1.61.0"
@@ -5712,105 +5719,105 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.62.0":
version: 8.62.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.62.0"
"@typescript-eslint/eslint-plugin@npm:8.61.1":
version: 8.61.1
resolution: "@typescript-eslint/eslint-plugin@npm:8.61.1"
dependencies:
"@eslint-community/regexpp": "npm:^4.12.2"
"@typescript-eslint/scope-manager": "npm:8.62.0"
"@typescript-eslint/type-utils": "npm:8.62.0"
"@typescript-eslint/utils": "npm:8.62.0"
"@typescript-eslint/visitor-keys": "npm:8.62.0"
"@typescript-eslint/scope-manager": "npm:8.61.1"
"@typescript-eslint/type-utils": "npm:8.61.1"
"@typescript-eslint/utils": "npm:8.61.1"
"@typescript-eslint/visitor-keys": "npm:8.61.1"
ignore: "npm:^7.0.5"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.5.0"
peerDependencies:
"@typescript-eslint/parser": ^8.62.0
"@typescript-eslint/parser": ^8.61.1
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/a093962c84e49d7524078a97c3ecfdedfaa217a6f68047d3eedb29677425210acfacaa2fe88f447e9662063979f31c8268e4568caca038df09deee9f06124d7f
checksum: 10/5434b78781f750eb1e2918f960ff3a6a3fae36951591456b1a309695a5c6c027d914038d7c2c71e614611b5c46a3be85b4b004581be0bcfb1be84e741b0e98a8
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.62.0":
version: 8.62.0
resolution: "@typescript-eslint/parser@npm:8.62.0"
"@typescript-eslint/parser@npm:8.61.1":
version: 8.61.1
resolution: "@typescript-eslint/parser@npm:8.61.1"
dependencies:
"@typescript-eslint/scope-manager": "npm:8.62.0"
"@typescript-eslint/types": "npm:8.62.0"
"@typescript-eslint/typescript-estree": "npm:8.62.0"
"@typescript-eslint/visitor-keys": "npm:8.62.0"
"@typescript-eslint/scope-manager": "npm:8.61.1"
"@typescript-eslint/types": "npm:8.61.1"
"@typescript-eslint/typescript-estree": "npm:8.61.1"
"@typescript-eslint/visitor-keys": "npm:8.61.1"
debug: "npm:^4.4.3"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/d0abbf12080532f6460af186098fab15f1f3695ee4817f96209ecfb00ef7ec89ec476051bde35a396217c8e37d5e441f3814807eb082e11904a0a1dc4b6d3b14
checksum: 10/cdaca9bb78bd6cc7210e88b28c42af11b23a47393a97ed37350e64a3846d036ebd178583fd2a54216974a740b3f6932274bdaf72046e6307ef26aee3ebe35cec
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.62.0":
version: 8.62.0
resolution: "@typescript-eslint/project-service@npm:8.62.0"
"@typescript-eslint/project-service@npm:8.61.1":
version: 8.61.1
resolution: "@typescript-eslint/project-service@npm:8.61.1"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.62.0"
"@typescript-eslint/types": "npm:^8.62.0"
"@typescript-eslint/tsconfig-utils": "npm:^8.61.1"
"@typescript-eslint/types": "npm:^8.61.1"
debug: "npm:^4.4.3"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10/e296f3aaaf7b4fc56e6410420a98a995f59bf45187445c9ad94d76de557a47071558869414c8ec179dfefce4f65ef8c15fcda7db653ed8fb95ff25b8119f9bb1
checksum: 10/51eb5cbd74748d08512db976bbeabdb9352f44e220621dce3bc96837bc309c7d266df49007be196e57950cf9e12e2c574649cbf14aa2e518734ee55ff7d86f2c
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.62.0":
version: 8.62.0
resolution: "@typescript-eslint/scope-manager@npm:8.62.0"
"@typescript-eslint/scope-manager@npm:8.61.1":
version: 8.61.1
resolution: "@typescript-eslint/scope-manager@npm:8.61.1"
dependencies:
"@typescript-eslint/types": "npm:8.62.0"
"@typescript-eslint/visitor-keys": "npm:8.62.0"
checksum: 10/6477062eb056986c9f94b35761c0b67bb9995798ba94c5d2bcb01932e525604715ce62e816468b2c80a8f05daa33b3339ea40646a31f733ea9840cee1dd3e82d
"@typescript-eslint/types": "npm:8.61.1"
"@typescript-eslint/visitor-keys": "npm:8.61.1"
checksum: 10/69c1d5b403b2e6adbae7e24856628941e912304ac728b6beae959df98092707adf3f60e1d0c9b90badd758d76174e9d44a7eba55608693c976e5cba5cc47593c
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.62.0, @typescript-eslint/tsconfig-utils@npm:^8.62.0":
version: 8.62.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.62.0"
"@typescript-eslint/tsconfig-utils@npm:8.61.1, @typescript-eslint/tsconfig-utils@npm:^8.61.1":
version: 8.61.1
resolution: "@typescript-eslint/tsconfig-utils@npm:8.61.1"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10/578f486df8eb2d2ec3939afc37102b89521d531d409d76e30a8ac3e9f48a3ae410e19e40c2aba3810f28391925a35ed391204ff786cc230542de82817efafda0
checksum: 10/ee81d01809178d6fd88a478bdf8fb546063c55f01385c6443fe7b93ebe9ba26ecda4f0eb804b2fc0a189dc34a51e89690477fb68cd099cd3952902f376d641c6
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.62.0":
version: 8.62.0
resolution: "@typescript-eslint/type-utils@npm:8.62.0"
"@typescript-eslint/type-utils@npm:8.61.1":
version: 8.61.1
resolution: "@typescript-eslint/type-utils@npm:8.61.1"
dependencies:
"@typescript-eslint/types": "npm:8.62.0"
"@typescript-eslint/typescript-estree": "npm:8.62.0"
"@typescript-eslint/utils": "npm:8.62.0"
"@typescript-eslint/types": "npm:8.61.1"
"@typescript-eslint/typescript-estree": "npm:8.61.1"
"@typescript-eslint/utils": "npm:8.61.1"
debug: "npm:^4.4.3"
ts-api-utils: "npm:^2.5.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/e1627d2bd792cf856c36db4f4e9c89e3111ad9bf81fc7489e957d26d36f89ab00226eee1838ee499947a898e37c1f30338bca4fa05ca437154c1813d54831b7e
checksum: 10/01c62227479d94ed3745e3bef5a0c870586d75b6ed550e4beb84b25df23de29558e0bdb6ff291e457977254764766c5d252b75a03d4ad592998269dba69a32a6
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.62.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.62.0":
version: 8.62.0
resolution: "@typescript-eslint/types@npm:8.62.0"
checksum: 10/459f5834dedbb73fc80c8eb92de693faef0f0f341d3b4d65426dbf43640f98a50104f6e15108808aed2c3c66d518db15a648c72c40831f498d36bfb62be564cb
"@typescript-eslint/types@npm:8.61.1, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.61.1":
version: 8.61.1
resolution: "@typescript-eslint/types@npm:8.61.1"
checksum: 10/9aa036b27d1874533bf1b931151adaaebb4380cfd14cc71395ab8d2c00c7420218e42811c2b5a68c9fa54cd048e1ab47085afd8b44cd5d736fb5cb8edd300d64
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.62.0":
version: 8.62.0
resolution: "@typescript-eslint/typescript-estree@npm:8.62.0"
"@typescript-eslint/typescript-estree@npm:8.61.1":
version: 8.61.1
resolution: "@typescript-eslint/typescript-estree@npm:8.61.1"
dependencies:
"@typescript-eslint/project-service": "npm:8.62.0"
"@typescript-eslint/tsconfig-utils": "npm:8.62.0"
"@typescript-eslint/types": "npm:8.62.0"
"@typescript-eslint/visitor-keys": "npm:8.62.0"
"@typescript-eslint/project-service": "npm:8.61.1"
"@typescript-eslint/tsconfig-utils": "npm:8.61.1"
"@typescript-eslint/types": "npm:8.61.1"
"@typescript-eslint/visitor-keys": "npm:8.61.1"
debug: "npm:^4.4.3"
minimatch: "npm:^10.2.2"
semver: "npm:^7.7.3"
@@ -5818,32 +5825,32 @@ __metadata:
ts-api-utils: "npm:^2.5.0"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10/c3ae8e13671957e4a1c4acfc861b40e1545a9d32fe9d5cc851992186314f5a1dbe780cecc8f16b8448a1350f4c11648572352b5d04b77bb62e8dac3e6f3a2e04
checksum: 10/36b8b68e7fe9bdba0bb24b46a43a29e39f0cd45440ea190644e230d10e96b0bab9289027668910ff976100d68a9b3bc222618bf96b99bae6fd0053eb560a1257
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.62.0":
version: 8.62.0
resolution: "@typescript-eslint/utils@npm:8.62.0"
"@typescript-eslint/utils@npm:8.61.1":
version: 8.61.1
resolution: "@typescript-eslint/utils@npm:8.61.1"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.9.1"
"@typescript-eslint/scope-manager": "npm:8.62.0"
"@typescript-eslint/types": "npm:8.62.0"
"@typescript-eslint/typescript-estree": "npm:8.62.0"
"@typescript-eslint/scope-manager": "npm:8.61.1"
"@typescript-eslint/types": "npm:8.61.1"
"@typescript-eslint/typescript-estree": "npm:8.61.1"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/95fed9feb823106f09b517637b0cf00e39fdc3537d05023f84710f23d00457898d8f1c68a69115d624a686411136b5cfdd7b84b657d6b51aea410cf2eb7fde7a
checksum: 10/7c8886801f73fc09ecf585b0e8f33799e4a341d51b00db0467f05853f84b808bb98a35e92eab49f28d5d24a1c915959d834214776f0ff6f0cfa5abb3f2e11496
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.62.0":
version: 8.62.0
resolution: "@typescript-eslint/visitor-keys@npm:8.62.0"
"@typescript-eslint/visitor-keys@npm:8.61.1":
version: 8.61.1
resolution: "@typescript-eslint/visitor-keys@npm:8.61.1"
dependencies:
"@typescript-eslint/types": "npm:8.62.0"
"@typescript-eslint/types": "npm:8.61.1"
eslint-visitor-keys: "npm:^5.0.0"
checksum: 10/63d66db628befb1d160c02f3fe3b00b7c7ffc47a477335591affde2108e913a72d4a327bdd15d91f5784c3c3624e9b347e9351754390a61ca182bb7e1788d350
checksum: 10/7d99c6ae9e91d32b8cc3662ead0e393c912351b5786ece62e1dc198a6b0e9813bb7eae44772970512e7e424a342c86529d9f3dae7c8ab83ac95b5dc33826647a
languageName: node
linkType: hard
@@ -6233,13 +6240,6 @@ __metadata:
languageName: node
linkType: hard
"@vvo/tzdb@npm:6.198.0":
version: 6.198.0
resolution: "@vvo/tzdb@npm:6.198.0"
checksum: 10/702d25ed7e7a55c4ee3c81e5de79cdb5d11c73bc02e511fa8f93eb497e6ada1b198805469f9e203bef6ea304b914637a6c570f19ade405b6e97d45181a0216a1
languageName: node
linkType: hard
"@webcomponents/scoped-custom-element-registry@npm:0.0.10":
version: 0.0.10
resolution: "@webcomponents/scoped-custom-element-registry@npm:0.0.10"
@@ -8536,10 +8536,11 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-import-x@npm:4.17.0":
version: 4.17.0
resolution: "eslint-plugin-import-x@npm:4.17.0"
"eslint-plugin-import-x@npm:4.16.2":
version: 4.16.2
resolution: "eslint-plugin-import-x@npm:4.16.2"
dependencies:
"@package-json/types": "npm:^0.0.12"
"@typescript-eslint/types": "npm:^8.56.0"
comment-parser: "npm:^1.4.1"
debug: "npm:^4.4.1"
@@ -8558,7 +8559,7 @@ __metadata:
optional: true
eslint-import-resolver-node:
optional: true
checksum: 10/143081e0a2cb418990d5d61c08ad4dd46f4f10dd7664939cc4be8454c2f51cd69134746d2d8b7534786f3a13857d176456bb0c1d1ffc9c168830b4ce93d2c0a8
checksum: 10/b149ca51ffba102535cddbe37f298ab3704181ea877a00b87d50062e03ad31a86317cf4205ac3ebc6d4e7872953215777ee4a3b0390616937f3adc9a86b86a90
languageName: node
linkType: hard
@@ -9463,10 +9464,10 @@ __metadata:
languageName: node
linkType: hard
"globals@npm:17.7.0":
version: 17.7.0
resolution: "globals@npm:17.7.0"
checksum: 10/79304ccc4d2ca167ea15bdb25da346aa34ce3847b18fbd6c3cad182e152505305db3c9722fd5e292c62f6db97a8fa06e0c110a1e7703d7325498e5351d08cab4
"globals@npm:17.6.0":
version: 17.6.0
resolution: "globals@npm:17.6.0"
checksum: 10/2bf0febf31c942edee6f4eca7e939a9c885f8ecfb767048b1c4dd2a32008d0ab136e6076665d76b44b29c2571bbbc1681371caab05fd8ee0067c7618e841b89d
languageName: node
linkType: hard
@@ -9510,6 +9511,13 @@ __metadata:
languageName: node
linkType: hard
"google-timezones-json@npm:1.2.0":
version: 1.2.0
resolution: "google-timezones-json@npm:1.2.0"
checksum: 10/c83fdaa3681de7b63704aa5d5c644fecd1e2c46047eb65716fd0a1ef28a778b1bbfd6f521c499247b4d7afdc085c7d8bbdbea56398492d395ef9c8d87a648b11
languageName: node
linkType: hard
"gopd@npm:^1.0.1, gopd@npm:^1.2.0":
version: 1.2.0
resolution: "gopd@npm:1.2.0"
@@ -9766,7 +9774,6 @@ __metadata:
"@types/tar": "npm:7.0.87"
"@vibrant/color": "npm:4.0.4"
"@vitest/coverage-v8": "npm:4.1.9"
"@vvo/tzdb": "npm:6.198.0"
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
"@webcomponents/webcomponentsjs": "npm:2.8.0"
babel-loader: "npm:10.1.1"
@@ -9789,7 +9796,7 @@ __metadata:
eslint: "npm:10.5.0"
eslint-config-prettier: "npm:10.1.8"
eslint-import-resolver-webpack: "npm:0.13.11"
eslint-plugin-import-x: "npm:4.17.0"
eslint-plugin-import-x: "npm:4.16.2"
eslint-plugin-lit: "npm:2.3.1"
eslint-plugin-lit-a11y: "npm:5.1.1"
eslint-plugin-unused-imports: "npm:4.4.1"
@@ -9799,7 +9806,8 @@ __metadata:
fuse.js: "npm:7.4.2"
generate-license-file: "npm:4.2.1"
glob: "npm:13.0.6"
globals: "npm:17.7.0"
globals: "npm:17.6.0"
google-timezones-json: "npm:1.2.0"
gulp: "npm:5.0.1"
gulp-brotli: "npm:3.0.0"
gulp-json-transform: "npm:0.5.0"
@@ -9849,7 +9857,7 @@ __metadata:
tinykeys: "patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch"
ts-lit-plugin: "npm:2.0.2"
typescript: "npm:6.0.3"
typescript-eslint: "npm:8.62.0"
typescript-eslint: "npm:8.61.1"
vite-tsconfig-paths: "npm:6.1.1"
vitest: "npm:4.1.9"
webpack-stats-plugin: "npm:1.1.3"
@@ -14837,18 +14845,18 @@ __metadata:
languageName: node
linkType: hard
"typescript-eslint@npm:8.62.0":
version: 8.62.0
resolution: "typescript-eslint@npm:8.62.0"
"typescript-eslint@npm:8.61.1":
version: 8.61.1
resolution: "typescript-eslint@npm:8.61.1"
dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.62.0"
"@typescript-eslint/parser": "npm:8.62.0"
"@typescript-eslint/typescript-estree": "npm:8.62.0"
"@typescript-eslint/utils": "npm:8.62.0"
"@typescript-eslint/eslint-plugin": "npm:8.61.1"
"@typescript-eslint/parser": "npm:8.61.1"
"@typescript-eslint/typescript-estree": "npm:8.61.1"
"@typescript-eslint/utils": "npm:8.61.1"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/c75b16115a3e6f7704f71a9fe14d8cf52129db7e0578755f10a344a0e22474cc0b5383822ef0344cd886b98300af4cce19a306ccda391b2e1eed6c07088f3019
checksum: 10/33a798da178f8942a5fb188a991a2eaa9047d7ca95178c67df2565531379b2a587c14ff836716a8b11ed3328c34af5e3b3ea985fa4d2a521a1bb605c2f0e0aa4
languageName: node
linkType: hard