Improve error handling in backup status banner (#23604)

* Improve error handling in backup status banner

* Fix completion

* Fix loading

* Check attempt and completion date first
pull/23613/head
Paul Bottein 2025-01-06 17:30:37 +01:00 committed by GitHub
parent 2a2b6d33b8
commit ffff7970f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 99 additions and 42 deletions

View File

@ -31,14 +31,24 @@ class HaBackupOverviewBackups extends LitElement {
@property({ type: Boolean }) public fetching = false; @property({ type: Boolean }) public fetching = false;
private _lastSuccessfulBackup = memoizeOne((backups: BackupContent[]) => { private _sortedBackups = memoizeOne((backups: BackupContent[]) =>
const sortedBackups = backups backups
.filter((backup) => backup.with_automatic_settings) .filter((backup) => backup.with_automatic_settings)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
);
private _lastBackup = memoizeOne((backups: BackupContent[]) => {
const sortedBackups = this._sortedBackups(backups);
return sortedBackups[0] as BackupContent | undefined; return sortedBackups[0] as BackupContent | undefined;
}); });
private _lastUploadedBackup = memoizeOne((backups: BackupContent[]) => {
const sortedBackups = this._sortedBackups(backups);
return sortedBackups.find(
(backup) => backup.failed_agent_ids?.length === 0
);
});
private _nextBackupDescription(schedule: BackupScheduleState) { private _nextBackupDescription(schedule: BackupScheduleState) {
const time = getFormattedBackupTime(this.hass.locale, this.hass.config); const time = getFormattedBackupTime(this.hass.locale, this.hass.config);
@ -65,6 +75,8 @@ class HaBackupOverviewBackups extends LitElement {
} }
protected render() { protected render() {
const now = new Date();
if (this.fetching) { if (this.fetching) {
return html` return html`
<ha-backup-summary-card heading="Loading backups" status="loading"> <ha-backup-summary-card heading="Loading backups" status="loading">
@ -82,24 +94,28 @@ class HaBackupOverviewBackups extends LitElement {
`; `;
} }
const lastSuccessfulBackup = this._lastSuccessfulBackup(this.backups); const lastBackup = this._lastBackup(this.backups);
const lastAttempt = this.config.last_attempted_automatic_backup const nextBackupDescription = this._nextBackupDescription(
this.config.schedule.state
);
const lastAttemptDate = this.config.last_attempted_automatic_backup
? new Date(this.config.last_attempted_automatic_backup) ? new Date(this.config.last_attempted_automatic_backup)
: undefined; : new Date(0);
const lastCompletedBackupDate = this.config.last_completed_automatic_backup const lastCompletedDate = this.config.last_completed_automatic_backup
? new Date(this.config.last_completed_automatic_backup) ? new Date(this.config.last_completed_automatic_backup)
: undefined; : new Date(0);
const now = new Date(); // If last attempt is after last completed backup, show error
if (lastAttemptDate > lastCompletedDate) {
const description = `The last automatic backup triggered ${relativeTime(lastAttemptDate, this.hass.locale, now, true)} wasn't successful.`;
const lastUploadedBackup = this._lastUploadedBackup(this.backups);
const secondaryDescription = lastUploadedBackup
? `Last successful backup ${relativeTime(new Date(lastUploadedBackup.date), this.hass.locale, now, true)} and stored in ${lastUploadedBackup.agent_ids?.length} locations.`
: nextBackupDescription;
const lastBackupDescription = lastSuccessfulBackup
? `Last successful backup ${relativeTime(new Date(lastSuccessfulBackup.date), this.hass.locale, now, true)} and stored in ${lastSuccessfulBackup.agent_ids?.length} locations.`
: "You have no successful backups.";
if (lastAttempt && lastAttempt > (lastCompletedBackupDate || 0)) {
const lastAttemptDescription = `The last automatic backup triggered ${relativeTime(lastAttempt, this.hass.locale, now, true)} wasn't successful.`;
return html` return html`
<ha-backup-summary-card <ha-backup-summary-card
heading="Last automatic backup failed" heading="Last automatic backup failed"
@ -108,29 +124,30 @@ class HaBackupOverviewBackups extends LitElement {
<ha-md-list> <ha-md-list>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${lastAttemptDescription}</span> <span slot="headline">${description}</span>
</ha-md-list-item> </ha-md-list-item>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">${lastBackupDescription}</span> <span slot="headline">${secondaryDescription}</span>
</ha-md-list-item> </ha-md-list-item>
</ha-md-list> </ha-md-list>
</ha-backup-summary-card> </ha-backup-summary-card>
`; `;
} }
const nextBackupDescription = this._nextBackupDescription( // If no backups yet, show warning
this.config.schedule.state if (!lastBackup) {
); const description = "You have no automatic backups yet.";
if (!lastSuccessfulBackup) {
return html` return html`
<ha-backup-summary-card <ha-backup-summary-card
heading="No automatic backup available" heading="No automatic backup available"
description="You have no automatic backups yet."
status="warning" status="warning"
> >
<ha-md-list> <ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${description}</span>
</ha-md-list-item>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">${nextBackupDescription}</span> <span slot="headline">${nextBackupDescription}</span>
@ -140,10 +157,41 @@ class HaBackupOverviewBackups extends LitElement {
`; `;
} }
const lastBackupDate = new Date(lastBackup.date);
// If last backup
if (lastBackup.failed_agent_ids?.length) {
const description = `The last automatic backup created ${relativeTime(lastBackupDate, this.hass.locale, now, true)} wasn't stored in all locations.`;
const lastUploadedBackup = this._lastUploadedBackup(this.backups);
const secondaryDescription = lastUploadedBackup
? `Last successful backup ${relativeTime(new Date(lastUploadedBackup.date), this.hass.locale, now, true)} and stored in ${lastUploadedBackup.agent_ids?.length} locations.`
: nextBackupDescription;
return html`
<ha-backup-summary-card
heading="Last automatic backup failed"
status="error"
>
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${description}</span>
</ha-md-list-item>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">${secondaryDescription}</span>
</ha-md-list-item>
</ha-md-list>
</ha-backup-summary-card>
`;
}
const description = `Last successful backup ${relativeTime(lastBackupDate, this.hass.locale, now, true)} and stored in ${lastBackup.agent_ids?.length} locations.`;
const numberOfDays = differenceInDays( const numberOfDays = differenceInDays(
// Subtract a few hours to avoid showing as overdue if it's just a few hours (e.g. daylight saving) // Subtract a few hours to avoid showing as overdue if it's just a few hours (e.g. daylight saving)
addHours(now, -OVERDUE_MARGIN_HOURS), addHours(now, -OVERDUE_MARGIN_HOURS),
new Date(lastSuccessfulBackup.date) lastBackupDate
); );
const isOverdue = const isOverdue =
@ -160,7 +208,7 @@ class HaBackupOverviewBackups extends LitElement {
<ha-md-list> <ha-md-list>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${lastBackupDescription}</span> <span slot="headline">${description}</span>
</ha-md-list-item> </ha-md-list-item>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
@ -170,12 +218,13 @@ class HaBackupOverviewBackups extends LitElement {
</ha-backup-summary-card> </ha-backup-summary-card>
`; `;
} }
return html` return html`
<ha-backup-summary-card heading=${`Backed up`} status="success"> <ha-backup-summary-card heading=${`Backed up`} status="success">
<ha-md-list> <ha-md-list>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${lastBackupDescription}</span> <span slot="headline">${description}</span>
</ha-md-list-item> </ha-md-list-item>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>

View File

@ -1,6 +1,12 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import type { BackupConfig, BackupContent } from "../../../data/backup";
import {
compareAgents,
fetchBackupConfig,
fetchBackupInfo,
} from "../../../data/backup";
import type { ManagerStateEvent } from "../../../data/backup_manager"; import type { ManagerStateEvent } from "../../../data/backup_manager";
import { import {
DEFAULT_MANAGER_STATE, DEFAULT_MANAGER_STATE,
@ -15,12 +21,6 @@ import type { HomeAssistant } from "../../../types";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import "./ha-config-backup-backups"; import "./ha-config-backup-backups";
import "./ha-config-backup-overview"; import "./ha-config-backup-overview";
import type { BackupConfig, BackupContent } from "../../../data/backup";
import {
compareAgents,
fetchBackupConfig,
fetchBackupInfo,
} from "../../../data/backup";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@ -47,13 +47,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this._fetching = true; this._fetchAll();
Promise.all([this._fetchBackupInfo(), this._fetchBackupConfig()]).finally(
() => {
this._fetching = false;
}
);
this.addEventListener("ha-refresh-backup-info", () => { this.addEventListener("ha-refresh-backup-info", () => {
this._fetchBackupInfo(); this._fetchBackupInfo();
}); });
@ -62,6 +56,15 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
}); });
} }
private _fetchAll() {
this._fetching = true;
Promise.all([this._fetchBackupInfo(), this._fetchBackupConfig()]).finally(
() => {
this._fetching = false;
}
);
}
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (this.hasUpdated) { if (this.hasUpdated) {
@ -128,11 +131,16 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
public hassSubscribe(): Promise<UnsubscribeFunc>[] { public hassSubscribe(): Promise<UnsubscribeFunc>[] {
return [ return [
subscribeBackupEvents(this.hass!, (event) => { subscribeBackupEvents(this.hass!, (event) => {
const curState = this._manager.manager_state;
this._manager = event; this._manager = event;
if ("state" in event) { if (
if (event.state === "completed" || event.state === "failed") { event.manager_state === "idle" &&
this._fetchBackupInfo(); event.manager_state !== curState
) {
this._fetchAll();
} }
if ("state" in event) {
if (event.state === "failed") { if (event.state === "failed") {
let message = ""; let message = "";
switch (this._manager.manager_state) { switch (this._manager.manager_state) {