Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions internal/admin/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
}
Expand Down
6 changes: 0 additions & 6 deletions internal/admin/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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 {
Expand Down
101 changes: 97 additions & 4 deletions internal/templates/components/pages/providers.templ
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ import (
// drawer scopes it via `x-data="providerCard" data-provider="..."`.
templ Providers(p ProvidersProps) {
<script src="/static/js/admin/components/providers.js"></script>
<!-- settings.js registers the `settingsAutoSave` Alpine factory the logo.dev
drawer's toggle + token use (each saves on change with a toast). -->
<script src="/static/js/admin/components/settings.js"></script>
<div>
@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",
Expand Down Expand Up @@ -69,16 +72,106 @@ 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)
</div>
}

// 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) {
<button
type="button"
class="bb-settings-row bb-settings-row--link w-full text-left"
@click="$store.drawers.open('provider-logodev')"
>
@components.LucideIcon("image", "w-5 h-5 text-base-content opacity-60 shrink-0")
<div class="bb-settings-row__main">
<div class="bb-settings-row__label">logo.dev</div>
<p class="bb-settings-row__caption">Brand logos for counterparties — merchants, people, employers</p>
</div>
<div class="bb-settings-row__control">
<div class="flex items-center gap-2.5">
if p.CounterpartyLogos && p.LogoDevToken != "" {
<span class="inline-flex items-center gap-1.5 text-xs text-base-content/60 whitespace-nowrap">
<span class="status status-sm status-success"></span>
On
</span>
} else {
<span class="text-xs text-base-content/40 whitespace-nowrap">Off</span>
}
@components.LucideIcon("chevron-right", "w-4 h-4 text-base-content opacity-40 shrink-0")
</div>
</div>
</button>
}

// 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.
templ providersLogoDevDrawer(p ProvidersProps) {
@components.Drawer(components.DrawerProps{
ID: "provider-logodev",
Title: "logo.dev",
Size: "md",
}) {
<div class="flex flex-col h-full">
@components.DrawerHeader(components.DrawerHeaderProps{
Icon: "image",
Title: "logo.dev",
Subtitle: "Brand logos for counterparties",
})
<div class="flex-1 overflow-y-auto p-5 space-y-5">
<p class="text-sm text-base-content/70 leading-relaxed">
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.
</p>
<div class="flex items-center justify-between gap-3">
<div class="min-w-0">
<div class="text-sm font-medium">Counterparty logos</div>
<p class="text-xs text-base-content/55 mt-0.5">The counterparty's domain is sent to logo.dev on each render.</p>
</div>
@components.SettingsAutoSaveForm(components.SettingsAutoSaveFormProps{
Action: "/settings/counterparty-logos",
CSRFToken: p.CSRFToken,
Label: "Counterparty logos saved",
}) {
<input type="checkbox" name="counterparty_logos" value="true" class="toggle toggle-sm toggle-primary" checked?={ p.CounterpartyLogos } aria-label="Enable counterparty brand logos"/>
}
</div>
<div>
<div class="text-sm font-medium mb-1.5">Publishable token</div>
@components.SettingsAutoSaveForm(components.SettingsAutoSaveFormProps{
Action: "/settings/logo-dev-token",
CSRFToken: p.CSRFToken,
Label: "logo.dev token saved",
}) {
<input type="text" name="logo_dev_token" value={ p.LogoDevToken } placeholder="pk_… (paste to enable logos)" autocomplete="off" spellcheck="false" class="input input-sm w-full font-mono" aria-label="logo.dev publishable token"/>
}
<p class="text-xs text-base-content/55 mt-1.5">Publishable key (<code>pk_…</code>) — required for logos to appear. Get a free one at <a href="https://logo.dev" target="_blank" rel="noopener" class="link link-hover">logo.dev</a>. Public by design (it rides in the image URL).</p>
</div>
</div>
@providerDrawerCloseFooter(false)
</div>
}
}

// 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.
Expand Down
7 changes: 7 additions & 0 deletions internal/templates/components/pages/providers_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
47 changes: 0 additions & 47 deletions internal/templates/components/pages/settings.templ
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ templ SettingsGeneral(p SettingsProps) {
<div>
@settingsSyncSection(p)
@settingsAppearanceSection()
@settingsCounterpartiesSection(p)
@settingsAvatarsSection(p)
@settingsSyncLogsSection(p)
</div>
Expand Down Expand Up @@ -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",
}) {
<input type="checkbox" name="counterparty_logos" value="true" class="toggle toggle-sm toggle-primary" checked?={ p.CounterpartyLogos } aria-label="Enable counterparty brand logos"/>
}
}
@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",
}) {
<input type="text" name="logo_dev_token" value={ p.LogoDevToken } placeholder="pk_… (paste to enable logos)" autocomplete="off" spellcheck="false" class="input input-sm w-full font-mono" aria-label="logo.dev publishable token"/>
}
}
}
}

// 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."
Expand Down
7 changes: 0 additions & 7 deletions internal/templates/components/pages/settings_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading