From d8eb52a6ac6a6a866c5b80339120d7979046efd2 Mon Sep 17 00:00:00 2001 From: Niek van der Maas Date: Wed, 15 Jul 2026 12:35:33 +0200 Subject: [PATCH 1/3] Use the HACS repository icon endpoint for dashboard icons Point integration icons at /api/hacs/repository/{id}/icon.png served by the HACS integration, instead of the public brands CDN which no longer accepts images for custom integrations. Repositories without a domain keep the CDN placeholder fallback. --- src/dashboards/hacs-dashboard.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/dashboards/hacs-dashboard.ts b/src/dashboards/hacs-dashboard.ts index a42c4bb4..cfa8c43a 100644 --- a/src/dashboards/hacs-dashboard.ts +++ b/src/dashboards/hacs-dashboard.ts @@ -327,12 +327,16 @@ export class HacsDashboard extends LitElement { ` From e12b0ed055b91855bae1ff9581d0e6d541ff4a45 Mon Sep 17 00:00:00 2001 From: Niek van der Maas Date: Wed, 15 Jul 2026 13:29:46 +0200 Subject: [PATCH 2/3] Encode repository id in icon URL and mark icon as decorative --- src/dashboards/hacs-dashboard.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dashboards/hacs-dashboard.ts b/src/dashboards/hacs-dashboard.ts index cfa8c43a..dd5eb611 100644 --- a/src/dashboards/hacs-dashboard.ts +++ b/src/dashboards/hacs-dashboard.ts @@ -327,8 +327,9 @@ export class HacsDashboard extends LitElement { Date: Wed, 15 Jul 2026 14:58:08 +0200 Subject: [PATCH 3/3] fix: authenticate repository brand icons --- src/dashboards/hacs-dashboard.ts | 73 ++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/src/dashboards/hacs-dashboard.ts b/src/dashboards/hacs-dashboard.ts index dd5eb611..c8f94a5b 100644 --- a/src/dashboards/hacs-dashboard.ts +++ b/src/dashboards/hacs-dashboard.ts @@ -10,7 +10,7 @@ import { mdiInformation, mdiNewBox, } from "@mdi/js"; -import type { CSSResultGroup, TemplateResult } from "lit"; +import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { LitElement, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoize from "memoize-one"; @@ -63,6 +63,7 @@ const defaultKeyData = { }; const STATUS_ORDER = ["pending-restart", "pending-upgrade", "installed", "new", "default"]; +const BRANDS_TOKEN_REFRESH_INTERVAL = 30 * 60 * 1000; const TABS: PageNavigation[] = [ { @@ -114,6 +115,28 @@ export class HacsDashboard extends LitElement { @state() private _overflowMenuRepository?: RepositoryBase; + @state() + private _brandsAccessToken?: string; + + private _brandsTokenRefreshInterval?: number; + + public connectedCallback(): void { + super.connectedCallback(); + if (this.hasUpdated) { + this._startBrandsTokenRefresh(); + } + } + + public disconnectedCallback(): void { + super.disconnectedCallback(); + this._stopBrandsTokenRefresh(); + } + + protected firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + this._startBrandsTokenRefresh(); + } + protected render = (): TemplateResult | void => { const repositories = this._filterRepositories( this.hacs.repositories, @@ -328,17 +351,19 @@ export class HacsDashboard extends LitElement { style="height: 32px; width: 32px" slot="item-icon" alt="" - src=${repository.domain + src=${repository.domain && this._brandsAccessToken ? `/api/hacs/repository/${encodeURIComponent(repository.id)}/${ this.hass.themes?.darkMode ? "dark_icon" : "icon" - }.png` + }.png?token=${encodeURIComponent(this._brandsAccessToken)}` : brandsUrl({ - domain: "invalid", + domain: repository.domain || "invalid", type: "icon", useFallback: true, darkOptimized: this.hass.themes?.darkMode, })} referrerpolicy="no-referrer" + @error=${(event: Event) => + this._handleRepositoryIconError(event, repository.domain)} /> ` : html` @@ -586,6 +611,46 @@ export class HacsDashboard extends LitElement { this._activeFilters = undefined; } + private _startBrandsTokenRefresh(): void { + this._stopBrandsTokenRefresh(); + void this._fetchBrandsAccessToken(); + this._brandsTokenRefreshInterval = window.setInterval( + () => void this._fetchBrandsAccessToken(), + BRANDS_TOKEN_REFRESH_INTERVAL, + ); + } + + private _stopBrandsTokenRefresh(): void { + if (this._brandsTokenRefreshInterval !== undefined) { + window.clearInterval(this._brandsTokenRefreshInterval); + this._brandsTokenRefreshInterval = undefined; + } + } + + private async _fetchBrandsAccessToken(): Promise { + try { + const { token } = await this.hass.callWS<{ token: string }>({ + type: "brands/access_token", + }); + this._brandsAccessToken = token; + } catch { + this._brandsAccessToken = undefined; + } + } + + private _handleRepositoryIconError(event: Event, domain?: string): void { + const image = event.currentTarget as HTMLImageElement; + const fallback = brandsUrl({ + domain: domain || "invalid", + type: "icon", + useFallback: true, + darkOptimized: this.hass.themes?.darkMode, + }); + if (image.src !== fallback) { + image.src = fallback; + } + } + static get styles(): CSSResultGroup { return [haStyle, HacsStyles]; }