Skip to content

Use the HACS repository icon endpoint for dashboard icons - #945

Open
Niek wants to merge 4 commits into
hacs:mainfrom
Niek:repository-brand-icons
Open

Use the HACS repository icon endpoint for dashboard icons#945
Niek wants to merge 4 commits into
hacs:mainfrom
Niek:repository-brand-icons

Conversation

@Niek

@Niek Niek commented Jul 15, 2026

Copy link
Copy Markdown

Proposed change

Uses the repository icon endpoint provided by hacs/integration#5388 for integration rows instead of relying only on the public brands CDN.

Since Home Assistant 2026.3, custom integrations ship brand icons inside the repository (custom_components/<domain>/brand/), and home-assistant/brands no longer accepts images for custom integrations. The CDN URLs used today therefore show placeholders for integrations that are not grandfathered in.

This change:

  • obtains Home Assistant's rotating brands access token through brands/access_token and refreshes it every 30 minutes;
  • includes the token in same-origin HACS icon URLs and keeps referrerpolicy="no-referrer";
  • uses dark_icon.png in dark mode and icon.png otherwise;
  • falls back to the existing brands CDN URL if the token command is unavailable (for older Home Assistant versions), the repository has no domain, or the HACS endpoint fails to load;
  • keeps the icon decorative with alt="".

Requires hacs/integration#5388.

Release follow-up

The integration currently pins frontend release 20250128065759 in scripts/install/frontend. After both PRs merge, a new frontend release must be published and that pin must be bumped before the integration release. Until then, merging this PR alone does not ship the frontend change to HACS users.

Related issues and PRs: hacs/integration#5171, hacs/integration#5179, hacs/integration#5223, and earlier installed-only approaches #937 / #929.

Verification

  • Prettier passes for src/dashboards/hacs-dashboard.ts.
  • Full dependency-based build: blocked before compilation because the repository pins the unavailable tsparticles-preset-links@2.12.0 package; the same dependency is present on main.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates how HACS dashboard repository icons are resolved, switching integration icons to the HACS backend’s repository icon endpoint so icons can be served from each repository’s bundled brand/ assets (with backend caching/redirect behavior), instead of relying on the public brands CDN which no longer accepts new custom-integration images.

Changes:

  • Use /api/hacs/repository/{id}/{icon|dark_icon}.png for integration repository icons when a domain exists.
  • Keep the existing brandsUrl(...) fallback path for repositories without a domain.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/dashboards/hacs-dashboard.ts Outdated
Comment thread src/dashboards/hacs-dashboard.ts Outdated

@thomasgregg thomasgregg left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this. I checked out both this PR and hacs/integration#5388 and built the frontend locally on Node 18 (with the unrelated dependency fix from #943 applied).

I found one remaining update issue: _columns is memoized using only localizeFunc and narrow, but _brandsAccessToken is populated asynchronously after firstUpdated. Updating that state rerenders HacsDashboard, yet it passes the same memoized columns object to hass-tabs-subpage-data-table. The nested data table only recomputes its rows when its columns property changes, so existing rows can retain the initial CDN/placeholder URL until an unrelated update happens to redraw the child table.

The minimal correction is to make the token and dark-mode state inputs to the memoized column factory, and use those arguments inside the icon template:

.columns=${this._columns(
  this.hacs.localize,
  this.narrow,
  this._brandsAccessToken,
  this.hass.themes?.darkMode,
)}
private _columns = memoize(
  (
    localizeFunc: LocalizeFunc<HacsLocalizeKeys>,
    narrow: boolean,
    brandsAccessToken: string | undefined,
    darkMode: boolean | undefined,
  ): DataTableColumnContainer<RepositoryBase> => ({
    // Use brandsAccessToken and darkMode in the icon template.
  }),
);

I would also keep the previous token when a scheduled refresh fails instead of setting _brandsAccessToken to undefined. Home Assistant retains both the current and previous brands tokens, so the existing token remains useful across a transient WebSocket failure; an expired token will still take the image's normal error fallback path.

With those changes, Prettier and the HACS build-hacs test build both pass locally. I can open a small follow-up patch if that would be useful.

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.
@Niek

Niek commented Sep 9, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough review — and for actually building both PRs locally, @thomasgregg. Both points are addressed in dd7aace:

  • _columns now takes brandsAccessToken and darkMode as memoize arguments, and the icon template uses those arguments instead of reading this._brandsAccessToken / this.hass.themes?.darkMode, so the data table gets a new columns object (and re-renders rows) when the token arrives or the theme flips — exactly as you suggested.
  • A failed scheduled refresh now keeps the previous token instead of clearing it; as you note, HA keeps the previous brands token valid, and an expired token still takes the image @error fallback to the CDN.

While in there I also fixed a small pre-existing type error: _handleRepositoryIconError now accepts domain?: string | null to match RepositoryBase.domain.

Verified locally: Prettier clean, tsc --noEmit reports no errors in the HACS sources, and gulp build-hacs passes on Node 18 with the #943 dependency fix applied.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants