1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-02-15 07:25:54 +00:00

Add repairs and updates cards to home dashboard overview (#29552)

* Add repairs and updates cards to home dashboard overview

Add two new cards to the "For You" section of the home dashboard that display
links to repairs and updates when there are active issues or available updates.
Both cards are only visible to admin users and hide when empty.

https://claude.ai/code/session_013NTgs1U9x59uaEJs1smy8i

* Fix navigation and visibility

* Reorder

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Paul Bottein
2026-02-11 08:15:37 +01:00
committed by GitHub
parent 541cc7d10b
commit 2e372b2f8a
9 changed files with 380 additions and 3 deletions

View File

@@ -41,6 +41,8 @@ class HaConfigSectionUpdates extends LitElement {
@property({ type: Boolean }) public narrow = false;
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _showSkipped = false;
@state() private _supervisorInfo?: HassioSupervisorInfo;
@@ -65,7 +67,9 @@ class HaConfigSectionUpdates extends LitElement {
return html`
<hass-subpage
back-path="/config/system"
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config/system"}
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.updates.caption")}

View File

@@ -32,6 +32,8 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
@state() private _repairsIssues: RepairsIssue[] = [];
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _showIgnored = false;
private _getFilteredIssues = memoizeOne(
@@ -75,7 +77,9 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
return html`
<hass-subpage
back-path="/config/system"
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config/system"}
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.repairs.caption")}

View File

@@ -29,6 +29,8 @@ export class HuiDiscoveredDevicesCard
extends SubscribeMixin(LitElement)
implements LovelaceCard
{
public connectedWhileHidden = true;
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: DiscoveredDevicesCardConfig;
@@ -181,7 +183,7 @@ export class HuiDiscoveredDevicesCard
tileCardStyle,
css`
:host {
--tile-color: var(--primary-color);
--tile-color: var(--info-color);
}
`,
];

View File

@@ -0,0 +1,162 @@
import { mdiWrench } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import "../../../components/ha-card";
import "../../../components/tile/ha-tile-container";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info";
import type { RepairsIssue } from "../../../data/repairs";
import { subscribeRepairsIssueRegistry } from "../../../data/repairs";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import type { LovelaceCard, LovelaceGridOptions } from "../types";
import { tileCardStyle } from "./tile/tile-card-style";
import type { RepairsCardConfig } from "./types";
@customElement("hui-repairs-card")
export class HuiRepairsCard
extends SubscribeMixin(LitElement)
implements LovelaceCard
{
public connectedWhileHidden = true;
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: RepairsCardConfig;
@state() private _repairsIssues: RepairsIssue[] = [];
public hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeRepairsIssueRegistry(
this.hass!.connection,
(repairs: { issues: RepairsIssue[] }) => {
// Filter to only active and non-ignored issues
this._repairsIssues = repairs.issues.filter(
(issue) => issue.active !== false && !issue.ignored
);
}
),
];
}
public setConfig(config: RepairsCardConfig): void {
this._config = config;
}
public getCardSize(): number {
return this._config?.vertical ? 2 : 1;
}
public getGridOptions(): LovelaceGridOptions {
const columns = 6;
let min_columns = 6;
let rows = 1;
if (this._config?.vertical) {
rows++;
min_columns = 3;
}
return {
columns,
rows,
min_columns,
min_rows: rows,
};
}
private async _handleAction(ev: ActionHandlerEvent) {
if (ev.detail.action === "tap" && !hasAction(this._config?.tap_action)) {
navigate("/config/repairs");
return;
}
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
private get _hasCardAction() {
return (
!this._config?.tap_action ||
hasAction(this._config?.tap_action) ||
hasAction(this._config?.hold_action) ||
hasAction(this._config?.double_tap_action)
);
}
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this._config || !this.hass) {
return;
}
// Update visibility based on admin status and repairs count
const shouldBeHidden =
!this.hass.user?.is_admin ||
(this._config.hide_empty && this._repairsIssues.length === 0);
if (shouldBeHidden !== this.hidden) {
this.style.display = shouldBeHidden ? "none" : "";
this.toggleAttribute("hidden", shouldBeHidden);
fireEvent(this, "card-visibility-changed", { value: !shouldBeHidden });
}
}
protected render(): TemplateResult | typeof nothing {
if (!this._config || !this.hass || this.hidden) {
return nothing;
}
const count = this._repairsIssues.length;
const label = this.hass.localize("ui.card.repairs.title");
const secondary =
count > 0
? this.hass.localize("ui.card.repairs.count_issues", {
count,
})
: this.hass.localize("ui.card.repairs.no_issues");
return html`
<ha-card>
<ha-tile-container
.vertical=${Boolean(this._config.vertical)}
.interactive=${this._hasCardAction}
.actionHandlerOptions=${{
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
}}
@action=${this._handleAction}
>
<ha-tile-icon slot="icon" .iconPath=${mdiWrench}></ha-tile-icon>
<ha-tile-info
slot="info"
.primary=${label}
.secondary=${secondary}
></ha-tile-info>
</ha-tile-container>
</ha-card>
`;
}
static styles = [
tileCardStyle,
css`
:host {
--tile-color: var(--warning-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"hui-repairs-card": HuiRepairsCard;
}
}

View File

@@ -0,0 +1,157 @@
import { mdiPackageUp } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import "../../../components/ha-card";
import "../../../components/tile/ha-tile-container";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import {
filterUpdateEntities,
updateCanInstall,
type UpdateEntity,
} from "../../../data/update";
import type { HomeAssistant } from "../../../types";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import type { LovelaceCard, LovelaceGridOptions } from "../types";
import { tileCardStyle } from "./tile/tile-card-style";
import type { UpdatesCardConfig } from "./types";
@customElement("hui-updates-card")
export class HuiUpdatesCard extends LitElement implements LovelaceCard {
public connectedWhileHidden = true;
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: UpdatesCardConfig;
public setConfig(config: UpdatesCardConfig): void {
this._config = config;
}
public getCardSize(): number {
return this._config?.vertical ? 2 : 1;
}
public getGridOptions(): LovelaceGridOptions {
const columns = 6;
let min_columns = 6;
let rows = 1;
if (this._config?.vertical) {
rows++;
min_columns = 3;
}
return {
columns,
rows,
min_columns,
min_rows: rows,
};
}
private _getUpdateEntities(): UpdateEntity[] {
if (!this.hass) {
return [];
}
return filterUpdateEntities(
this.hass.states,
this.hass.locale.language
).filter((entity) => updateCanInstall(entity, false));
}
private async _handleAction(ev: ActionHandlerEvent) {
if (ev.detail.action === "tap" && !hasAction(this._config?.tap_action)) {
navigate("/config/updates");
return;
}
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
private get _hasCardAction() {
return (
!this._config?.tap_action ||
hasAction(this._config?.tap_action) ||
hasAction(this._config?.hold_action) ||
hasAction(this._config?.double_tap_action)
);
}
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this._config || !this.hass) {
return;
}
const updateEntities = this._getUpdateEntities();
// Update visibility based on admin status and updates count
const shouldBeHidden =
!this.hass.user?.is_admin ||
(this._config.hide_empty && updateEntities.length === 0);
if (shouldBeHidden !== this.hidden) {
this.style.display = shouldBeHidden ? "none" : "";
this.toggleAttribute("hidden", shouldBeHidden);
fireEvent(this, "card-visibility-changed", { value: !shouldBeHidden });
}
}
protected render(): TemplateResult | typeof nothing {
if (!this._config || !this.hass || this.hidden) {
return nothing;
}
const updateEntities = this._getUpdateEntities();
const count = updateEntities.length;
const label = this.hass.localize("ui.card.updates.title");
const secondary =
count > 0
? this.hass.localize("ui.card.updates.count_updates", {
count,
})
: this.hass.localize("ui.card.updates.no_updates");
return html`
<ha-card>
<ha-tile-container
.vertical=${Boolean(this._config.vertical)}
.interactive=${this._hasCardAction}
.actionHandlerOptions=${{
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
}}
@action=${this._handleAction}
>
<ha-tile-icon slot="icon" .iconPath=${mdiPackageUp}></ha-tile-icon>
<ha-tile-info
slot="info"
.primary=${label}
.secondary=${secondary}
></ha-tile-info>
</ha-tile-container>
</ha-card>
`;
}
static styles = [
tileCardStyle,
css`
:host {
--tile-color: var(--info-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"hui-updates-card": HuiUpdatesCard;
}
}

View File

@@ -682,3 +682,19 @@ export interface DiscoveredDevicesCardConfig extends LovelaceCardConfig {
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
export interface RepairsCardConfig extends LovelaceCardConfig {
hide_empty?: boolean;
vertical?: boolean;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
export interface UpdatesCardConfig extends LovelaceCardConfig {
hide_empty?: boolean;
vertical?: boolean;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}

View File

@@ -74,6 +74,8 @@ const LAZY_LOAD_TYPES = {
error: () => import("../cards/hui-error-card"),
"home-summary": () => import("../cards/hui-home-summary-card"),
"discovered-devices": () => import("../cards/hui-discovered-devices-card"),
repairs: () => import("../cards/hui-repairs-card"),
updates: () => import("../cards/hui-updates-card"),
gauge: () => import("../cards/hui-gauge-card"),
"history-graph": () => import("../cards/hui-history-graph-card"),
"horizontal-stack": () => import("../cards/hui-horizontal-stack-card"),

View File

@@ -23,7 +23,9 @@ import type {
EmptyStateCardConfig,
HomeSummaryCard,
MarkdownCardConfig,
RepairsCardConfig,
TileCardConfig,
UpdatesCardConfig,
} from "../../cards/types";
import type { Condition } from "../../common/validate-condition";
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
@@ -252,6 +254,24 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
// Build summary cards (used in both mobile section and sidebar)
const summaryCards: LovelaceCardConfig[] = [
// Repairs card - only visible to admins, hides when empty
{
type: "repairs",
hide_empty: true,
tap_action: {
action: "navigate",
navigation_path: "/config/repairs?historyBack=1",
},
} satisfies RepairsCardConfig,
// Updates card - only visible to admins, hides when empty
{
type: "updates",
hide_empty: true,
tap_action: {
action: "navigate",
navigation_path: "/config/updates?historyBack=1",
},
} satisfies UpdatesCardConfig,
// Discovered devices card - only visible to admins, hides when empty
{
type: "discovered-devices",

View File

@@ -220,6 +220,16 @@
"count_devices": "{count} {count, plural,\n one {device to add}\n other {devices to add}\n}",
"no_devices": "No devices to add"
},
"repairs": {
"title": "Repairs",
"count_issues": "{count} {count, plural,\n one {issue}\n other {issues}\n}",
"no_issues": "No issues"
},
"updates": {
"title": "Updates",
"count_updates": "{count} {count, plural,\n one {update available}\n other {updates available}\n}",
"no_updates": "Up to date"
},
"media_player": {
"source": "Source",
"sound_mode": "Sound mode",