Use failed add-ons and folders of backup (#25548)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Wendelin 2025-05-26 15:25:57 +02:00 committed by GitHub
parent 208e863327
commit bb5f01ac81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 261 additions and 216 deletions

View File

@ -103,12 +103,20 @@ export interface BackupContentAgent {
protected: boolean; protected: boolean;
} }
export interface AddonInfo {
name: string | null;
slug: string;
version: string | null;
}
export interface BackupContent { export interface BackupContent {
backup_id: string; backup_id: string;
date: string; date: string;
name: string; name: string;
agents: Record<string, BackupContentAgent>; agents: Record<string, BackupContentAgent>;
failed_agent_ids?: string[]; failed_agent_ids?: string[];
failed_addons?: AddonInfo[];
failed_folders?: string[];
extra_metadata?: { extra_metadata?: {
"supervisor.addon_update"?: string; "supervisor.addon_update"?: string;
}; };

View File

@ -1,15 +1,17 @@
import { css, html, LitElement } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; 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-card";
import "../../../../components/ha-md-list"; import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item"; import "../../../../components/ha-md-list-item";
import type { HomeAssistant } from "../../../../types";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
import { import {
computeBackupSize, computeBackupSize,
computeBackupType, computeBackupType,
type BackupContentExtended, type BackupContentExtended,
} from "../../../../data/backup"; } from "../../../../data/backup";
import type { HomeAssistant } from "../../../../types";
import { bytesToString } from "../../../../util/bytes-to-string"; import { bytesToString } from "../../../../util/bytes-to-string";
@customElement("ha-backup-details-summary") @customElement("ha-backup-details-summary")
@ -28,12 +30,35 @@ class HaBackupDetailsSummary extends LitElement {
this.hass.config 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` return html`
<ha-card> <ha-card>
<div class="card-header"> <div class="card-header">
${this.hass.localize("ui.panel.config.backup.details.summary.title")} ${this.hass.localize("ui.panel.config.backup.details.summary.title")}
</div> </div>
<div class="card-content"> <div class="card-content">
${errors.length ? this._renderErrorSummary(errors) : nothing}
<ha-md-list class="summary"> <ha-md-list class="summary">
<ha-md-list-item> <ha-md-list-item>
<span slot="headline"> <span slot="headline">
@ -69,6 +94,45 @@ class HaBackupDetailsSummary extends LitElement {
`; `;
} }
private _renderErrorSummary(errors: { title: string; items: string[] }[]) {
return html`
<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.backup.details.summary.error.title"
)}
>
${errors.map(
({ title, items }) => html`
<br />
<b>${title}:</b>
<ul>
${items.map((item) => html`<li>${item}</li>`)}
</ul>
`
)}
</ha-alert>
`;
}
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` static styles = css`
:host { :host {
max-width: 690px; max-width: 690px;

View File

@ -4,13 +4,18 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import {
formatDate,
formatDateWeekday,
} from "../../../../../common/datetime/format_date";
import { relativeTime } from "../../../../../common/datetime/relative_time"; import { relativeTime } from "../../../../../common/datetime/relative_time";
import type { LocalizeKeys } from "../../../../../common/translations/localize";
import "../../../../../components/ha-button"; import "../../../../../components/ha-button";
import "../../../../../components/ha-card"; import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-md-list"; import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item"; import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon"; import "../../../../../components/ha-svg-icon";
import "../../../../../components/ha-icon-button";
import type { BackupConfig, BackupContent } from "../../../../../data/backup"; import type { BackupConfig, BackupContent } from "../../../../../data/backup";
import { import {
BackupScheduleRecurrence, BackupScheduleRecurrence,
@ -18,12 +23,8 @@ import {
} from "../../../../../data/backup"; } from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types"; 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 { showAlertDialog } from "../../../../lovelace/custom-card-helpers";
import "../ha-backup-summary-card";
const OVERDUE_MARGIN_HOURS = 3; 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`
<ha-backup-summary-card .heading=${heading} .status=${status}>
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline" class=${headline === null ? "skeleton" : ""}
>${headline}</span
>
</ha-md-list-item>
${description || description === null
? html`<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span
slot="headline"
class=${description === null ? "skeleton" : ""}
>${description}</span
>
${lastCompletedDate
? html` <ha-icon-button
slot="end"
@click=${this._createAdditionalBackupDescription(
lastCompletedDate
)}
.path=${mdiInformation}
></ha-icon-button>`
: nothing}
</ha-md-list-item>`
: nothing}
</ha-md-list>
</ha-backup-summary-card>
`;
}
protected render() { protected render() {
const now = new Date(); const now = new Date();
if (this.fetching) { if (this.fetching) {
return html` return this._renderSummaryCard(
<ha-backup-summary-card this.hass.localize("ui.panel.config.backup.overview.summary.loading"),
.heading=${this.hass.localize( "loading",
"ui.panel.config.backup.overview.summary.loading" null,
)} null
status="loading" );
>
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline" class="skeleton"></span>
</ha-md-list-item>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline" class="skeleton"></span>
</ha-md-list-item>
</ha-md-list>
</ha-backup-summary-card>
`;
} }
const lastBackup = this._lastBackup(this.backups); const lastBackup = this._lastBackup(this.backups);
@ -137,18 +166,12 @@ class HaBackupOverviewBackups extends LitElement {
if (lastAttemptDate > lastCompletedDate) { if (lastAttemptDate > lastCompletedDate) {
const lastUploadedBackup = this._lastUploadedBackup(this.backups); const lastUploadedBackup = this._lastUploadedBackup(this.backups);
return html` return this._renderSummaryCard(
<ha-backup-summary-card this.hass.localize(
.heading=${this.hass.localize(
"ui.panel.config.backup.overview.summary.last_backup_failed_heading" "ui.panel.config.backup.overview.summary.last_backup_failed_heading"
)} ),
status="error" "error",
> this.hass.localize(
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.overview.summary.last_backup_failed_description", "ui.panel.config.backup.overview.summary.last_backup_failed_description",
{ {
relative_time: relativeTime( relative_time: relativeTime(
@ -158,18 +181,9 @@ class HaBackupOverviewBackups extends LitElement {
true true
), ),
} }
)} ),
</span> lastUploadedBackup || nextBackupDescription
</ha-md-list-item> ? lastUploadedBackup
${lastUploadedBackup || nextBackupDescription
? html`
<ha-md-list-item>
<ha-svg-icon
slot="start"
.path=${mdiCalendar}
></ha-svg-icon>
<span slot="headline">
${lastUploadedBackup
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.backup.overview.summary.last_successful_backup_description", "ui.panel.config.backup.overview.summary.last_successful_backup_description",
{ {
@ -179,67 +193,60 @@ class HaBackupOverviewBackups extends LitElement {
now, now,
true true
), ),
count: Object.keys(lastUploadedBackup.agents) count: Object.keys(lastUploadedBackup.agents).length,
.length,
} }
) )
: nextBackupDescription} : nextBackupDescription
</span> : undefined
</ha-md-list-item> );
`
: nothing}
</ha-md-list>
</ha-backup-summary-card>
`;
} }
// If no backups yet, show warning // If no backups yet, show warning
if (!lastBackup) { if (!lastBackup) {
return html` return this._renderSummaryCard(
<ha-backup-summary-card this.hass.localize(
.heading=${this.hass.localize(
"ui.panel.config.backup.overview.summary.no_backup_heading" "ui.panel.config.backup.overview.summary.no_backup_heading"
)} ),
status="warning" "warning",
> this.hass.localize(
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.overview.summary.no_backup_description" "ui.panel.config.backup.overview.summary.no_backup_description"
)} ),
</span>
</ha-md-list-item>
${this._renderNextBackupDescription(
nextBackupDescription, nextBackupDescription,
lastCompletedDate, showAdditionalBackupDescription ? lastCompletedDate : undefined
showAdditionalBackupDescription );
)}
</ha-md-list>
</ha-backup-summary-card>
`;
} }
const lastBackupDate = new Date(lastBackup.date); const lastBackupDate = new Date(lastBackup.date);
// If last backup // if parts of the last backup failed
if (lastBackup.failed_agent_ids?.length) { if (
lastBackup.failed_agent_ids?.length ||
lastBackup.failed_addons?.length ||
lastBackup.failed_folders?.length
) {
const lastUploadedBackup = this._lastUploadedBackup(this.backups); const lastUploadedBackup = this._lastUploadedBackup(this.backups);
return html` const failedTypes: string[] = [];
<ha-backup-summary-card
.heading=${this.hass.localize( 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" "ui.panel.config.backup.overview.summary.last_backup_failed_heading"
)} ),
status="error" "error",
> this.hass.localize(
<ha-md-list> `ui.panel.config.backup.overview.summary.last_backup_failed_${type}_description` as LocalizeKeys,
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.overview.summary.last_backup_failed_locations_description",
{ {
relative_time: relativeTime( relative_time: relativeTime(
lastAttemptDate, lastAttemptDate,
@ -248,15 +255,8 @@ class HaBackupOverviewBackups extends LitElement {
true true
), ),
} }
)} ),
</span> lastUploadedBackup
</ha-md-list-item>
${lastUploadedBackup || nextBackupDescription
? html` <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">
${lastUploadedBackup
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.backup.overview.summary.last_successful_backup_description", "ui.panel.config.backup.overview.summary.last_successful_backup_description",
{ {
@ -266,17 +266,12 @@ class HaBackupOverviewBackups extends LitElement {
now, now,
true true
), ),
count: Object.keys(lastUploadedBackup.agents) count: Object.keys(lastUploadedBackup.agents).length,
.length,
} }
) )
: nextBackupDescription} : nextBackupDescription,
</span> showAdditionalBackupDescription ? lastCompletedDate : undefined
</ha-md-list-item>` );
: nothing}
</ha-md-list>
</ha-backup-summary-card>
`;
} }
const lastSuccessfulBackupDescription = this.hass.localize( const lastSuccessfulBackupDescription = this.hass.localize(
@ -303,37 +298,20 @@ class HaBackupOverviewBackups extends LitElement {
this.config.schedule.recurrence === BackupScheduleRecurrence.DAILY) || this.config.schedule.recurrence === BackupScheduleRecurrence.DAILY) ||
numberOfDays >= 7; numberOfDays >= 7;
return html` return this._renderSummaryCard(
<ha-backup-summary-card this.hass.localize(
.heading=${this.hass.localize(
`ui.panel.config.backup.overview.summary.${isOverdue ? "backup_too_old_heading" : "backup_success_heading"}`, `ui.panel.config.backup.overview.summary.${isOverdue ? "backup_too_old_heading" : "backup_success_heading"}`,
{ count: numberOfDays } { count: numberOfDays }
)} ),
.status=${isOverdue ? "warning" : "success"} isOverdue ? "warning" : "success",
> lastSuccessfulBackupDescription,
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${lastSuccessfulBackupDescription}</span>
</ha-md-list-item>
${this._renderNextBackupDescription(
nextBackupDescription, nextBackupDescription,
lastCompletedDate, showAdditionalBackupDescription ? lastCompletedDate : undefined
showAdditionalBackupDescription );
)}
</ha-md-list>
</ha-backup-summary-card>
`;
} }
private _renderNextBackupDescription( private _createAdditionalBackupDescription =
nextBackupDescription: string, (lastCompletedDate: Date) => () => {
lastCompletedDate: Date,
showTip = false
) {
// handle edge case that there is an additional backup scheduled
const openAdditionalBackupDescriptionDialog = showTip
? () => {
showAlertDialog(this, { showAlertDialog(this, {
text: this.hass.localize( text: this.hass.localize(
"ui.panel.config.backup.overview.summary.additional_backup_description", "ui.panel.config.backup.overview.summary.additional_backup_description",
@ -346,24 +324,7 @@ class HaBackupOverviewBackups extends LitElement {
} }
), ),
}); });
} };
: undefined;
return nextBackupDescription
? html`<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">${nextBackupDescription}</span>
${showTip
? html` <ha-icon-button
slot="end"
@click=${openAdditionalBackupDescriptionDialog}
.path=${mdiInformation}
></ha-icon-button>`
: nothing}
</ha-md-list-item>`
: nothing;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [

View File

@ -2654,6 +2654,12 @@
"last_backup_failed_heading": "Last automatic backup failed", "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_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_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}.", "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_heading": "No automatic backup available",
"no_backup_description": "You have no automatic backups yet.", "no_backup_description": "You have no automatic backups yet.",
@ -2769,7 +2775,13 @@
"summary": { "summary": {
"title": "Backup", "title": "Backup",
"size": "Size", "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": { "restore": {
"title": "Select what to restore", "title": "Select what to restore",