From 5112425673eb9c55840cb4cea34b66ff68d94315 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Mon, 24 Aug 2026 17:08:40 -0500 Subject: [PATCH] appview: make the footer Bluesky link configurable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The link was hardcoded before fa34da0 too — it pointed at bsky.app/profile/atcr.io. That commit was right to switch to a DID, since handles change and a stale handle link breaks silently, but the DID went into components/footer.html, a shared template. Every self-hoster's footer therefore links to the project's Bluesky account. Now ui.bluesky_profile, following source_url in the same footer exactly: config field with a default, plumbed through UIDependencies and PageData, and guarded with {{ with }} so an unset value omits the link rather than rendering something wrong. It takes a handle or a DID; the comment says to prefer a DID for the reason fa34da0 changed it. Defaulting to the project account matches source_url's logic — both name the upstream project rather than the operator — and self-hosters who want their own or none set one line. The aria-label switched to $.ClientShortName: `with` rebinds the dot, so the label would otherwise have silently rendered empty. Two things worth recording: * `{{ with }}` hides the link on an empty value, but you cannot get an empty value from the environment. Viper runs with AllowEmptyEnv(false), so an empty env var reads as unset and the default wins. Only "" in YAML works. That applies to every string field in this config, not just this one, and the comment now says so. * config-appview.example.yaml must NOT be regenerated with `config init`, despite what the checklist in CLAUDE.md says. The file is hand-curated well past the defaults, and regenerating replaces real Stripe price IDs with price_xxx placeholders and blanks registry_domains, managed_holds, theme and the tier names. Added by hand instead. Verified live both ways: the configured value renders, and `bluesky_profile: ""` in YAML drops the link while leaving the Source link intact. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SeaUS5AFPX9gqCahoLRMRh --- config-appview.example.yaml | 2 ++ pkg/appview/config.go | 5 +++++ pkg/appview/handlers/base.go | 1 + pkg/appview/handlers/common.go | 2 ++ pkg/appview/routes/routes.go | 2 ++ pkg/appview/server.go | 1 + pkg/appview/templates/components/footer.html | 4 +++- 7 files changed, 16 insertions(+), 1 deletion(-) diff --git a/config-appview.example.yaml b/config-appview.example.yaml index 7176efe..73932b1 100644 --- a/config-appview.example.yaml +++ b/config-appview.example.yaml @@ -50,6 +50,8 @@ ui: libsql_sync_interval: 1m0s # Source code URL displayed in the footer "Source" link. Defaults to the upstream ATCR project. source_url: https://tangled.org/evan.jarrett.net/at-container-registry + # Bluesky handle or DID linked from the footer. Prefer a DID: handles can change. Defaults to the ATCR project account. Set to "" in YAML to hide the link (an empty env var will not do it: Viper treats it as unset). + bluesky_profile: did:plc:wfj5kyialpmcv2fzk6uqwsln # Health check and cache settings. health: # How long to cache hold health check results. diff --git a/pkg/appview/config.go b/pkg/appview/config.go index 39a52d4..c2bfc2e 100644 --- a/pkg/appview/config.go +++ b/pkg/appview/config.go @@ -96,6 +96,10 @@ type UIConfig struct { // Source code URL displayed in the footer "Source" link. SourceURL string `yaml:"source_url" comment:"Source code URL displayed in the footer \"Source\" link. Defaults to the upstream ATCR project."` + + // Bluesky profile linked from the footer. Handle or DID; prefer a DID, + // since handles change and a stale handle link breaks silently. + BlueskyProfile string `yaml:"bluesky_profile" comment:"Bluesky handle or DID linked from the footer. Prefer a DID: handles can change. Defaults to the ATCR project account. Set to \"\" in YAML to hide the link (an empty env var will not do it: Viper treats it as unset)."` } // HealthConfig defines health check and cache settings @@ -236,6 +240,7 @@ func setDefaults(v *viper.Viper) { v.SetDefault("ui.libsql_auth_token", "") v.SetDefault("ui.libsql_sync_interval", "60s") v.SetDefault("ui.source_url", "https://tangled.org/evan.jarrett.net/at-container-registry") + v.SetDefault("ui.bluesky_profile", "did:plc:wfj5kyialpmcv2fzk6uqwsln") // Health defaults v.SetDefault("health.cache_ttl", "15m") diff --git a/pkg/appview/handlers/base.go b/pkg/appview/handlers/base.go index 9bd9a0f..83fe4d2 100644 --- a/pkg/appview/handlers/base.go +++ b/pkg/appview/handlers/base.go @@ -53,6 +53,7 @@ type BaseUIHandler struct { AIAdvisorEnabled bool // True when billing is fully configured AND Claude API key is set BillingEnabled bool // True when the billing build is compiled in and Stripe is configured SourceURL string // Source code URL for the footer "Source" link + BlueskyProfile string // Bluesky handle or DID for the footer link ("" hides it) } // IsManagedHold reports whether a hold DID is one of the appview's managed diff --git a/pkg/appview/handlers/common.go b/pkg/appview/handlers/common.go index d1bc30d..19e1573 100644 --- a/pkg/appview/handlers/common.go +++ b/pkg/appview/handlers/common.go @@ -20,6 +20,7 @@ type PageData struct { OciClient string // Preferred OCI client for pull commands (e.g., "docker", "podman") AIAdvisorEnabled bool // True when AI Image Advisor is available SourceURL string // Source code URL for the footer "Source" link + BlueskyProfile string // Bluesky handle or DID for the footer link ("" hides it) CurrentPath string // Request path (used for OAuth return_to) } @@ -51,6 +52,7 @@ func NewPageData(r *http.Request, h *BaseUIHandler) PageData { OciClient: ociClient, AIAdvisorEnabled: h.AIAdvisorEnabled, SourceURL: h.SourceURL, + BlueskyProfile: h.BlueskyProfile, CurrentPath: r.URL.RequestURI(), } } diff --git a/pkg/appview/routes/routes.go b/pkg/appview/routes/routes.go index f05bd75..85dc097 100644 --- a/pkg/appview/routes/routes.go +++ b/pkg/appview/routes/routes.go @@ -48,6 +48,7 @@ type UIDependencies struct { WebhookDispatcher *webhooks.Dispatcher // Webhook dispatcher (nil if not configured) ClaudeAPIKey string // Anthropic API key for AI advisor (empty = disabled) SourceURL string // Source code URL for the footer "Source" link + BlueskyProfile string // Bluesky handle or DID for the footer link ("" hides it) } // RegisterUIRoutes registers all web UI and API routes on the provided router @@ -88,6 +89,7 @@ func RegisterUIRoutes(router chi.Router, deps UIDependencies) { BillingEnabled: deps.BillingManager != nil && deps.BillingManager.Enabled(), AIAdvisorEnabled: deps.BillingManager != nil && deps.BillingManager.Enabled() && deps.ClaudeAPIKey != "", SourceURL: deps.SourceURL, + BlueskyProfile: deps.BlueskyProfile, } // OAuth login routes (public) diff --git a/pkg/appview/server.go b/pkg/appview/server.go index afa07ee..b5bb4f7 100644 --- a/pkg/appview/server.go +++ b/pkg/appview/server.go @@ -412,6 +412,7 @@ func NewAppViewServer(cfg *Config, branding *BrandingOverrides) (*AppViewServer, WebhookDispatcher: s.WebhookDispatcher, ClaudeAPIKey: cfg.AI.APIKey, SourceURL: cfg.UI.SourceURL, + BlueskyProfile: cfg.UI.BlueskyProfile, LegalConfig: routes.LegalConfig{ CompanyName: cfg.Legal.CompanyName, Jurisdiction: cfg.Legal.Jurisdiction, diff --git a/pkg/appview/templates/components/footer.html b/pkg/appview/templates/components/footer.html index e8190f0..6a66a40 100644 --- a/pkg/appview/templates/components/footer.html +++ b/pkg/appview/templates/components/footer.html @@ -5,11 +5,13 @@ Privacy Terms + {{ with .BlueskyProfile }} - + Bluesky + {{ end }} {{ with .SourceURL }}