diff --git a/hub/internal/web/embed.go b/hub/internal/web/embed.go index 5a57db7..53f3b13 100644 --- a/hub/internal/web/embed.go +++ b/hub/internal/web/embed.go @@ -10,3 +10,6 @@ var defaultControllerTemplate string //go:embed static/chart.min.js var chartJS []byte + +//go:embed static/fonts/*.woff2 +var fontFS embed.FS diff --git a/hub/internal/web/funcmap_test.go b/hub/internal/web/funcmap_test.go new file mode 100644 index 0000000..afe7134 --- /dev/null +++ b/hub/internal/web/funcmap_test.go @@ -0,0 +1,56 @@ +package web + +import ( + "html/template" + "testing" +) + +// TASK-D4 §7B: statusColor maps every OverallStatus value the hub emits (server.go +// handleDashboard + configs.go + the hosts stale state) to a v2 semantic token consumed +// as a class suffix — never an inline color. Exception-color: healthy fleet = blue/neutral. +func TestStatusColorTruthTable(t *testing.T) { + cases := []struct { + status string + want string + }{ + {"ok", "nominal"}, // operating normally — blue + {"warn", "warn"}, // degraded / stale report — amber + {"stale", "warn"}, // host stale state + {"down", "crit"}, // outage — red + {"fail", "crit"}, // health=fail — red + {"pending", "neutral"}, // not-yet-provisioned customer is a NORMAL fleet state + {"disabled", "neutral"}, // deliberately paused — not a deviation + {"blocked", "warn"}, // operator cut-off: intentional but attention-worthy + {"bogus", "neutral"}, // unknown values stay quiet + {"", "neutral"}, + } + for _, c := range cases { + if got := statusColor(c.status); got != c.want { + t.Errorf("statusColor(%q) = %q, want %q", c.status, got, c.want) + } + } +} + +// TestTemplatesParseWithFuncmap parses all hub templates with the production funcmap — +// catches a typo'd function name introduced during the D4 sweep (did not exist before). +func TestTemplatesParseWithFuncmap(t *testing.T) { + s := New(nil, "", "", "test", 0, nil) + _ = s // New itself template.Must-parses; reaching here means parse succeeded + // belt and braces: parse again explicitly + if _, err := template.New("").Funcs(template.FuncMap{ + "timeAgo": timeAgo, + "timeAgoPtr": func(v interface{}) string { return "" }, + "statusColor": statusColor, + "formatFloat": func(f float64) string { return "" }, + "joinStrings": func(s []string, sep string) string { return "" }, + "json": func(v interface{}) template.JS { return "" }, + "hubVersion": func() string { return "" }, + "add": func(a, b int) int { return 0 }, + "mapGet": func(m map[string]int, k string) int { return 0 }, + "memoryColor": memoryColor, + "accuracyClass": accuracyClass, + "gt": func(a, b int) bool { return false }, + }).ParseFS(templateFS, "templates/*.html"); err != nil { + t.Fatalf("templates failed to parse: %v", err) + } +} diff --git a/hub/internal/web/server.go b/hub/internal/web/server.go index c3904a2..2dd3764 100644 --- a/hub/internal/web/server.go +++ b/hub/internal/web/server.go @@ -73,7 +73,6 @@ func New(store *store.Store, passwordHash, apiKey, version string, staleThreshol return timeAgo(*t) }, "statusColor": statusColor, - "statusIcon": statusIcon, "formatFloat": func(f float64) string { return fmt.Sprintf("%.0f", f) }, "joinStrings": func(s []string, sep string) string { return strings.Join(s, sep) }, "json": func(v interface{}) template.JS { @@ -204,6 +203,17 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/javascript") w.Header().Set("Cache-Control", "public, max-age=86400") w.Write(chartJS) + case strings.HasPrefix(path, "/static/fonts/"): + // Vendored brand fonts (design system v2) — embedded, no CDN. + name := strings.TrimPrefix(path, "/static/fonts/") + data, err := fontFS.ReadFile("static/fonts/" + name) + if err != nil { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "font/woff2") + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + w.Write(data) case path == "/configuration": if r.Method == http.MethodPost { s.handleConfigurationAction(w, r) @@ -608,25 +618,32 @@ func timeAgo(t time.Time) string { return fmt.Sprintf("%dd ago", days) } +// statusColor maps a status value to a design-system-v2 semantic token, consumed as a +// class suffix (status-dot-nominal, tag-nominal, …) — never as an inline color (D4). +// Exception-color principle: a healthy fleet is blue/neutral; amber/red only on deviation. +// ok -> nominal (blue — operating normally) +// warn, stale -> warn (amber — degraded / stale report) +// down, fail -> crit (red — outage) +// pending -> neutral (a not-yet-provisioned customer is a normal fleet state) +// disabled -> neutral (deliberately paused — not a deviation) +// blocked -> warn (an operator cut a customer off: intentional but attention-worthy) func statusColor(status string) string { switch status { case "ok": - return "#4ade80" // green - case "warn": - return "#facc15" // yellow + return "nominal" + case "warn", "stale": + return "warn" case "down", "fail": - return "#f87171" // red - case "disabled", "pending", "blocked": - return "#94a3b8" // gray + return "crit" + case "blocked": + return "warn" + case "disabled", "pending": + return "neutral" default: - return "#94a3b8" // gray + return "neutral" } } -func statusIcon(status string) string { - return "●" -} - // handleConfiguration renders the Configuration page. func (s *Server) handleConfiguration(w http.ResponseWriter, r *http.Request) { csrfToken := s.getCSRFToken(r) diff --git a/hub/internal/web/static/fonts/jbm-latin-ext.woff2 b/hub/internal/web/static/fonts/jbm-latin-ext.woff2 new file mode 100644 index 0000000..82f9668 Binary files /dev/null and b/hub/internal/web/static/fonts/jbm-latin-ext.woff2 differ diff --git a/hub/internal/web/static/fonts/jbm-latin.woff2 b/hub/internal/web/static/fonts/jbm-latin.woff2 new file mode 100644 index 0000000..4d09cda Binary files /dev/null and b/hub/internal/web/static/fonts/jbm-latin.woff2 differ diff --git a/hub/internal/web/static/fonts/pjs-latin-ext.woff2 b/hub/internal/web/static/fonts/pjs-latin-ext.woff2 new file mode 100644 index 0000000..f82597c Binary files /dev/null and b/hub/internal/web/static/fonts/pjs-latin-ext.woff2 differ diff --git a/hub/internal/web/static/fonts/pjs-latin.woff2 b/hub/internal/web/static/fonts/pjs-latin.woff2 new file mode 100644 index 0000000..a180dc4 Binary files /dev/null and b/hub/internal/web/static/fonts/pjs-latin.woff2 differ diff --git a/hub/internal/web/templates/app_detail.html b/hub/internal/web/templates/app_detail.html index a556138..2778e92 100644 --- a/hub/internal/web/templates/app_detail.html +++ b/hub/internal/web/templates/app_detail.html @@ -8,6 +8,7 @@
+ {{template "icon_sprite"}}