diff --git a/landing-page/src/components/landing-page-logs.ts b/landing-page/src/components/landing-page-logs.ts index f4745fd2ebac..0b0c3e7c8159 100644 --- a/landing-page/src/components/landing-page-logs.ts +++ b/landing-page/src/components/landing-page-logs.ts @@ -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; diff --git a/landing-page/src/components/landing-page-network.ts b/landing-page/src/components/landing-page-network.ts index bf5a7e911564..bea2336e48d6 100644 --- a/landing-page/src/components/landing-page-network.ts +++ b/landing-page/src/components/landing-page-network.ts @@ -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; - @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`

${this.localize("network_issue.error_get_network_info")}

@@ -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`

${this.localize("network_issue.description", { - dns: this._dnsPrimaryInterfaceNameservers || "?", + dns: dnsPrimaryInterfaceNameservers || "?", })}

${this.localize("network_issue.resolve_different")}

- ${!this._dnsPrimaryInterfaceNameservers + ${!dnsPrimaryInterfaceNameservers ? html`

${this.localize("network_issue.no_primary_interface")} @@ -74,7 +68,7 @@ class LandingPageNetwork extends LitElement { ({ translationKey }, key) => html`${this.localize(translationKey)}` @@ -84,97 +78,40 @@ class LandingPageNetwork extends LitElement { `; } - protected firstUpdated(_changedProperties: PropertyValues): void { - super.firstUpdated(_changedProperties); - this._pingSupervisor(); - } - - private _schedulePingSupervisor() { - setTimeout( - () => this._pingSupervisor(), - SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000 - ); - } + private _getPrimaryInterface = memoizeOne((interfaces?: NetworkInterface[]) => + interfaces?.find((intf) => intf.primary && intf.enabled) + ); - 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; + } } diff --git a/landing-page/src/data/supervisor.ts b/landing-page/src/data/supervisor.ts index 41c28297e051..1f0fcff24d35 100644 --- a/landing-page/src/data/supervisor.ts +++ b/landing-page/src/data/supervisor.ts @@ -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 { + const responseData = await handleFetchPromise>( + fetch("/supervisor-api/network/info") + ); + return responseData?.data; } export const setSupervisorNetworkDns = async ( diff --git a/landing-page/src/ha-landing-page.ts b/landing-page/src/ha-landing-page.ts index 83faa88675e3..e4c55d6311d0 100644 --- a/landing-page/src/ha-landing-page.ts +++ b/landing-page/src/ha-landing-page.ts @@ -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`

${this.localize("header")}

- ${!this._networkIssue && !this._supervisorError + ${!networkIssue && !this._supervisorError ? html`

${this.localize("subheader")}

` : nothing} - - + ${networkIssue || this._networkInfoError + ? html` + + ` + : nothing} ${this._supervisorError ? html` 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) { diff --git a/src/common/util/wait.ts b/src/common/util/wait.ts new file mode 100644 index 000000000000..7ed8544d666e --- /dev/null +++ b/src/common/util/wait.ts @@ -0,0 +1,6 @@ +export const waitForMs = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +export const waitForSeconds = (seconds: number) => waitForMs(seconds * 1000); diff --git a/src/data/hassio/network.ts b/src/data/hassio/network.ts index 0107e9b03327..40500360afea 100644 --- a/src/data/hassio/network.ts +++ b/src/data/hassio/network.ts @@ -21,7 +21,7 @@ export interface NetworkInterface { wifi?: Partial | null; } -interface DockerNetwork { +export interface DockerNetwork { address: string; dns: string; gateway: string;