mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
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:
parent
cdfe4b53bf
commit
c97916bea4
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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
6
src/common/util/wait.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const waitForMs = (ms: number) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
export const waitForSeconds = (seconds: number) => waitForMs(seconds * 1000);
|
@ -21,7 +21,7 @@ export interface NetworkInterface {
|
||||
wifi?: Partial<WifiConfiguration> | null;
|
||||
}
|
||||
|
||||
interface DockerNetwork {
|
||||
export interface DockerNetwork {
|
||||
address: string;
|
||||
dns: string;
|
||||
gateway: string;
|
||||
|
Loading…
x
Reference in New Issue
Block a user