diff --git a/src/data/backup.ts b/src/data/backup.ts index d37c50f431..12261eb810 100644 --- a/src/data/backup.ts +++ b/src/data/backup.ts @@ -103,12 +103,20 @@ export interface BackupContentAgent { protected: boolean; } +export interface AddonInfo { + name: string | null; + slug: string; + version: string | null; +} + export interface BackupContent { backup_id: string; date: string; name: string; agents: Record; failed_agent_ids?: string[]; + failed_addons?: AddonInfo[]; + failed_folders?: string[]; extra_metadata?: { "supervisor.addon_update"?: string; }; diff --git a/src/panels/config/backup/components/ha-backup-details-summary.ts b/src/panels/config/backup/components/ha-backup-details-summary.ts index d855696bbe..89f69bc5df 100644 --- a/src/panels/config/backup/components/ha-backup-details-summary.ts +++ b/src/panels/config/backup/components/ha-backup-details-summary.ts @@ -1,15 +1,17 @@ -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; +import { formatDateTime } from "../../../../common/datetime/format_date_time"; +import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; +import "../../../../components/ha-alert"; import "../../../../components/ha-card"; import "../../../../components/ha-md-list"; import "../../../../components/ha-md-list-item"; -import type { HomeAssistant } from "../../../../types"; -import { formatDateTime } from "../../../../common/datetime/format_date_time"; import { computeBackupSize, computeBackupType, type BackupContentExtended, } from "../../../../data/backup"; +import type { HomeAssistant } from "../../../../types"; import { bytesToString } from "../../../../util/bytes-to-string"; @customElement("ha-backup-details-summary") @@ -28,12 +30,35 @@ class HaBackupDetailsSummary extends LitElement { this.hass.config ); + const errors: { title: string; items: string[] }[] = []; + if (this.backup.failed_addons?.length) { + errors.push({ + title: this.hass.localize( + "ui.panel.config.backup.details.summary.error.failed_addons" + ), + items: this.backup.failed_addons.map( + (addon) => `${addon.name || addon.slug} (${addon.version})` + ), + }); + } + if (this.backup.failed_folders?.length) { + errors.push({ + title: this.hass.localize( + "ui.panel.config.backup.details.summary.error.failed_folders" + ), + items: this.backup.failed_folders.map((folder) => + this._localizeFolder(folder) + ), + }); + } + return html`
${this.hass.localize("ui.panel.config.backup.details.summary.title")}
+ ${errors.length ? this._renderErrorSummary(errors) : nothing} @@ -69,6 +94,45 @@ class HaBackupDetailsSummary extends LitElement { `; } + private _renderErrorSummary(errors: { title: string; items: string[] }[]) { + return html` + + ${errors.map( + ({ title, items }) => html` +
+ ${title}: +
    + ${items.map((item) => html`
  • ${item}
  • `)} +
+ ` + )} +
+ `; + } + + private _localizeFolder(folder: string): string { + switch (folder) { + case "media": + return this.hass.localize(`ui.panel.config.backup.data_picker.media`); + case "share": + return this.hass.localize( + `ui.panel.config.backup.data_picker.share_folder` + ); + case "ssl": + return this.hass.localize(`ui.panel.config.backup.data_picker.ssl`); + case "addons/local": + return this.hass.localize( + `ui.panel.config.backup.data_picker.local_addons` + ); + } + return capitalizeFirstLetter(folder); + } + static styles = css` :host { max-width: 690px; diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts b/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts index b238ad9767..7488440e5d 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts @@ -4,13 +4,18 @@ import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { + formatDate, + formatDateWeekday, +} from "../../../../../common/datetime/format_date"; import { relativeTime } from "../../../../../common/datetime/relative_time"; +import type { LocalizeKeys } from "../../../../../common/translations/localize"; import "../../../../../components/ha-button"; import "../../../../../components/ha-card"; +import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-md-list"; import "../../../../../components/ha-md-list-item"; import "../../../../../components/ha-svg-icon"; -import "../../../../../components/ha-icon-button"; import type { BackupConfig, BackupContent } from "../../../../../data/backup"; import { BackupScheduleRecurrence, @@ -18,12 +23,8 @@ import { } from "../../../../../data/backup"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; -import "../ha-backup-summary-card"; -import { - formatDate, - formatDateWeekday, -} from "../../../../../common/datetime/format_date"; import { showAlertDialog } from "../../../../lovelace/custom-card-helpers"; +import "../ha-backup-summary-card"; const OVERDUE_MARGIN_HOURS = 3; @@ -55,29 +56,57 @@ class HaBackupOverviewBackups extends LitElement { ); }); + private _renderSummaryCard( + heading: string, + status: "error" | "info" | "warning" | "loading" | "success", + headline: string | null, + description?: string | null, + lastCompletedDate?: Date + ) { + return html` + + + + + ${headline} + + ${description || description === null + ? html` + + ${description} + + ${lastCompletedDate + ? html` ` + : nothing} + ` + : nothing} + + + `; + } + protected render() { const now = new Date(); if (this.fetching) { - return html` - - - - - - - - - - - - - `; + return this._renderSummaryCard( + this.hass.localize("ui.panel.config.backup.overview.summary.loading"), + "loading", + null, + null + ); } const lastBackup = this._lastBackup(this.backups); @@ -137,146 +166,112 @@ class HaBackupOverviewBackups extends LitElement { if (lastAttemptDate > lastCompletedDate) { const lastUploadedBackup = this._lastUploadedBackup(this.backups); - return html` - - - - - - ${this.hass.localize( - "ui.panel.config.backup.overview.summary.last_backup_failed_description", - { - relative_time: relativeTime( - lastAttemptDate, - this.hass.locale, - now, - true - ), - } - )} - - - ${lastUploadedBackup || nextBackupDescription - ? html` - - - - ${lastUploadedBackup - ? this.hass.localize( - "ui.panel.config.backup.overview.summary.last_successful_backup_description", - { - relative_time: relativeTime( - new Date(lastUploadedBackup.date), - this.hass.locale, - now, - true - ), - count: Object.keys(lastUploadedBackup.agents) - .length, - } - ) - : nextBackupDescription} - - - ` - : nothing} - - - `; + return this._renderSummaryCard( + this.hass.localize( + "ui.panel.config.backup.overview.summary.last_backup_failed_heading" + ), + "error", + this.hass.localize( + "ui.panel.config.backup.overview.summary.last_backup_failed_description", + { + relative_time: relativeTime( + lastAttemptDate, + this.hass.locale, + now, + true + ), + } + ), + lastUploadedBackup || nextBackupDescription + ? lastUploadedBackup + ? this.hass.localize( + "ui.panel.config.backup.overview.summary.last_successful_backup_description", + { + relative_time: relativeTime( + new Date(lastUploadedBackup.date), + this.hass.locale, + now, + true + ), + count: Object.keys(lastUploadedBackup.agents).length, + } + ) + : nextBackupDescription + : undefined + ); } // If no backups yet, show warning if (!lastBackup) { - return html` - - - - - - ${this.hass.localize( - "ui.panel.config.backup.overview.summary.no_backup_description" - )} - - - ${this._renderNextBackupDescription( - nextBackupDescription, - lastCompletedDate, - showAdditionalBackupDescription - )} - - - `; + return this._renderSummaryCard( + this.hass.localize( + "ui.panel.config.backup.overview.summary.no_backup_heading" + ), + "warning", + this.hass.localize( + "ui.panel.config.backup.overview.summary.no_backup_description" + ), + nextBackupDescription, + showAdditionalBackupDescription ? lastCompletedDate : undefined + ); } const lastBackupDate = new Date(lastBackup.date); - // If last backup - if (lastBackup.failed_agent_ids?.length) { + // if parts of the last backup failed + if ( + lastBackup.failed_agent_ids?.length || + lastBackup.failed_addons?.length || + lastBackup.failed_folders?.length + ) { const lastUploadedBackup = this._lastUploadedBackup(this.backups); - return html` - - - - - - ${this.hass.localize( - "ui.panel.config.backup.overview.summary.last_backup_failed_locations_description", - { - relative_time: relativeTime( - lastAttemptDate, - this.hass.locale, - now, - true - ), - } - )} - - + const failedTypes: string[] = []; - ${lastUploadedBackup || nextBackupDescription - ? html` - - - ${lastUploadedBackup - ? this.hass.localize( - "ui.panel.config.backup.overview.summary.last_successful_backup_description", - { - relative_time: relativeTime( - new Date(lastUploadedBackup.date), - this.hass.locale, - now, - true - ), - count: Object.keys(lastUploadedBackup.agents) - .length, - } - ) - : nextBackupDescription} - - ` - : nothing} - - - `; + if (lastBackup.failed_agent_ids?.length) { + failedTypes.push("locations"); + } + if (lastBackup.failed_addons?.length) { + failedTypes.push("addons"); + } + if (lastBackup.failed_folders?.length) { + failedTypes.push("folders"); + } + + const type = failedTypes.join("_"); + + return this._renderSummaryCard( + this.hass.localize( + "ui.panel.config.backup.overview.summary.last_backup_failed_heading" + ), + "error", + this.hass.localize( + `ui.panel.config.backup.overview.summary.last_backup_failed_${type}_description` as LocalizeKeys, + { + relative_time: relativeTime( + lastAttemptDate, + this.hass.locale, + now, + true + ), + } + ), + lastUploadedBackup + ? this.hass.localize( + "ui.panel.config.backup.overview.summary.last_successful_backup_description", + { + relative_time: relativeTime( + new Date(lastUploadedBackup.date), + this.hass.locale, + now, + true + ), + count: Object.keys(lastUploadedBackup.agents).length, + } + ) + : nextBackupDescription, + showAdditionalBackupDescription ? lastCompletedDate : undefined + ); } const lastSuccessfulBackupDescription = this.hass.localize( @@ -303,67 +298,33 @@ class HaBackupOverviewBackups extends LitElement { this.config.schedule.recurrence === BackupScheduleRecurrence.DAILY) || numberOfDays >= 7; - return html` - - - - - ${lastSuccessfulBackupDescription} - - ${this._renderNextBackupDescription( - nextBackupDescription, - lastCompletedDate, - showAdditionalBackupDescription - )} - - - `; + return this._renderSummaryCard( + this.hass.localize( + `ui.panel.config.backup.overview.summary.${isOverdue ? "backup_too_old_heading" : "backup_success_heading"}`, + { count: numberOfDays } + ), + isOverdue ? "warning" : "success", + lastSuccessfulBackupDescription, + nextBackupDescription, + showAdditionalBackupDescription ? lastCompletedDate : undefined + ); } - private _renderNextBackupDescription( - nextBackupDescription: string, - lastCompletedDate: Date, - showTip = false - ) { - // handle edge case that there is an additional backup scheduled - const openAdditionalBackupDescriptionDialog = showTip - ? () => { - showAlertDialog(this, { - text: this.hass.localize( - "ui.panel.config.backup.overview.summary.additional_backup_description", - { - date: formatDate( - lastCompletedDate, - this.hass.locale, - this.hass.config - ), - } + private _createAdditionalBackupDescription = + (lastCompletedDate: Date) => () => { + showAlertDialog(this, { + text: this.hass.localize( + "ui.panel.config.backup.overview.summary.additional_backup_description", + { + date: formatDate( + lastCompletedDate, + this.hass.locale, + this.hass.config ), - }); - } - : undefined; - - return nextBackupDescription - ? html` - - ${nextBackupDescription} - - ${showTip - ? html` ` - : nothing} - ` - : nothing; - } + } + ), + }); + }; static get styles(): CSSResultGroup { return [ diff --git a/src/translations/en.json b/src/translations/en.json index ff6bd50f71..43ca921f43 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2654,6 +2654,12 @@ "last_backup_failed_heading": "Last automatic backup failed", "last_backup_failed_description": "The last automatic backup triggered {relative_time} wasn't successful.", "last_backup_failed_locations_description": "The last automatic backup created {relative_time} wasn't stored in all locations.", + "last_backup_failed_addons_description": "The last automatic backup created {relative_time} was not able to backup all add-ons.", + "last_backup_failed_folders_description": "The last automatic backup created {relative_time} was not able to backup all folders.", + "last_backup_failed_addons_folders_description": "The last automatic backup created {relative_time} wasn't able to backup all add-ons and folders.", + "last_backup_failed_locations_addons_description": "The last automatic backup created {relative_time} wasn't able to backup all add-ons and wasn't stored in all locations.", + "last_backup_failed_locations_folders_description": "The last automatic backup created {relative_time} wasn't able to backup all folders and wasn't stored in all locations.", + "last_backup_failed_locations_addons_folders_description": "The last automatic backup created {relative_time} wasn't able to backup all add-ons and folders and wasn't stored in all locations.", "last_successful_backup_description": "Last successful automatic backup {relative_time} and stored in {count} {count, plural,\n one {location}\n other {locations}\n}.", "no_backup_heading": "No automatic backup available", "no_backup_description": "You have no automatic backups yet.", @@ -2769,7 +2775,13 @@ "summary": { "title": "Backup", "size": "Size", - "created": "Created" + "created": "Created", + "error": { + "title": "This backup was not created successfully. Some data is missing.", + "failed_locations": "Failed locations", + "failed_addons": "Failed add-ons", + "failed_folders": "Failed folders" + } }, "restore": { "title": "Select what to restore",