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;
}
export interface AddonInfo {
name: string | null;
slug: string;
version: string | null;
}
export interface BackupContent {
backup_id: string;
date: string;
name: string;
agents: Record<string, BackupContentAgent>;
failed_agent_ids?: string[];
failed_addons?: AddonInfo[];
failed_folders?: string[];
extra_metadata?: {
"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 { 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`
<ha-card>
<div class="card-header">
${this.hass.localize("ui.panel.config.backup.details.summary.title")}
</div>
<div class="card-content">
${errors.length ? this._renderErrorSummary(errors) : nothing}
<ha-md-list class="summary">
<ha-md-list-item>
<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`
:host {
max-width: 690px;

View File

@ -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`
<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() {
const now = new Date();
if (this.fetching) {
return html`
<ha-backup-summary-card
.heading=${this.hass.localize(
"ui.panel.config.backup.overview.summary.loading"
)}
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>
`;
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`
<ha-backup-summary-card
.heading=${this.hass.localize(
"ui.panel.config.backup.overview.summary.last_backup_failed_heading"
)}
status="error"
>
<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",
{
relative_time: relativeTime(
lastAttemptDate,
this.hass.locale,
now,
true
),
}
)}
</span>
</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(
"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}
</span>
</ha-md-list-item>
`
: nothing}
</ha-md-list>
</ha-backup-summary-card>
`;
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`
<ha-backup-summary-card
.heading=${this.hass.localize(
"ui.panel.config.backup.overview.summary.no_backup_heading"
)}
status="warning"
>
<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"
)}
</span>
</ha-md-list-item>
${this._renderNextBackupDescription(
nextBackupDescription,
lastCompletedDate,
showAdditionalBackupDescription
)}
</ha-md-list>
</ha-backup-summary-card>
`;
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`
<ha-backup-summary-card
.heading=${this.hass.localize(
"ui.panel.config.backup.overview.summary.last_backup_failed_heading"
)}
status="error"
>
<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_locations_description",
{
relative_time: relativeTime(
lastAttemptDate,
this.hass.locale,
now,
true
),
}
)}
</span>
</ha-md-list-item>
const failedTypes: string[] = [];
${lastUploadedBackup || nextBackupDescription
? html` <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">
${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}
</span>
</ha-md-list-item>`
: nothing}
</ha-md-list>
</ha-backup-summary-card>
`;
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`
<ha-backup-summary-card
.heading=${this.hass.localize(
`ui.panel.config.backup.overview.summary.${isOverdue ? "backup_too_old_heading" : "backup_success_heading"}`,
{ count: numberOfDays }
)}
.status=${isOverdue ? "warning" : "success"}
>
<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,
lastCompletedDate,
showAdditionalBackupDescription
)}
</ha-md-list>
</ha-backup-summary-card>
`;
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`<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 {
return [

View File

@ -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",