From 3e32b8e2a0a2d305771123f2aea602d61df0af3b Mon Sep 17 00:00:00 2001 From: Ricardo Canales Date: Tue, 23 Jun 2026 23:43:30 -0700 Subject: [PATCH 1/2] feat(settings): move logo.dev into Providers as an API integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reframe the Providers tab to cover all pluggable APIs, not just bank-data providers, and relocate the logo.dev brand-logo settings (the counterparty logos toggle + publishable token) out of General → Counterparties into a new 'API integrations' section there. logo.dev now reads as a directory row (image icon · name · on/off status · chevron) that opens a config drawer — matching the bank-provider rows — so the tab is a uniform directory of services you plug in. The drawer holds the enable toggle and token (both auto-saving via settings.js to the existing 204 endpoints) plus an info note about the domain-restricted-key referrer gotcha (a restricted key needs this instance's domain in its allowed referrers, wildcard included) — the exact 401 a user just hit. The two config keys + POST endpoints are unchanged; only where they render moves. ProvidersProps gains CounterpartyLogos/LogoDevToken (populated from the existing CounterpartyLogoSettings); SettingsProps sheds the now-unused fields. General loses the Counterparties section. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012fRHZ1CTjEZ1xFwQmW27jU --- internal/admin/providers.go | 8 ++ internal/admin/settings.go | 6 - .../components/pages/providers.templ | 107 +++++++++++++++++- .../components/pages/providers_types.go | 7 ++ .../templates/components/pages/settings.templ | 47 -------- .../components/pages/settings_types.go | 7 -- 6 files changed, 118 insertions(+), 64 deletions(-) diff --git a/internal/admin/providers.go b/internal/admin/providers.go index 78d13ad6..ab4a0503 100644 --- a/internal/admin/providers.go +++ b/internal/admin/providers.go @@ -51,6 +51,11 @@ func ProvidersGetHandler(a *app.App, svc *service.Service, sm *scs.SessionManage "Flash": GetFlash(r.Context(), sm), } + // logo.dev integration (the "API integrations" section): the + // counterparty brand-logo toggle + its publishable token (env → + // app_config → default). + counterpartyLogos, logoDevToken := svc.CounterpartyLogoSettings(ctx) + props := pages.ProvidersProps{ CSRFToken: GetCSRFToken(r), ConfigSources: a.Config.ConfigSources, @@ -75,6 +80,9 @@ func ProvidersGetHandler(a *app.App, svc *service.Service, sm *scs.SessionManage HasEncryptionKey: len(a.Config.EncryptionKey) > 0, SyncIntervalMinutes: a.Config.SyncIntervalMinutes, ProviderHealth: providerHealth, + + CounterpartyLogos: counterpartyLogos, + LogoDevToken: logoDevToken, } renderProviders(w, r, sm, tr, data, props) } diff --git a/internal/admin/settings.go b/internal/admin/settings.go index 8635eda3..b3095490 100644 --- a/internal/admin/settings.go +++ b/internal/admin/settings.go @@ -83,10 +83,6 @@ func buildSettingsProps(a *app.App, r *http.Request) (pages.SettingsProps, map[s } agentAvatarStyle := appconfig.String(ctx, a.Queries, appconfig.KeyAvatarAgentStyle, avatar.DefaultAgentStyle) - // Counterparty logo hotlinking (env → app_config → default) for the - // Appearance section's toggle + optional logo.dev publishable token. - counterpartyLogos, logoDevToken := a.Service.CounterpartyLogoSettings(ctx) - nextSyncTime := "" if a.Scheduler != nil { nextSyncTime = formatNextSync(a.Scheduler.NextRun()) @@ -117,8 +113,6 @@ func buildSettingsProps(a *app.App, r *http.Request) (pages.SettingsProps, map[s AvatarUserStyle: userAvatarStyle, AvatarAgentStyle: agentAvatarStyle, AvatarStyles: avatar.AvailableStyles, - CounterpartyLogos: counterpartyLogos, - LogoDevToken: logoDevToken, } if a.VersionChecker != nil { if updateAvailable, latest, err := a.VersionChecker.CheckForUpdate(ctx); err == nil && updateAvailable != nil && *updateAvailable && latest != nil { diff --git a/internal/templates/components/pages/providers.templ b/internal/templates/components/pages/providers.templ index e045be26..535ce4a6 100644 --- a/internal/templates/components/pages/providers.templ +++ b/internal/templates/components/pages/providers.templ @@ -20,10 +20,13 @@ import ( // drawer scopes it via `x-data="providerCard" data-provider="..."`. templ Providers(p ProvidersProps) { + +
@settingsTabHeader(settingsTabHeaderProps{ Title: "Providers", - Subtitle: "Connect bank-data providers to sync accounts and transactions", + Subtitle: "Connect bank-data providers and API integrations to your Breadbox", }) @components.SettingsSection(components.SettingsSectionProps{ Icon: "plug", @@ -69,16 +72,112 @@ templ Providers(p ProvidersProps) { Health: p.ProviderHealth["csv"], }) } - // Per-provider drawers. Position-independent (fixed), so rendering - // them after the section is fine; only the one matching the open id - // (plus its scrim) shows. + @components.SettingsSection(components.SettingsSectionProps{ + Icon: "puzzle", + Title: "API integrations", + Caption: "Non-bank APIs you can plug in to enrich your data", + }) { + @logoDevRow(p) + } + // Per-provider + integration drawers. Position-independent (fixed), so + // rendering them after the section is fine; only the one matching the open + // id (plus its scrim) shows. @providersPlaidDrawer(p, p.ProviderHealth["plaid"]) @providersTellerDrawer(p, p.ProviderHealth["teller"]) @providersSimplefinDrawer(p, p.ProviderHealth["simplefin"]) @providersCSVDrawer(p.ProviderHealth["csv"]) + @providersLogoDevDrawer(p)
} +// logoDevRow is the directory row for the logo.dev brand-logo integration. It +// mirrors providerRow's shape (icon · name · desc · status · chevron, opening a +// drawer) but carries a plain on/off status rather than a bank-provider health +// dot — logo.dev has no connection to health-check. +templ logoDevRow(p ProvidersProps) { + +} + +// providersLogoDevDrawer configures the logo.dev integration — the enable toggle +// and the publishable token, relocated from the General → Counterparties section +// so every pluggable API lives on the Providers directory. Both controls +// auto-save on change (settings.js's settingsAutoSave → the 204 endpoints), so +// the drawer has only a Close footer. The referrer note heads off the common +// domain-restriction 401 (a restricted key needs this instance's domain in its +// allowed referrers). +templ providersLogoDevDrawer(p ProvidersProps) { + @components.Drawer(components.DrawerProps{ + ID: "provider-logodev", + Title: "logo.dev", + Size: "md", + }) { +
+ @components.DrawerHeader(components.DrawerHeaderProps{ + Icon: "image", + Title: "logo.dev", + Subtitle: "Brand logos for counterparties", + }) +
+

+ Hotlink real brand logos onto counterparty rows and headers. Each counterparty with a website (and no manual logo) shows its brand logo from logo.dev; otherwise it falls back to a gradient monogram. +

+
+
+
Counterparty logos
+

The counterparty's domain is sent to logo.dev on each render.

+
+ @components.SettingsAutoSaveForm(components.SettingsAutoSaveFormProps{ + Action: "/settings/counterparty-logos", + CSRFToken: p.CSRFToken, + Label: "Counterparty logos saved", + }) { + + } +
+
+
Publishable token
+ @components.SettingsAutoSaveForm(components.SettingsAutoSaveFormProps{ + Action: "/settings/logo-dev-token", + CSRFToken: p.CSRFToken, + Label: "logo.dev token saved", + }) { + + } +

Publishable key (pk_…) — required for logos to appear. Get a free one at logo.dev. Public by design (it rides in the image URL).

+
+
+ @components.LucideIcon("info", "w-4 h-4 shrink-0 mt-0.5") + If your logo.dev key is domain-restricted, add this Breadbox instance's domain to its allowed referrers (a wildcard like *.example.com covers subdomains). Otherwise logo.dev answers 401 and logos quietly fall back to monograms. +
+
+ @providerDrawerCloseFooter(false) +
+ } +} + // providersSimplefinDrawer holds the SimpleFIN opt-in. SimpleFIN has no // server-level credential — the access token is pasted per connection — so the // only setting is whether the provider is offered when adding a connection. diff --git a/internal/templates/components/pages/providers_types.go b/internal/templates/components/pages/providers_types.go index b5223441..c99fad99 100644 --- a/internal/templates/components/pages/providers_types.go +++ b/internal/templates/components/pages/providers_types.go @@ -42,4 +42,11 @@ type ProvidersProps struct { // Per-provider health summaries (always populated for "plaid", // "teller", "csv" — handler ensures stub entries exist). ProviderHealth map[string]*service.ProviderHealthSummary + + // logo.dev integration (the "API integrations" section) — the + // counterparty brand-logo toggle and its publishable token, relocated + // from the General → Counterparties section so all pluggable APIs live on + // the Providers directory. + CounterpartyLogos bool + LogoDevToken string } diff --git a/internal/templates/components/pages/settings.templ b/internal/templates/components/pages/settings.templ index 1b345a25..44464cb0 100644 --- a/internal/templates/components/pages/settings.templ +++ b/internal/templates/components/pages/settings.templ @@ -27,7 +27,6 @@ templ SettingsGeneral(p SettingsProps) {
@settingsSyncSection(p) @settingsAppearanceSection() - @settingsCounterpartiesSection(p) @settingsAvatarsSection(p) @settingsSyncLogsSection(p)
@@ -59,52 +58,6 @@ templ settingsAppearanceSection() { } } -// --------------------------------------------------------------------- -// General — Counterparties (brand logos) -// --------------------------------------------------------------------- - -// settingsCounterpartiesSection toggles hotlinking real brand logos from -// logo.dev onto counterparty rows and headers. When on, every counterparty -// with a website (and no manual logo) shows its brand logo, hotlinked at render -// time from img.logo.dev — the domain is sent to logo.dev on each render. When -// off (or when logo.dev has no logo), the counterparty falls back to its -// gradient monogram. The optional publishable token lifts rate limits; it's a -// public key (it rides in the image URL), so it's shown in the clear. Both -// controls auto-save on change. -templ settingsCounterpartiesSection(p SettingsProps) { - @components.SettingsSection(components.SettingsSectionProps{ - Icon: "store", - Title: "Counterparties", - Caption: "Brand logos for merchants and other counterparties", - }) { - @components.SettingsRow(components.SettingsRowProps{ - Label: "Counterparty logos", - Caption: "Hotlink brand logos from logo.dev (the domain is sent to logo.dev on render); needs a token below, else falls back to a monogram", - }) { - @components.SettingsAutoSaveForm(components.SettingsAutoSaveFormProps{ - Action: "/settings/counterparty-logos", - CSRFToken: p.CSRFToken, - Label: "Counterparty logos saved", - }) { - - } - } - @components.SettingsRow(components.SettingsRowProps{ - Label: "logo.dev token", - Caption: "Publishable key (pk_…) — required for logos to appear. Get a free token at logo.dev. Public by design.", - Stack: true, - }) { - @components.SettingsAutoSaveForm(components.SettingsAutoSaveFormProps{ - Action: "/settings/logo-dev-token", - CSRFToken: p.CSRFToken, - Label: "logo.dev token saved", - }) { - - } - } - } -} - // SettingsSystem renders the System tab — encryption key status and // runtime/version info. Security folds in here so the admin has one // stop for "this server's posture." diff --git a/internal/templates/components/pages/settings_types.go b/internal/templates/components/pages/settings_types.go index f960777a..702338b6 100644 --- a/internal/templates/components/pages/settings_types.go +++ b/internal/templates/components/pages/settings_types.go @@ -82,11 +82,4 @@ type SettingsProps struct { AvatarUserStyle string AvatarAgentStyle string AvatarStyles []avatar.StyleOption - - // CounterpartyLogos toggles hotlinking real brand logos from logo.dev on - // counterparty rows/headers (default on). LogoDevToken is the optional - // logo.dev publishable key (pk_…) for higher rate limits — public by design, - // so it's shown in the clear. - CounterpartyLogos bool - LogoDevToken string } From 7315e5a43b628e546c523a01da04a4662a9c4402 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 07:00:39 +0000 Subject: [PATCH 2/2] fix(settings): remove domain-restricted banner alert from logo.dev drawer Co-authored-by: Ricardo Canales --- internal/templates/components/pages/providers.templ | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/internal/templates/components/pages/providers.templ b/internal/templates/components/pages/providers.templ index 535ce4a6..98acd141 100644 --- a/internal/templates/components/pages/providers.templ +++ b/internal/templates/components/pages/providers.templ @@ -125,9 +125,7 @@ templ logoDevRow(p ProvidersProps) { // and the publishable token, relocated from the General → Counterparties section // so every pluggable API lives on the Providers directory. Both controls // auto-save on change (settings.js's settingsAutoSave → the 204 endpoints), so -// the drawer has only a Close footer. The referrer note heads off the common -// domain-restriction 401 (a restricted key needs this instance's domain in its -// allowed referrers). +// the drawer has only a Close footer. templ providersLogoDevDrawer(p ProvidersProps) { @components.Drawer(components.DrawerProps{ ID: "provider-logodev", @@ -168,10 +166,6 @@ templ providersLogoDevDrawer(p ProvidersProps) { }

Publishable key (pk_…) — required for logos to appear. Get a free one at logo.dev. Public by design (it rides in the image URL).

-
- @components.LucideIcon("info", "w-4 h-4 shrink-0 mt-0.5") - If your logo.dev key is domain-restricted, add this Breadbox instance's domain to its allowed referrers (a wildcard like *.example.com covers subdomains). Otherwise logo.dev answers 401 and logos quietly fall back to monograms. -
@providerDrawerCloseFooter(false)