Landingpage add core checks before show errors (#24493)

* Check core only if supervisor or observer is unresponsive

* Improve core check

* Revert test code

* Remove unused prop

* Combine network info with core check

* Combine ping and network info

* Add 30 sec timeout before show errors

* Update landing-page/src/ha-landing-page.ts

* Assume supervisor update on failed ping

* Fix typo
This commit is contained in:
Wendelin 2025-03-06 11:33:33 +01:00 committed by Bram Kragten
parent 68960ba03d
commit 690cd47945
6 changed files with 154 additions and 129 deletions

View File

@ -22,6 +22,8 @@ import {
import { fireEvent } from "../../../src/common/dom/fire_event";
import { fileDownload } from "../../../src/util/file_download";
import { getSupervisorLogs, getSupervisorLogsFollow } from "../data/supervisor";
import { waitForSeconds } from "../../../src/common/util/wait";
import { ASSUME_CORE_START_SECONDS } from "../ha-landing-page";
const ERROR_CHECK = /^[\d\s-:]+(ERROR|CRITICAL)(.*)/gm;
declare global {
@ -216,7 +218,7 @@ class LandingPageLogs extends LitElement {
// eslint-disable-next-line no-console
console.error(err);
// fallback to observerlogs if there is a problem with supervisor
// fallback to observer logs if there is a problem with supervisor
this._loadObserverLogs();
}
}
@ -251,6 +253,9 @@ class LandingPageLogs extends LitElement {
this._scheduleObserverLogs();
} catch (err) {
// wait because there is a moment where landingpage is down and core is not up yet
await waitForSeconds(ASSUME_CORE_START_SECONDS);
// eslint-disable-next-line no-console
console.error(err);
this._error = true;

View File

@ -1,13 +1,7 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import {
type CSSResultGroup,
LitElement,
type PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type {
LandingPageKeys,
LocalizeFunc,
@ -16,34 +10,24 @@ import "../../../src/components/ha-button";
import "../../../src/components/ha-alert";
import {
ALTERNATIVE_DNS_SERVERS,
getSupervisorNetworkInfo,
pingSupervisor,
setSupervisorNetworkDns,
type NetworkInfo,
} from "../data/supervisor";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
import type { NetworkInterface } from "../../../src/data/hassio/network";
import { fireEvent } from "../../../src/common/dom/fire_event";
@customElement("landing-page-network")
class LandingPageNetwork extends LitElement {
@property({ attribute: false })
public localize!: LocalizeFunc<LandingPageKeys>;
@state() private _networkIssue = false;
@property({ attribute: false }) public networkInfo?: NetworkInfo;
@state() private _getNetworkInfoError = false;
@state() private _dnsPrimaryInterfaceNameservers?: string;
@state() private _dnsPrimaryInterface?: string;
@property({ type: Boolean }) public error = false;
protected render() {
if (!this._networkIssue && !this._getNetworkInfoError) {
return nothing;
}
if (this._getNetworkInfoError) {
if (this.error) {
return html`
<ha-alert alert-type="error">
<p>${this.localize("network_issue.error_get_network_info")}</p>
@ -51,6 +35,16 @@ class LandingPageNetwork extends LitElement {
`;
}
let dnsPrimaryInterfaceNameservers: string | undefined;
const primaryInterface = this._getPrimaryInterface(
this.networkInfo?.interfaces
);
if (primaryInterface) {
dnsPrimaryInterfaceNameservers =
this._getPrimaryNameservers(primaryInterface);
}
return html`
<ha-alert
alert-type="warning"
@ -58,11 +52,11 @@ class LandingPageNetwork extends LitElement {
>
<p>
${this.localize("network_issue.description", {
dns: this._dnsPrimaryInterfaceNameservers || "?",
dns: dnsPrimaryInterfaceNameservers || "?",
})}
</p>
<p>${this.localize("network_issue.resolve_different")}</p>
${!this._dnsPrimaryInterfaceNameservers
${!dnsPrimaryInterfaceNameservers
? html`
<p>
<b>${this.localize("network_issue.no_primary_interface")} </b>
@ -74,7 +68,7 @@ class LandingPageNetwork extends LitElement {
({ translationKey }, key) =>
html`<ha-button
.index=${key}
.disabled=${!this._dnsPrimaryInterfaceNameservers}
.disabled=${!dnsPrimaryInterfaceNameservers}
@click=${this._setDns}
>${this.localize(translationKey)}</ha-button
>`
@ -84,97 +78,40 @@ class LandingPageNetwork extends LitElement {
`;
}
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
this._pingSupervisor();
}
private _getPrimaryInterface = memoizeOne((interfaces?: NetworkInterface[]) =>
interfaces?.find((intf) => intf.primary && intf.enabled)
);
private _schedulePingSupervisor() {
setTimeout(
() => this._pingSupervisor(),
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
);
}
private async _pingSupervisor() {
try {
const response = await pingSupervisor();
if (!response.ok) {
throw new Error("Failed to ping supervisor, assume update in progress");
}
this._fetchSupervisorInfo();
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
this._schedulePingSupervisor();
}
}
private _scheduleFetchSupervisorInfo() {
setTimeout(
() => this._fetchSupervisorInfo(),
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
);
}
private async _fetchSupervisorInfo() {
let data;
try {
const response = await getSupervisorNetworkInfo();
if (!response.ok) {
throw new Error("Failed to fetch network info");
}
({ data } = await response.json());
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
this._getNetworkInfoError = true;
this._dnsPrimaryInterfaceNameservers = undefined;
this._dnsPrimaryInterface = undefined;
return;
}
this._getNetworkInfoError = false;
const primaryInterface = data.interfaces.find(
(intf) => intf.primary && intf.enabled
);
if (primaryInterface) {
this._dnsPrimaryInterfaceNameservers = [
private _getPrimaryNameservers = memoizeOne(
(primaryInterface: NetworkInterface) =>
[
...(primaryInterface.ipv4?.nameservers || []),
...(primaryInterface.ipv6?.nameservers || []),
].join(", ");
this._dnsPrimaryInterface = primaryInterface.interface;
} else {
this._dnsPrimaryInterfaceNameservers = undefined;
this._dnsPrimaryInterface = undefined;
}
if (!data.host_internet) {
this._networkIssue = true;
} else {
this._networkIssue = false;
}
fireEvent(this, "value-changed", {
value: this._networkIssue,
});
this._scheduleFetchSupervisorInfo();
}
].join(", ")
);
private async _setDns(ev) {
const primaryInterface = this._getPrimaryInterface(
this.networkInfo?.interfaces
);
const index = ev.target?.index;
try {
const dnsPrimaryInterface = primaryInterface?.interface;
if (!dnsPrimaryInterface) {
throw new Error("No primary interface found");
}
const response = await setSupervisorNetworkDns(
index,
this._dnsPrimaryInterface!
dnsPrimaryInterface
);
if (!response.ok) {
throw new Error("Failed to set DNS");
}
this._networkIssue = false;
// notify landing page to trigger a network info reload
fireEvent(this, "dns-set");
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
@ -205,4 +142,7 @@ declare global {
interface HTMLElementTagNameMap {
"landing-page-network": LandingPageNetwork;
}
interface HASSDomEvents {
"dns-set": undefined;
}
}

View File

@ -1,4 +1,17 @@
import type { LandingPageKeys } from "../../../src/common/translations/localize";
import type { HassioResponse } from "../../../src/data/hassio/common";
import type {
DockerNetwork,
NetworkInterface,
} from "../../../src/data/hassio/network";
import { handleFetchPromise } from "../../../src/util/hass-call-api";
export interface NetworkInfo {
interfaces: NetworkInterface[];
docker: DockerNetwork;
host_internet: boolean;
supervisor_internet: boolean;
}
export const ALTERNATIVE_DNS_SERVERS: {
ipv4: string[];
@ -37,8 +50,11 @@ export async function pingSupervisor() {
return fetch("/supervisor-api/supervisor/ping");
}
export async function getSupervisorNetworkInfo() {
return fetch("/supervisor-api/network/info");
export async function getSupervisorNetworkInfo(): Promise<NetworkInfo> {
const responseData = await handleFetchPromise<HassioResponse<NetworkInfo>>(
fetch("/supervisor-api/network/info")
);
return responseData?.data;
}
export const setSupervisorNetworkDns = async (

View File

@ -10,36 +10,56 @@ import { extractSearchParam } from "../../src/common/url/search-params";
import { onBoardingStyles } from "../../src/onboarding/styles";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { LandingPageBaseElement } from "./landing-page-base-element";
import {
getSupervisorNetworkInfo,
pingSupervisor,
type NetworkInfo,
} from "./data/supervisor";
const SCHEDULE_CORE_CHECK_SECONDS = 5;
export const ASSUME_CORE_START_SECONDS = 30;
const SCHEDULE_CORE_CHECK_SECONDS = 1;
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
@customElement("ha-landing-page")
class HaLandingPage extends LandingPageBaseElement {
@property({ attribute: false }) public translationFragment = "landing-page";
@state() private _networkIssue = false;
@state() private _supervisorError = false;
@state() private _networkInfo?: NetworkInfo;
@state() private _coreStatusChecked = false;
@state() private _networkInfoError = false;
@state() private _coreCheckActive = false;
private _mobileApp =
extractSearchParam("redirect_uri") === "homeassistant://auth-callback";
render() {
const networkIssue = this._networkInfo && !this._networkInfo.host_internet;
return html`
<ha-card>
<div class="card-content">
<h1>${this.localize("header")}</h1>
${!this._networkIssue && !this._supervisorError
${!networkIssue && !this._supervisorError
? html`
<p>${this.localize("subheader")}</p>
<mwc-linear-progress indeterminate></mwc-linear-progress>
`
: nothing}
<landing-page-network
@value-changed=${this._networkInfoChanged}
.localize=${this.localize}
></landing-page-network>
${networkIssue || this._networkInfoError
? html`
<landing-page-network
.localize=${this.localize}
.networkInfo=${this._networkInfo}
.error=${this._networkInfoError}
@dns-set=${this._fetchSupervisorInfo}
></landing-page-network>
`
: nothing}
${this._supervisorError
? html`
<ha-alert
@ -88,24 +108,66 @@ class HaLandingPage extends LandingPageBaseElement {
}
import("../../src/components/ha-language-picker");
this._scheduleCoreCheck();
this._fetchSupervisorInfo(true);
}
private _scheduleCoreCheck() {
private _scheduleFetchSupervisorInfo() {
setTimeout(
() => this._checkCoreAvailability(),
SCHEDULE_CORE_CHECK_SECONDS * 1000
() => this._fetchSupervisorInfo(true),
// on assumed core start check every second, otherwise every 5 seconds
(this._coreCheckActive
? SCHEDULE_CORE_CHECK_SECONDS
: SCHEDULE_FETCH_NETWORK_INFO_SECONDS) * 1000
);
}
private _scheduleTurnOffCoreCheck() {
setTimeout(() => {
this._coreCheckActive = false;
}, ASSUME_CORE_START_SECONDS * 1000);
}
private async _fetchSupervisorInfo(schedule = false) {
try {
const response = await pingSupervisor();
if (!response.ok) {
throw new Error("ping-failed");
}
this._networkInfo = await getSupervisorNetworkInfo();
this._networkInfoError = false;
this._coreStatusChecked = false;
} catch (err: any) {
if (!this._coreStatusChecked) {
// wait before show errors, because we assume that core is starting
this._coreCheckActive = true;
this._scheduleTurnOffCoreCheck();
}
await this._checkCoreAvailability();
// assume supervisor update if ping fails -> don't show an error
if (!this._coreCheckActive && err.message !== "ping-failed") {
// eslint-disable-next-line no-console
console.error(err);
this._networkInfoError = true;
}
}
if (schedule) {
this._scheduleFetchSupervisorInfo();
}
}
private async _checkCoreAvailability() {
try {
const response = await fetch("/manifest.json");
if (response.ok) {
location.reload();
} else {
throw new Error("Failed to fetch manifest");
}
} finally {
this._scheduleCoreCheck();
} catch (_err) {
this._coreStatusChecked = true;
}
}
@ -113,10 +175,6 @@ class HaLandingPage extends LandingPageBaseElement {
this._supervisorError = true;
}
private _networkInfoChanged(ev: CustomEvent) {
this._networkIssue = ev.detail.value;
}
private _languageChanged(ev: CustomEvent) {
const language = ev.detail.value;
if (language !== this.language && language) {

6
src/common/util/wait.ts Normal file
View File

@ -0,0 +1,6 @@
export const waitForMs = (ms: number) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});
export const waitForSeconds = (seconds: number) => waitForMs(seconds * 1000);

View File

@ -21,7 +21,7 @@ export interface NetworkInterface {
wifi?: Partial<WifiConfiguration> | null;
}
interface DockerNetwork {
export interface DockerNetwork {
address: string;
dns: string;
gateway: string;