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/4] 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/4] 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/4] 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]; } From dd7aace62a57d4070535269e405dd10e74add5fc Mon Sep 17 00:00:00 2001 From: Niek van der Maas Date: Wed, 9 Sep 2026 10:15:02 +0200 Subject: [PATCH 4/4] fix: rerender repository icons when brands token or theme changes Make the brands access token and dark-mode state inputs to the memoized column factory so the data table recomputes rows when the token arrives or the theme changes. Keep the previous token when a scheduled refresh fails, since it usually remains valid and an expired token already falls back through the image error handler. Also accept null domains in the icon error handler to match RepositoryBase.domain. --- src/dashboards/hacs-dashboard.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/dashboards/hacs-dashboard.ts b/src/dashboards/hacs-dashboard.ts index c8f94a5b..56ea737d 100644 --- a/src/dashboards/hacs-dashboard.ts +++ b/src/dashboards/hacs-dashboard.ts @@ -147,7 +147,12 @@ export class HacsDashboard extends LitElement { return html`, narrow: boolean, + brandsAccessToken: string | undefined, + darkMode: boolean | undefined, ): DataTableColumnContainer => ({ icon: { title: "", @@ -351,15 +358,15 @@ export class HacsDashboard extends LitElement { style="height: 32px; width: 32px" slot="item-icon" alt="" - src=${repository.domain && this._brandsAccessToken + src=${repository.domain && brandsAccessToken ? `/api/hacs/repository/${encodeURIComponent(repository.id)}/${ - this.hass.themes?.darkMode ? "dark_icon" : "icon" - }.png?token=${encodeURIComponent(this._brandsAccessToken)}` + darkMode ? "dark_icon" : "icon" + }.png?token=${encodeURIComponent(brandsAccessToken)}` : brandsUrl({ domain: repository.domain || "invalid", type: "icon", useFallback: true, - darkOptimized: this.hass.themes?.darkMode, + darkOptimized: darkMode, })} referrerpolicy="no-referrer" @error=${(event: Event) => @@ -634,11 +641,12 @@ export class HacsDashboard extends LitElement { }); this._brandsAccessToken = token; } catch { - this._brandsAccessToken = undefined; + // Keep the previous token; it usually remains valid across a transient + // failure, and an expired one falls back via the image error handler. } } - private _handleRepositoryIconError(event: Event, domain?: string): void { + private _handleRepositoryIconError(event: Event, domain?: string | null): void { const image = event.currentTarget as HTMLImageElement; const fallback = brandsUrl({ domain: domain || "invalid",