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,146 +166,112 @@ 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" ),
)} "error",
status="error" this.hass.localize(
> "ui.panel.config.backup.overview.summary.last_backup_failed_description",
<ha-md-list> {
<ha-md-list-item> relative_time: relativeTime(
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon> lastAttemptDate,
<span slot="headline"> this.hass.locale,
${this.hass.localize( now,
"ui.panel.config.backup.overview.summary.last_backup_failed_description", true
{ ),
relative_time: relativeTime( }
lastAttemptDate, ),
this.hass.locale, lastUploadedBackup || nextBackupDescription
now, ? lastUploadedBackup
true ? this.hass.localize(
), "ui.panel.config.backup.overview.summary.last_successful_backup_description",
} {
)} relative_time: relativeTime(
</span> new Date(lastUploadedBackup.date),
</ha-md-list-item> this.hass.locale,
${lastUploadedBackup || nextBackupDescription now,
? html` true
<ha-md-list-item> ),
<ha-svg-icon count: Object.keys(lastUploadedBackup.agents).length,
slot="start" }
.path=${mdiCalendar} )
></ha-svg-icon> : nextBackupDescription
<span slot="headline"> : undefined
${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 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" ),
)} "warning",
status="warning" this.hass.localize(
> "ui.panel.config.backup.overview.summary.no_backup_description"
<ha-md-list> ),
<ha-md-list-item> nextBackupDescription,
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon> showAdditionalBackupDescription ? lastCompletedDate : undefined
<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>
`;
} }
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(
"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>
${lastUploadedBackup || nextBackupDescription if (lastBackup.failed_agent_ids?.length) {
? html` <ha-md-list-item> failedTypes.push("locations");
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon> }
<span slot="headline"> if (lastBackup.failed_addons?.length) {
${lastUploadedBackup failedTypes.push("addons");
? this.hass.localize( }
"ui.panel.config.backup.overview.summary.last_successful_backup_description", if (lastBackup.failed_folders?.length) {
{ failedTypes.push("folders");
relative_time: relativeTime( }
new Date(lastUploadedBackup.date),
this.hass.locale, const type = failedTypes.join("_");
now,
true return this._renderSummaryCard(
), this.hass.localize(
count: Object.keys(lastUploadedBackup.agents) "ui.panel.config.backup.overview.summary.last_backup_failed_heading"
.length, ),
} "error",
) this.hass.localize(
: nextBackupDescription} `ui.panel.config.backup.overview.summary.last_backup_failed_${type}_description` as LocalizeKeys,
</span> {
</ha-md-list-item>` relative_time: relativeTime(
: nothing} lastAttemptDate,
</ha-md-list> this.hass.locale,
</ha-backup-summary-card> 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( const lastSuccessfulBackupDescription = this.hass.localize(
@ -303,67 +298,33 @@ 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 } ),
)} isOverdue ? "warning" : "success",
.status=${isOverdue ? "warning" : "success"} lastSuccessfulBackupDescription,
> nextBackupDescription,
<ha-md-list> showAdditionalBackupDescription ? lastCompletedDate : undefined
<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>
`;
} }
private _renderNextBackupDescription( private _createAdditionalBackupDescription =
nextBackupDescription: string, (lastCompletedDate: Date) => () => {
lastCompletedDate: Date, showAlertDialog(this, {
showTip = false text: this.hass.localize(
) { "ui.panel.config.backup.overview.summary.additional_backup_description",
// handle edge case that there is an additional backup scheduled {
const openAdditionalBackupDescriptionDialog = showTip date: formatDate(
? () => { lastCompletedDate,
showAlertDialog(this, { this.hass.locale,
text: this.hass.localize( this.hass.config
"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 { 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",