Compare commits

..
Author SHA1 Message Date
Stefan AgnerandClaude Fable 5 62c9e3026a Fix backup progress segment order to match backend execution order
The segmented backup progress bar showed Apps, Media, Home Assistant,
but Supervisor backs up Home Assistant first since 2024 (supervisor#5203),
then add-ons, then folders. The Home Assistant segment activated first
while Apps/Media incorrectly showed as completed, and the bar regressed
once the apps stage started.

Also move the await-addon-restart stages into the last creation group,
since Supervisor awaits add-on restarts after the backup file is
finished, keeping the progress bar monotonic.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-15 13:37:42 +02:00
28 changed files with 228 additions and 431 deletions
+2 -2
View File
@@ -32,12 +32,12 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
with:
languages: javascript-typescript
build-mode: none
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
with:
category: "/language:javascript-typescript"
+8 -8
View File
@@ -1,15 +1,15 @@
{
"_comment": "Initial JS budget (raw/uncompressed bytes) for the cold-load critical entrypoints. Enforced by build-scripts/check-bundle-size.cjs in CI. Re-seed after an intentional change with `--update --headroom=<percent>`.",
"frontend-modern": {
"app": 585518,
"core": 57048,
"authorize": 552423,
"onboarding": 666818
"app": 595204,
"core": 57741,
"authorize": 576928,
"onboarding": 685964
},
"frontend-legacy": {
"app": 887384,
"core": 244482,
"authorize": 844995,
"onboarding": 1008816
"app": 861452,
"core": 258557,
"authorize": 834356,
"onboarding": 1001360
}
}
-5
View File
@@ -102,11 +102,6 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
cost_sensors: {},
solar_forecast_domains: [],
}));
hass.mockWS("energy/validate", () => ({
energy_sources: Array.from({ length: 6 }, () => []),
device_consumption: Array.from({ length: 6 }, () => []),
device_consumption_water: Array.from({ length: 2 }, () => []),
}));
hass.mockWS(
"energy/fossil_energy_consumption",
({ period }): FossilEnergyConsumption => ({
-6
View File
@@ -1,12 +1,6 @@
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockSensor = (hass: MockHomeAssistant) => {
hass.mockWS(
"sensor/device_class_convertible_units",
({ device_class }: { device_class: string }) => ({
units: device_class === "energy" ? ["kWh"] : ["W"],
})
);
hass.mockWS("sensor/numeric_device_classes", () => ({
numeric_device_classes: [
"volume_storage",
+11 -1
View File
@@ -15,6 +15,7 @@ const ALL_FEATURES =
VacuumEntityFeature.STOP +
VacuumEntityFeature.RETURN_HOME +
VacuumEntityFeature.FAN_SPEED +
VacuumEntityFeature.BATTERY +
VacuumEntityFeature.STATUS +
VacuumEntityFeature.LOCATE +
VacuumEntityFeature.CLEAN_SPOT +
@@ -27,6 +28,8 @@ const ENTITIES = [
attributes: {
friendly_name: "Full featured vacuum",
supported_features: ALL_FEATURES,
battery_level: 85,
battery_icon: "mdi:battery-80",
fan_speed: "balanced",
fan_speed_list: ["silent", "standard", "balanced", "turbo", "max"],
status: "Charged",
@@ -38,6 +41,8 @@ const ENTITIES = [
attributes: {
friendly_name: "Cleaning vacuum",
supported_features: ALL_FEATURES,
battery_level: 62,
battery_icon: "mdi:battery-60",
fan_speed: "turbo",
fan_speed_list: ["silent", "standard", "balanced", "turbo", "max"],
status: "Cleaning bedroom",
@@ -53,7 +58,10 @@ const ENTITIES = [
VacuumEntityFeature.START +
VacuumEntityFeature.PAUSE +
VacuumEntityFeature.STOP +
VacuumEntityFeature.RETURN_HOME,
VacuumEntityFeature.RETURN_HOME +
VacuumEntityFeature.BATTERY,
battery_level: 23,
battery_icon: "mdi:battery-20",
status: "Returning to dock",
},
},
@@ -88,6 +96,8 @@ const ENTITIES = [
attributes: {
friendly_name: "Paused vacuum",
supported_features: ALL_FEATURES,
battery_level: 45,
battery_icon: "mdi:battery-40",
fan_speed: "standard",
fan_speed_list: ["silent", "standard", "balanced", "turbo", "max"],
status: "Paused",
+5 -4
View File
@@ -103,7 +103,7 @@
"echarts": "6.1.0",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.5.0",
"hls.js": "1.7.0",
"hls.js": "1.6.17",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.3.0",
"intl-messageformat": "11.2.13",
@@ -151,8 +151,8 @@
"@octokit/plugin-retry": "8.1.1",
"@octokit/rest": "22.0.1",
"@playwright/test": "1.62.1",
"@rsdoctor/rspack-plugin": "1.6.2",
"@rspack/core": "2.1.10",
"@rsdoctor/rspack-plugin": "1.6.1",
"@rspack/core": "2.1.9",
"@rspack/dev-server": "2.2.0",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.26",
@@ -186,7 +186,7 @@
"fs-extra": "11.4.0",
"generate-license-file": "4.2.1",
"glob": "13.0.6",
"globals": "17.11.0",
"globals": "17.9.0",
"gulp": "5.0.1",
"gulp-json-transform": "0.5.0",
"gulp-rename": "2.1.0",
@@ -223,6 +223,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.2",
"@fullcalendar/daygrid": "6.1.21",
"globals": "17.9.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"
},
-3
View File
@@ -34,8 +34,6 @@ export class HaButtonToggleGroup extends LitElement {
@property({ type: Boolean, reflect: true, attribute: "full-width" })
public fullWidth = false;
@property({ type: Boolean }) public disabled = false;
@property() public variant:
"brand" | "neutral" | "success" | "warning" | "danger" = "brand";
@@ -59,7 +57,6 @@ export class HaButtonToggleGroup extends LitElement {
.value=${button.value}
@click=${this._handleClick}
.title=${button.label}
.disabled=${this.disabled}
.appearance=${this.active === button.value ? "accent" : "filled"}
>
${
+2 -3
View File
@@ -68,7 +68,6 @@ export class HaDurationInput extends LitElement {
{ label: "-", iconPath: mdiMinusThick, value: "-" },
]}
.active=${this._negative ? "-" : "+"}
.disabled=${this.disabled}
@value-changed=${this._negativeChanged}
></ha-button-toggle-group>
`
@@ -236,8 +235,8 @@ export class HaDurationInput extends LitElement {
ev.stopPropagation();
const negative = (ev.detail?.value || ev.target.value) === "-";
this._toggleNegative = negative;
if (this.data) {
const value = { ...this.data };
const value = this.data;
if (value) {
FIELDS.forEach((t) => {
if (value[t]) {
value[t] = negative ? -Math.abs(value[t]) : Math.abs(value[t]);
@@ -81,7 +81,6 @@ export class HaChooseSelector extends LitElement {
.required=${this.required}
@value-changed=${this._handleValueChanged}
.helper=${this.helper}
.localizeValue=${this.localizeValue}
></ha-selector>`;
}
@@ -108,12 +107,7 @@ export class HaChooseSelector extends LitElement {
: {
[this._activeChoice!]: this.value,
};
const choice = ev.detail?.value || ev.target.value;
this._activeChoice = choice;
if (choice && "constant" in this.selector.choose.choices[choice].selector) {
value[choice] =
this.selector.choose.choices[choice].selector.constant?.value;
}
this._activeChoice = ev.detail?.value || ev.target.value;
fireEvent(this, "value-changed", {
value: {
...value,
-1
View File
@@ -664,7 +664,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
display: flex;
flex-direction: column;
overflow: hidden;
overscroll-behavior: contain;
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
+2
View File
@@ -22,6 +22,7 @@ export enum VacuumEntityFeature {
STOP = 8,
RETURN_HOME = 16,
FAN_SPEED = 32,
BATTERY = 64,
STATUS = 128,
SEND_COMMAND = 256,
LOCATE = 512,
@@ -33,6 +34,7 @@ export enum VacuumEntityFeature {
}
interface VacuumEntityAttributes extends HassEntityAttributeBase {
battery_level?: number;
fan_speed?: string;
fan_speed_list?: string[];
[key: string]: any;
+2 -3
View File
@@ -566,10 +566,9 @@ export const getWeatherStateIcon = (
if (userDefinedIcon) {
return html`
<div
style=${styleMap({
"background-size": "cover",
style="background-size: cover;${styleMap({
"background-image": userDefinedIcon,
})}
})}"
></div>
`;
}
@@ -159,6 +159,23 @@ class MoreInfoVacuum extends LitElement {
`;
}
// Use deprecated battery_level and battery_icon attributes
if (
supportsFeature(this.stateObj, VacuumEntityFeature.BATTERY) &&
this.stateObj.attributes.battery_level
) {
return html`
<span class="battery" slot="after-time">
<span
>${Math.round(
this.stateObj.attributes.battery_level
)}${blankBeforePercent(this._i18n.locale)}%</span
>
<ha-icon .icon=${this.stateObj.attributes.battery_icon}></ha-icon>
</span>
`;
}
return nothing;
}
+5
View File
@@ -74,6 +74,11 @@ export class MockVacuumEntity extends MockBaseEntity {
stateAttrs.fan_speed = attrs.fan_speed ?? null;
}
if (supportsFeatureFromAttributes(attrs, VacuumEntityFeature.BATTERY)) {
stateAttrs.battery_level = attrs.battery_level ?? null;
stateAttrs.battery_icon = attrs.battery_icon ?? null;
}
if (supportsFeatureFromAttributes(attrs, VacuumEntityFeature.STATUS)) {
stateAttrs.status = attrs.status ?? null;
}
@@ -39,17 +39,22 @@ const ADDON_STAGES: CreateBackupStage[] = [
"addon_repositories",
"app_repositories",
"docker_config",
];
// Add-on restarts are awaited after the backup file is finished, so those
// stages belong to the last creation group to keep the progress monotonic
const MEDIA_STAGES: CreateBackupStage[] = [
"folders",
"finishing_file",
"await_addon_restarts",
"await_app_restarts",
];
const MEDIA_STAGES: CreateBackupStage[] = ["folders", "finishing_file"];
// Ordered groups matching actual backend execution order
const STAGE_ORDER: CreateBackupStage[][] = [
HA_STAGES,
ADDON_STAGES,
MEDIA_STAGES,
HA_STAGES,
["upload_to_agents"],
["cleaning_up"],
];
@@ -165,21 +170,21 @@ export class HaBackupOverviewProgress extends LitElement {
return [
{
label: this.hass.localize(
"ui.panel.config.backup.overview.progress.segments.apps"
"ui.panel.config.backup.overview.progress.segments.home_assistant"
),
state: this._getSegmentState(0, currentGroupIndex),
flex: 2,
},
{
label: this.hass.localize(
"ui.panel.config.backup.overview.progress.segments.media"
"ui.panel.config.backup.overview.progress.segments.apps"
),
state: this._getSegmentState(1, currentGroupIndex),
flex: 2,
},
{
label: this.hass.localize(
"ui.panel.config.backup.overview.progress.segments.home_assistant"
"ui.panel.config.backup.overview.progress.segments.media"
),
state: this._getSegmentState(2, currentGroupIndex),
flex: 2,
@@ -201,18 +206,18 @@ export class HaBackupOverviewProgress extends LitElement {
];
}
// Non-HAOS: No app segment, just Media, HA, Upload and Cleaning up
// Non-HAOS: No app segment, just HA, Media, Upload and Cleaning up
return [
{
label: this.hass.localize(
"ui.panel.config.backup.overview.progress.segments.media"
"ui.panel.config.backup.overview.progress.segments.home_assistant"
),
state: this._getSegmentState(1, currentGroupIndex),
state: this._getSegmentState(0, currentGroupIndex),
flex: 2,
},
{
label: this.hass.localize(
"ui.panel.config.backup.overview.progress.segments.home_assistant"
"ui.panel.config.backup.overview.progress.segments.media"
),
state: this._getSegmentState(2, currentGroupIndex),
flex: 2,
@@ -1,4 +1,4 @@
import { mdiAccount, mdiAccountPlus, mdiPencil } from "@mdi/js";
import { mdiPencil } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -19,7 +19,6 @@ import type { PersonMutableParams } from "../../../data/person";
import type { User } from "../../../data/user";
import {
deleteUser,
fetchUsers,
SYSTEM_GROUP_ID_ADMIN,
SYSTEM_GROUP_ID_USER,
updateUser,
@@ -37,8 +36,6 @@ import { documentationUrl } from "../../../util/documentation-url";
import { showAddUserDialog } from "../users/show-dialog-add-user";
import { showAdminChangePasswordDialog } from "../users/show-dialog-admin-change-password";
import type { PersonDetailDialogParams } from "./show-dialog-person-detail";
import { showListItemsDialog } from "../../../dialogs/dialog-list-items/show-list-items-dialog";
import { computeDomain } from "../../../common/entity/compute_domain";
const includeDomains = ["device_tracker"];
@@ -88,8 +85,6 @@ class DialogPersonDetail
@state() private _open = false;
private _linkedExistingUser = false;
private _deviceTrackersAvailable = memoizeOne((hass) =>
Object.keys(hass.states).some(
(entityId) =>
@@ -145,7 +140,7 @@ class DialogPersonDetail
private _dialogClosed() {
// If we do not have a person ID yet (= person creation dialog was just cancelled), but
// we already created a user ID for it, delete it now to not have it "free floating".
if (!this._personExists && this._userId && !this._linkedExistingUser) {
if (!this._personExists && this._userId) {
const callback = this._params?.refreshUsers;
deleteUser(this.hass, this._userId).then(() => {
callback?.();
@@ -431,71 +426,26 @@ class DialogPersonDetail
this._updateDirtyState(this._currentState());
}
private async _linkUser(user: User, newUser: boolean) {
this._linkedExistingUser = !newUser;
if (this._params!.entry && this._params!.updateEntry) {
await this._params!.updateEntry({ user_id: user.id });
}
if (newUser) {
this._params?.refreshUsers?.();
}
this._user = user;
this._userId = user.id;
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = user.local_only;
this._updateDirtyState(this._currentState());
}
private async _allowLoginChanged(ev): Promise<void> {
const target = ev.target;
if (target.checked) {
target.checked = false;
const users = await fetchUsers(this.hass);
const currentLinkedUsers = new Set(
Object.values(this.hass.states)
.filter(
(s) =>
computeDomain(s.entity_id) === "person" && s.attributes.user_id
)
.map((s) => s.attributes.user_id)
);
const eligibleUsers = users.filter(
(u) =>
!currentLinkedUsers.has(u.id) && !u.system_generated && u.username
);
const addUserDialog = () =>
showAddUserDialog(this, {
userAddedCallback: async (user?: User) => {
if (user) {
target.checked = true;
this._linkUser(user, true);
showAddUserDialog(this, {
userAddedCallback: async (user?: User) => {
if (user) {
target.checked = true;
if (this._params!.entry && this._params!.updateEntry) {
await this._params!.updateEntry({ user_id: user.id });
}
},
name: this._name,
});
if (eligibleUsers.length === 0) {
addUserDialog();
return;
}
showListItemsDialog(this, {
title: this.hass.localize("ui.panel.config.person.detail.select_user"),
items: [
{
iconPath: mdiAccountPlus,
label: this.hass.localize(
"ui.panel.config.person.detail.create_new_user"
),
action: addUserDialog,
},
...eligibleUsers.map((user) => ({
iconPath: mdiAccount,
label: `${user.name} (${user.username})`,
action: () => this._linkUser(user, false),
})),
],
this._params?.refreshUsers?.();
this._user = user;
this._userId = user.id;
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = user.local_only;
this._updateDirtyState(this._currentState());
}
},
name: this._name,
});
} else if (this._userId) {
if (
@@ -20,10 +20,9 @@ type ReloadableDomain = Exclude<
"heading" | "introduction" | "reload"
>;
interface TranslatedReloadableItem {
domain: ReloadableDomain | "frontend";
interface TranslatedReloadableDomain {
domain: ReloadableDomain;
name: string;
service?: string;
}
@customElement("tools-yaml-config")
@@ -38,7 +37,7 @@ export class ToolsYamlConfig extends LitElement {
@state() private _validating = false;
@state() private _reloadableItems: TranslatedReloadableItem[] = [];
@state() private _reloadableDomains: TranslatedReloadableDomain[] = [];
@state() private _validateResult?: CheckConfigResult;
@@ -55,10 +54,10 @@ export class ToolsYamlConfig extends LitElement {
oldHass.config.components !== this.hass.config.components ||
oldHass.localize !== this.hass.localize)
) {
this._reloadableItems = (
this._reloadableDomains = (
componentsWithService(this.hass, "reload") as ReloadableDomain[]
)
.map<TranslatedReloadableItem>((domain) => ({
.map((domain) => ({
domain,
name:
this.hass.localize(
@@ -69,15 +68,6 @@ export class ToolsYamlConfig extends LitElement {
{ domain: domainToName(this.hass.localize, domain) }
),
}))
.concat([
{
domain: "frontend",
service: "reload_themes",
name: this.hass.localize(
`ui.panel.config.tools.tabs.yaml.section.reloading.themes`
),
},
])
.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
@@ -200,12 +190,12 @@ export class ToolsYamlConfig extends LitElement {
)}
</ha-call-service-button>
</div>
${this._reloadableItems.map(
${this._reloadableDomains.map(
(reloadable) => html`
<div class="card-actions">
<ha-call-service-button
.domain=${reloadable.domain}
.service=${reloadable.service || "reload"}
service="reload"
>${reloadable.name}
</ha-call-service-button>
</div>
@@ -271,10 +271,6 @@ export class HuiEnergyUsageGraphCard
);
private async _getStatistics(energyData: EnergyData): Promise<void> {
if (!this.isConnected) {
return;
}
const datasets: BarSeriesOption[] = [];
let yMin = Infinity;
@@ -175,10 +175,6 @@ export class HuiPowerSourcesGraphCard
);
private async _getStatistics(energyData: EnergyData): Promise<void> {
if (!this.isConnected) {
return;
}
const result = generatePowerSourcesGraphData({
localize: this.hass.localize,
states: this.hass.states,
@@ -36,7 +36,7 @@ import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction, hasAnyAction } from "../common/has-action";
import { hasAction } from "../common/has-action";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type {
@@ -300,7 +300,6 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
return html`
<ha-card
class=${classMap({
"no-action": !hasAnyAction(this._config!),
[this._sizeController.value?.height]: Boolean(
this._sizeController.value
),
@@ -731,10 +730,6 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
padding: 16px 0;
}
ha-card.no-action {
cursor: default;
}
.content {
display: flex;
flex-wrap: nowrap;
+3 -12
View File
@@ -15,22 +15,13 @@ const _loadLovelaceResource = (
hass.auth.data.hassUrl
).toString();
const logLoadError = (err: unknown) => {
// eslint-disable-next-line no-console
console.error(
`Failed to load Lovelace resource ${normalizedUrl} (type: ${resource.type}). Check that the URL is correct and the file exists.`,
err
);
};
switch (resource.type) {
case "css": {
if (normalizedUrl in CSS_CACHE) {
return CSS_CACHE[normalizedUrl];
}
// Catch before caching, so a cache hit cannot log the failure again
const loadTask = loadCSS(normalizedUrl).catch(logLoadError);
const loadTask = loadCSS(normalizedUrl);
CSS_CACHE[normalizedUrl] = loadTask;
return loadTask;
}
@@ -40,13 +31,13 @@ const _loadLovelaceResource = (
return JS_CACHE[normalizedUrl];
}
const loadTask = loadJS(normalizedUrl).catch(logLoadError);
const loadTask = loadJS(normalizedUrl);
JS_CACHE[normalizedUrl] = loadTask;
return loadTask;
}
case "module":
return loadModule(normalizedUrl).catch(logLoadError);
return loadModule(normalizedUrl);
default:
// eslint-disable-next-line
-2
View File
@@ -7041,8 +7041,6 @@
"confirm_delete_user_text": "The user account for ''{name}'' will be permanently deleted. You can still track the user, but the person will no longer be able to log in.",
"allow_login": "Allow login",
"allow_login_description": "Allow access using username and password.",
"select_user": "Select user account",
"create_new_user": "Create a new username",
"username": "[%key:ui::panel::config::users::editor::username%]",
"password": "[%key:ui::panel::config::users::editor::password%]",
"admin": "[%key:ui::panel::config::users::editor::admin%]",
-79
View File
@@ -279,85 +279,6 @@ test.describe("Lovelace dashboard", () => {
});
});
test.describe("Energy dashboard", () => {
test("returns to Energy after repeatedly opening the device dialog", async ({
page,
}) => {
const errors = trackPageErrors(page);
await goToPanel(
page,
"/?historyBack=1&backPath=%2Flovelace#/energy/overview"
);
const energyRoot = page.locator("ha-panel-energy hui-root");
await expect(energyRoot).toBeAttached({ timeout: PANEL_TIMEOUT });
const editDashboard = energyRoot.getByRole("button", {
name: /^Edit dashboard\b/,
});
const dashboardMenu = energyRoot.getByRole("button", {
name: "Open dashboard menu",
});
await expect(editDashboard.or(dashboardMenu)).toBeVisible({
timeout: QUICK_TIMEOUT,
});
if (await editDashboard.isVisible()) {
await editDashboard.click();
} else {
await dashboardMenu.click();
await page.getByRole("menuitem", { name: /^Edit dashboard\b/ }).click();
}
await expect(page.locator("ha-config-energy")).toBeAttached({
timeout: PANEL_TIMEOUT,
});
const backLink = page
.locator("ha-config-energy hass-tabs-subpage")
.getByRole("link", { name: "Back" });
await expect(backLink).toHaveAttribute(
"href",
"/config/lovelace/dashboards"
);
const addDevice = page
.locator("ha-energy-device-settings")
.locator("ha-button")
.first();
const openAndCancelDeviceDialog = async () => {
await addDevice.click();
const dialog = page.locator("dialog-energy-device-settings");
const cancel = dialog.locator("ha-dialog-footer ha-button").first();
await expect(cancel).toBeVisible({ timeout: QUICK_TIMEOUT });
await cancel.click();
await expect(cancel).toBeHidden({ timeout: QUICK_TIMEOUT });
};
await openAndCancelDeviceDialog();
await openAndCancelDeviceDialog();
await openAndCancelDeviceDialog();
await backLink.click();
await expect
.poll(
() =>
page.evaluate(() => ({
hash: window.location.hash,
search: window.location.search,
})),
{ timeout: PANEL_TIMEOUT }
)
.toEqual({
hash: "#/energy/overview",
search: "?historyBack=1&backPath=%2Flovelace",
});
await expect(page.locator("ha-panel-energy")).toBeAttached();
await expect(
energyRoot.getByRole("link", { name: "Back" })
).toHaveAttribute("href", "/lovelace");
expectNoPageErrors(errors);
});
});
// ---------------------------------------------------------------------------
// More-info dialog (light)
// ---------------------------------------------------------------------------
-2
View File
@@ -1,2 +0,0 @@
// HomeAssistantAppEl registers this path; keep it inert in E2E tests.
void 0;
-1
View File
@@ -1,2 +1 @@
import "../../../../src/resources/append-ha-style";
import "./ha-test";
+9 -5
View File
@@ -247,12 +247,16 @@ export class HaTest extends HomeAssistantAppEl {
window.__mockHass = hass;
// SPA navigation
window.addEventListener("click", (e) => {
const href = isNavigationClick(e);
if (href) {
document.body.addEventListener(
"click",
(e) => {
const href = isNavigationClick(e);
if (!href) return;
e.preventDefault();
navigate(href);
}
});
},
{ capture: true }
);
this.hassConnected();
}
@@ -1,58 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { loadJS, loadModule } from "../../../../src/common/dom/load_resource";
import { loadLovelaceResourcesAndWait } from "../../../../src/panels/lovelace/common/load-resources";
import type { HomeAssistant } from "../../../../src/types";
vi.mock("../../../../src/common/dom/load_resource", () => ({
loadCSS: vi.fn(),
loadJS: vi.fn(),
loadModule: vi.fn(),
}));
const hass = {
auth: { data: { hassUrl: "http://localhost:8123" } },
} as unknown as HomeAssistant;
describe("loadLovelaceResourcesAndWait", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("logs an error naming the resource that failed to load", async () => {
const consoleError = vi
.spyOn(console, "error")
.mockImplementation(() => undefined);
const loadError = new Error("load failed");
vi.mocked(loadModule).mockImplementation(() => Promise.reject(loadError));
await loadLovelaceResourcesAndWait(
[{ id: "1", url: "/local/missing.js", type: "module" }],
hass
);
expect(consoleError).toHaveBeenCalledTimes(1);
expect(consoleError.mock.calls[0][0]).toContain(
"http://localhost:8123/local/missing.js"
);
expect(consoleError.mock.calls[0][0]).toContain("module");
// the rejection reason is forwarded, so any detail it carries is not lost
expect(consoleError.mock.calls[0][1]).toBe(loadError);
});
it("logs only once for a cached resource that failed to load", async () => {
const consoleError = vi
.spyOn(console, "error")
.mockImplementation(() => undefined);
vi.mocked(loadJS).mockImplementation((url) => Promise.reject(url));
const resources = [
{ id: "2", url: "/local/broken.js", type: "js" as const },
];
await loadLovelaceResourcesAndWait(resources, hass);
await loadLovelaceResourcesAndWait(resources, hass);
expect(loadJS).toHaveBeenCalledTimes(1);
expect(consoleError).toHaveBeenCalledTimes(1);
});
});
+121 -121
View File
@@ -4648,22 +4648,22 @@ __metadata:
languageName: node
linkType: hard
"@rsdoctor/client@npm:1.6.2":
version: 1.6.2
resolution: "@rsdoctor/client@npm:1.6.2"
checksum: 10/7b57cf3a9c6e3dc1b64b0f07969330579fd0e84963843cb3517b7c1160bc9b2fa08e709eaf835139d8a7e8c32977af8aae050d100df80daed44371ced8272642
"@rsdoctor/client@npm:1.6.1":
version: 1.6.1
resolution: "@rsdoctor/client@npm:1.6.1"
checksum: 10/aefc3a378e3ecdc9f03c21272dfb06d4eb68ca5395d83edb0a44fd816ea96b45e894de8c862e605fcb02611d306014ffef4ca09723945cf75c9a33b7062d5d4b
languageName: node
linkType: hard
"@rsdoctor/core@npm:1.6.2":
version: 1.6.2
resolution: "@rsdoctor/core@npm:1.6.2"
"@rsdoctor/core@npm:1.6.1":
version: 1.6.1
resolution: "@rsdoctor/core@npm:1.6.1"
dependencies:
"@rsbuild/plugin-check-syntax": "npm:^1.6.1"
"@rsdoctor/graph": "npm:1.6.2"
"@rsdoctor/sdk": "npm:1.6.2"
"@rsdoctor/types": "npm:1.6.2"
"@rsdoctor/utils": "npm:1.6.2"
"@rsdoctor/graph": "npm:1.6.1"
"@rsdoctor/sdk": "npm:1.6.1"
"@rsdoctor/types": "npm:1.6.1"
"@rsdoctor/utils": "npm:1.6.1"
"@rspack/resolver": "npm:^0.2.8"
browserslist-load-config: "npm:^1.0.2"
es-toolkit: "npm:^1.49.0"
@@ -4671,60 +4671,60 @@ __metadata:
fs-extra: "npm:^11.1.1"
semver: "npm:^7.8.5"
source-map: "npm:^0.7.6"
checksum: 10/803bb41042b2af07b096f2dde6383d516bb22b62705aeb9693959788e8a63df80e6d25514f1d92639a267e46e73732689abf5ba2c25679fbd80da9dd126f7ed9
checksum: 10/de5708e15e1efd67a23d753ed906e573ca6a6857428dbdfea0aff2f85f1ed3f631ff34e23bf40e955855ed878ed548c073c620a4edbd12959c20a3f9ff9c1cfd
languageName: node
linkType: hard
"@rsdoctor/graph@npm:1.6.2":
version: 1.6.2
resolution: "@rsdoctor/graph@npm:1.6.2"
"@rsdoctor/graph@npm:1.6.1":
version: 1.6.1
resolution: "@rsdoctor/graph@npm:1.6.1"
dependencies:
"@rsdoctor/types": "npm:1.6.2"
"@rsdoctor/utils": "npm:1.6.2"
"@rsdoctor/types": "npm:1.6.1"
"@rsdoctor/utils": "npm:1.6.1"
es-toolkit: "npm:^1.49.0"
path-browserify: "npm:1.0.1"
source-map: "npm:^0.7.6"
checksum: 10/f28aaedc529a6cff782f1813faa6d19d10f76ab8f529bbe2899385b5659d247f0543149e2fcd700264a00e9568605a85fde04cbf634e821f24e9bf239eb1b009
checksum: 10/065c485f11939143021c12a1acff47a2539c22e9a9d5f8501c906578213ca16b71a35d08bc0a83035529d42a7906fc098b6e3c536d895cd13ee74b5340a39e40
languageName: node
linkType: hard
"@rsdoctor/rspack-plugin@npm:1.6.2":
version: 1.6.2
resolution: "@rsdoctor/rspack-plugin@npm:1.6.2"
"@rsdoctor/rspack-plugin@npm:1.6.1":
version: 1.6.1
resolution: "@rsdoctor/rspack-plugin@npm:1.6.1"
dependencies:
"@rsdoctor/core": "npm:1.6.2"
"@rsdoctor/graph": "npm:1.6.2"
"@rsdoctor/sdk": "npm:1.6.2"
"@rsdoctor/types": "npm:1.6.2"
"@rsdoctor/utils": "npm:1.6.2"
"@rsdoctor/core": "npm:1.6.1"
"@rsdoctor/graph": "npm:1.6.1"
"@rsdoctor/sdk": "npm:1.6.1"
"@rsdoctor/types": "npm:1.6.1"
"@rsdoctor/utils": "npm:1.6.1"
peerDependencies:
"@rspack/core": "*"
peerDependenciesMeta:
"@rspack/core":
optional: true
checksum: 10/dec9f0424d14ec7ed03cd6cbc5e0b11cd74c29be12c3f79466bf8da0f317a4bdba858a58daf64e4a7c16d20a1d58433b1d2c038b62ff771c1d045bca3d71c3ad
checksum: 10/8908f4a3c2b46fdaba2c6d39dd535623ddcc411fb50d358de949142a5628a512827f5206a93ae975bd93e5de6b72d5cd71f63463c65a37ba718727567187ce8e
languageName: node
linkType: hard
"@rsdoctor/sdk@npm:1.6.2":
version: 1.6.2
resolution: "@rsdoctor/sdk@npm:1.6.2"
"@rsdoctor/sdk@npm:1.6.1":
version: 1.6.1
resolution: "@rsdoctor/sdk@npm:1.6.1"
dependencies:
"@rsdoctor/client": "npm:1.6.2"
"@rsdoctor/graph": "npm:1.6.2"
"@rsdoctor/types": "npm:1.6.2"
"@rsdoctor/utils": "npm:1.6.2"
"@rsdoctor/client": "npm:1.6.1"
"@rsdoctor/graph": "npm:1.6.1"
"@rsdoctor/types": "npm:1.6.1"
"@rsdoctor/utils": "npm:1.6.1"
launch-editor: "npm:^2.13.2"
safer-buffer: "npm:2.1.2"
socket.io: "npm:4.8.1"
tapable: "npm:2.3.3"
checksum: 10/89d5850145b490bbf3f43486176ebdd1f9eb67cdbe96a07549bb0b400ab76d85f3b6d5667be7cf8304434281f0db74fbaaab3360228eb1650f0a8ce640101208
checksum: 10/d667d62d730bed7606bcf1ed2063fb7597024e31d9108c3ba9729313764866407c216499ccb3c7102f239d7d82463e510f2b2ec71270af6a40039cbe6f876d23
languageName: node
linkType: hard
"@rsdoctor/types@npm:1.6.2":
version: 1.6.2
resolution: "@rsdoctor/types@npm:1.6.2"
"@rsdoctor/types@npm:1.6.1":
version: 1.6.1
resolution: "@rsdoctor/types@npm:1.6.1"
dependencies:
"@types/connect": "npm:3.4.38"
"@types/estree": "npm:1.0.5"
@@ -4738,16 +4738,16 @@ __metadata:
optional: true
webpack:
optional: true
checksum: 10/39328739b31f7df46b663a9f6b5ac8c1e290c6b7ce0f68d9b45a423b89a1a296d32fbee0c26f555fc40d60b356a3605909f7bbe19c77d9c6bca3603d6928aef2
checksum: 10/1c4ae6c1aa6fa525d65ab78396954d86358880959ee3fdb68247c2f3215a35e4cb2aab3836cb82f6c208c2c3834cfe0dc8bf575ba59fd8e2ff89ffc92322fcad
languageName: node
linkType: hard
"@rsdoctor/utils@npm:1.6.2":
version: 1.6.2
resolution: "@rsdoctor/utils@npm:1.6.2"
"@rsdoctor/utils@npm:1.6.1":
version: 1.6.1
resolution: "@rsdoctor/utils@npm:1.6.1"
dependencies:
"@babel/code-frame": "npm:7.26.2"
"@rsdoctor/types": "npm:1.6.2"
"@rsdoctor/types": "npm:1.6.1"
"@types/estree": "npm:1.0.5"
acorn: "npm:^8.10.0"
acorn-import-attributes: "npm:^1.9.5"
@@ -4761,83 +4761,83 @@ __metadata:
picocolors: "npm:^1.1.1"
rslog: "npm:^2.1.2"
strip-ansi: "npm:^7.2.0"
checksum: 10/fb6a147e452756018e93789e07dbe015354fc2690699755ef411748a393c7db86fdff2614cdf9ce07822434abcac3eef362aea8c63cbca9fe3f96c958fde56dc
checksum: 10/11f4e9136dd849fa05b0ab56ab0de3be3de43880d530dcbfb1835780dccad9976636f6a743852893948ae053e71fb7cd6144cac3ffac27929a87710e3981c048
languageName: node
linkType: hard
"@rspack/binding-darwin-arm64@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-darwin-arm64@npm:2.1.10"
"@rspack/binding-darwin-arm64@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-darwin-arm64@npm:2.1.9"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-darwin-x64@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-darwin-x64@npm:2.1.10"
"@rspack/binding-darwin-x64@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-darwin-x64@npm:2.1.9"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-gnu@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.10"
"@rspack/binding-linux-arm64-gnu@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.9"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-musl@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.10"
"@rspack/binding-linux-arm64-musl@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.9"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-linux-ppc64-gnu@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-linux-ppc64-gnu@npm:2.1.10"
"@rspack/binding-linux-ppc64-gnu@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-linux-ppc64-gnu@npm:2.1.9"
conditions: os=linux & cpu=ppc64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-riscv64-gnu@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.10"
"@rspack/binding-linux-riscv64-gnu@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.9"
conditions: os=linux & cpu=riscv64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-riscv64-musl@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.10"
"@rspack/binding-linux-riscv64-musl@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.9"
conditions: os=linux & cpu=riscv64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-linux-s390x-gnu@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-linux-s390x-gnu@npm:2.1.10"
"@rspack/binding-linux-s390x-gnu@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-linux-s390x-gnu@npm:2.1.9"
conditions: os=linux & cpu=s390x & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-x64-gnu@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.10"
"@rspack/binding-linux-x64-gnu@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.9"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-x64-musl@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.10"
"@rspack/binding-linux-x64-musl@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.9"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-wasm32-wasi@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.10"
"@rspack/binding-wasm32-wasi@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.9"
dependencies:
"@emnapi/core": "npm:1.11.3"
"@emnapi/runtime": "npm:1.11.3"
@@ -4846,45 +4846,45 @@ __metadata:
languageName: node
linkType: hard
"@rspack/binding-win32-arm64-msvc@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.10"
"@rspack/binding-win32-arm64-msvc@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.9"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-win32-ia32-msvc@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.10"
"@rspack/binding-win32-ia32-msvc@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.9"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@rspack/binding-win32-x64-msvc@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.10"
"@rspack/binding-win32-x64-msvc@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.9"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@rspack/binding@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/binding@npm:2.1.10"
"@rspack/binding@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/binding@npm:2.1.9"
dependencies:
"@rspack/binding-darwin-arm64": "npm:2.1.10"
"@rspack/binding-darwin-x64": "npm:2.1.10"
"@rspack/binding-linux-arm64-gnu": "npm:2.1.10"
"@rspack/binding-linux-arm64-musl": "npm:2.1.10"
"@rspack/binding-linux-ppc64-gnu": "npm:2.1.10"
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.10"
"@rspack/binding-linux-riscv64-musl": "npm:2.1.10"
"@rspack/binding-linux-s390x-gnu": "npm:2.1.10"
"@rspack/binding-linux-x64-gnu": "npm:2.1.10"
"@rspack/binding-linux-x64-musl": "npm:2.1.10"
"@rspack/binding-wasm32-wasi": "npm:2.1.10"
"@rspack/binding-win32-arm64-msvc": "npm:2.1.10"
"@rspack/binding-win32-ia32-msvc": "npm:2.1.10"
"@rspack/binding-win32-x64-msvc": "npm:2.1.10"
"@rspack/binding-darwin-arm64": "npm:2.1.9"
"@rspack/binding-darwin-x64": "npm:2.1.9"
"@rspack/binding-linux-arm64-gnu": "npm:2.1.9"
"@rspack/binding-linux-arm64-musl": "npm:2.1.9"
"@rspack/binding-linux-ppc64-gnu": "npm:2.1.9"
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.9"
"@rspack/binding-linux-riscv64-musl": "npm:2.1.9"
"@rspack/binding-linux-s390x-gnu": "npm:2.1.9"
"@rspack/binding-linux-x64-gnu": "npm:2.1.9"
"@rspack/binding-linux-x64-musl": "npm:2.1.9"
"@rspack/binding-wasm32-wasi": "npm:2.1.9"
"@rspack/binding-win32-arm64-msvc": "npm:2.1.9"
"@rspack/binding-win32-ia32-msvc": "npm:2.1.9"
"@rspack/binding-win32-x64-msvc": "npm:2.1.9"
dependenciesMeta:
"@rspack/binding-darwin-arm64":
optional: true
@@ -4914,15 +4914,15 @@ __metadata:
optional: true
"@rspack/binding-win32-x64-msvc":
optional: true
checksum: 10/b15410b23aac7bf934d271a17994fc13dd10ddf65eeaa20a84dedb2b2baf753cd411c4b56e971728c3ee054be5bafd46238e7fc9721ae883a0a8a5c5e121bd66
checksum: 10/bf3ef6f75b0fa3dafc8dc73bfb49ae31e706e466699a88e0ef35d7273800e3c241489def836041688dded31b9ec3eca61d12bc0c30f8fa18839ae879ab8d2182
languageName: node
linkType: hard
"@rspack/core@npm:2.1.10":
version: 2.1.10
resolution: "@rspack/core@npm:2.1.10"
"@rspack/core@npm:2.1.9":
version: 2.1.9
resolution: "@rspack/core@npm:2.1.9"
dependencies:
"@rspack/binding": "npm:2.1.10"
"@rspack/binding": "npm:2.1.9"
peerDependencies:
"@module-federation/runtime-tools": ^0.24.1 || ^2.0.0
"@swc/helpers": ^0.5.23
@@ -4931,7 +4931,7 @@ __metadata:
optional: true
"@swc/helpers":
optional: true
checksum: 10/bf1e7183aa5e507668cc3233562a74d2d61ddbfb7279748ae9569094f43e041ced5893fe7191de3e2d44306cc017c6db0c0c4d823bc2fd9c6fbc53fbb18a8f22
checksum: 10/81a8d5b4855ea8783d18e9e0c422e18337e50b820bf1f867d5287dc5a3643307c03e39011049f485e63e676480ba71f5fb0e7fb7f4ceea72e4bc2eec2651c13d
languageName: node
linkType: hard
@@ -9698,10 +9698,10 @@ __metadata:
languageName: node
linkType: hard
"globals@npm:17.11.0":
version: 17.11.0
resolution: "globals@npm:17.11.0"
checksum: 10/9cff5b8099b585862ab0d31e8b93dc1c3e7b59239dc58b59447a38709715751df8794ee0d28f434f8e421b01aee3cc10051e2455b5c967d960e79f882e68aa0f
"globals@npm:17.9.0":
version: 17.9.0
resolution: "globals@npm:17.9.0"
checksum: 10/8619386d449e8cbb81e242eced8bca0a42e5c3633a3a35996b8b1618beb17ff9fb33bfe703d60d83d4b7f6f8a996dede4faf02f9e966e50ff895e0cf6122ae15
languageName: node
linkType: hard
@@ -9888,10 +9888,10 @@ __metadata:
languageName: node
linkType: hard
"hls.js@npm:1.7.0":
version: 1.7.0
resolution: "hls.js@npm:1.7.0"
checksum: 10/8bb97ba86b72a3be057bd0036b01df83adfe7dc10c76a5e177b24bae3f380e19d4180e061d8140b3b826e10c9f61a7b230e10019dcb81619ab2fc41e4d292ed9
"hls.js@npm:1.6.17":
version: 1.6.17
resolution: "hls.js@npm:1.6.17"
checksum: 10/345d6c98f7f32f25f1924d34d4177f37aa9ae10c95fe6518c4ca10b295acbf174fbeba113696fbce673a1ee90461b530e101dd2b2f4d958af82577706d6e24c2
languageName: node
linkType: hard
@@ -9955,8 +9955,8 @@ __metadata:
"@octokit/rest": "npm:22.0.1"
"@playwright/test": "npm:1.62.1"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.6.2"
"@rspack/core": "npm:2.1.10"
"@rsdoctor/rspack-plugin": "npm:1.6.1"
"@rspack/core": "npm:2.1.9"
"@rspack/dev-server": "npm:2.2.0"
"@swc/helpers": "npm:0.5.23"
"@thomasloven/round-slider": "npm:0.6.0"
@@ -10012,11 +10012,11 @@ __metadata:
fuse.js: "npm:7.5.0"
generate-license-file: "npm:4.2.1"
glob: "npm:13.0.6"
globals: "npm:17.11.0"
globals: "npm:17.9.0"
gulp: "npm:5.0.1"
gulp-json-transform: "npm:0.5.0"
gulp-rename: "npm:2.1.0"
hls.js: "npm:1.7.0"
hls.js: "npm:1.6.17"
home-assistant-js-websocket: "npm:9.6.0"
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"