From 8ecea8dbdf338c9559aae4ddfdc8584397172091 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 26 Mar 2026 10:48:42 +0000 Subject: [PATCH] fix(cloudcp): serve control plane favicon --- internal/cloudcp/portal/handlers_test.go | 2 + internal/cloudcp/portal/templates/portal.html | 1 + internal/cloudcp/routes.go | 2 + internal/cloudcp/routes_favicon_test.go | 60 +++++++++++++++++++ internal/cloudcp/static_assets.go | 33 ++++++++++ 5 files changed, 98 insertions(+) create mode 100644 internal/cloudcp/routes_favicon_test.go create mode 100644 internal/cloudcp/static_assets.go diff --git a/internal/cloudcp/portal/handlers_test.go b/internal/cloudcp/portal/handlers_test.go index 821067264d..7381cc4f39 100644 --- a/internal/cloudcp/portal/handlers_test.go +++ b/internal/cloudcp/portal/handlers_test.go @@ -781,6 +781,7 @@ func TestPortalPageTemplate_AccountServicesRendered(t *testing.T) { mustContain := []string{ "Pulse Account", + ``, `id="portal-user-info"`, `id="portal-app-root"`, `id="pulse-account-bootstrap"`, @@ -1074,6 +1075,7 @@ func TestPortalPageTemplate_UsesPulseAccountBrandingWhenSignedOut(t *testing.T) mustContain := []string{ "Pulse Account", + ``, "Pulse Account", `id="portal-app-root"`, "Enter the commercial email address for your Pulse account.", diff --git a/internal/cloudcp/portal/templates/portal.html b/internal/cloudcp/portal/templates/portal.html index 183550a363..5b76b6f0f9 100644 --- a/internal/cloudcp/portal/templates/portal.html +++ b/internal/cloudcp/portal/templates/portal.html @@ -4,6 +4,7 @@ Pulse Account + diff --git a/internal/cloudcp/routes.go b/internal/cloudcp/routes.go index f1737b9163..ea5134a3b4 100644 --- a/internal/cloudcp/routes.go +++ b/internal/cloudcp/routes.go @@ -69,6 +69,8 @@ func RegisterRoutes(mux *http.ServeMux, deps *Deps) { // Health / readiness are unauthenticated liveness/readiness probes. mux.HandleFunc("/healthz", admin.HandleHealthz) mux.HandleFunc("/readyz", admin.HandleReadyz(deps.Registry)) + mux.HandleFunc("/favicon.svg", handleControlPlaneFaviconSVG) + mux.HandleFunc("/favicon.ico", handleControlPlaneFaviconICO) // Status and metrics are private by default. statusHandler := http.HandlerFunc(admin.HandleStatus(deps.Registry, deps.Version)) diff --git a/internal/cloudcp/routes_favicon_test.go b/internal/cloudcp/routes_favicon_test.go new file mode 100644 index 0000000000..f3442e25dc --- /dev/null +++ b/internal/cloudcp/routes_favicon_test.go @@ -0,0 +1,60 @@ +package cloudcp + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/registry" +) + +func TestRegisterRoutes_ControlPlaneFavicons(t *testing.T) { + dir := t.TempDir() + reg, err := registry.NewTenantRegistry(dir) + if err != nil { + t.Fatalf("NewTenantRegistry: %v", err) + } + t.Cleanup(func() { _ = reg.Close() }) + trialStore, err := NewTrialSignupStore(dir) + if err != nil { + t.Fatalf("NewTrialSignupStore: %v", err) + } + t.Cleanup(func() { trialStore.Close() }) + + mux := http.NewServeMux() + RegisterRoutes(mux, &Deps{ + Config: &CPConfig{ + DataDir: dir, + AdminKey: "test-admin-key", + BaseURL: "https://cloud.example.com", + StripeWebhookSecret: "whsec_test", + }, + Registry: reg, + TrialSignupStore: trialStore, + Version: "test", + }) + + svgReq := httptest.NewRequest(http.MethodGet, "/favicon.svg", nil) + svgRec := httptest.NewRecorder() + mux.ServeHTTP(svgRec, svgReq) + if svgRec.Code != http.StatusOK { + t.Fatalf("GET /favicon.svg status=%d, want %d", svgRec.Code, http.StatusOK) + } + if got := svgRec.Header().Get("Content-Type"); got != "image/svg+xml" { + t.Fatalf("GET /favicon.svg content-type=%q, want %q", got, "image/svg+xml") + } + if !strings.Contains(svgRec.Body.String(), " + + + + + +` + +func handleControlPlaneFaviconSVG(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + w.Header().Set("Content-Type", "image/svg+xml") + w.Header().Set("Cache-Control", "public, max-age=86400") + w.WriteHeader(http.StatusOK) + if r.Method == http.MethodHead { + return + } + _, _ = w.Write([]byte(controlPlaneFaviconSVG)) +} + +func handleControlPlaneFaviconICO(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + http.Redirect(w, r, "/favicon.svg", http.StatusMovedPermanently) +}