From 55a9ae1557939571b5b33b421b552bb12f25372c Mon Sep 17 00:00:00 2001 From: prorochestvo Date: Wed, 12 Aug 2026 09:40:54 +0500 Subject: [PATCH] feat(daemon): add a PI monogram dashboard favicon The browser tab showed the generic placeholder, so the dashboard was unidentifiable among open tabs. Ship a "PI" monogram in JetBrains Mono Bold on the page's own dark tile, glyphs in the dashboard accent blue so the icon matches the product it belongs to. The mark rides inline as a base64 data URI rather than a second embedded asset: the dashboard is one self-contained file whose version always equals the binary, and a separate asset would need its own embed, route and cache policy for no user-visible gain. The serving path is unchanged. The accompanying test asserts the served page carries exactly one icon link whose payload fully decodes as a 32x32 PNG, by marker rather than by byte-exact blob, so the mark can be regenerated without breaking it. Refs: #8 Co-Authored-By: Claude Opus 5 --- gateway/httpapi/ui_test.go | 49 ++++++++++++++++++++++++++++++++++ gateway/httpapi/web/index.html | 1 + 2 files changed, 50 insertions(+) diff --git a/gateway/httpapi/ui_test.go b/gateway/httpapi/ui_test.go index ee38c3b..52aae26 100644 --- a/gateway/httpapi/ui_test.go +++ b/gateway/httpapi/ui_test.go @@ -1,6 +1,9 @@ package httpapi import ( + "bytes" + "encoding/base64" + "image/png" "net/http" "strings" "testing" @@ -73,4 +76,50 @@ func TestHandleDashboard(t *testing.T) { t.Fatal("dashboardHTML is empty: the //go:embed path is broken or the asset is empty") } }) + + // The favicon rides inline in the page rather than as a second embedded + // asset, so nothing but this test stands between a stray edit and a tab + // that silently falls back to the browser placeholder. Asserted by marker, + // never by the exact blob: the mark is regenerated from time to time. + t.Run("the page carries the favicon as an inline PNG data URI", func(t *testing.T) { + t.Parallel() + + srv := newTestServer(&fakeRepo{}, "tok") + body := doRequest(t, srv, http.MethodGet, "/", "tok").Body.String() + + const iconAttr = `rel="icon"` + if n := strings.Count(body, iconAttr); n != 1 { + t.Fatalf("the page has %d %s links, want exactly 1", n, iconAttr) + } + + link := body[strings.Index(body, iconAttr):] + if end := strings.IndexByte(link, '>'); end >= 0 { + link = link[:end] + } + + const marker = "data:image/png;base64," + i := strings.Index(link, marker) + if i < 0 { + t.Fatalf("the icon href is not a %q URI: the favicon must stay inline so the dashboard remains a single self-contained asset", marker) + } + encoded := link[i+len(marker):] + end := strings.IndexByte(encoded, '"') + if end < 0 { + t.Fatal("the icon data URI has no closing quote") + } + + raw, err := base64.StdEncoding.DecodeString(encoded[:end]) + if err != nil { + t.Fatalf("decode the icon payload: %v", err) + } + // Decode in full, not just the header: a truncated blob still carries + // an intact IHDR, so DecodeConfig alone would wave a broken icon through. + img, err := png.Decode(bytes.NewReader(raw)) + if err != nil { + t.Fatalf("the icon payload is not a valid PNG: %v", err) + } + if b := img.Bounds(); b.Dx() != 32 || b.Dy() != 32 { + t.Fatalf("icon is %dx%d, want 32x32", b.Dx(), b.Dy()) + } + }) } diff --git a/gateway/httpapi/web/index.html b/gateway/httpapi/web/index.html index 5547096..cb201f6 100644 --- a/gateway/httpapi/web/index.html +++ b/gateway/httpapi/web/index.html @@ -5,6 +5,7 @@ + pi5 · status