feat(admin): redesign dashboard and make cluster view a real control

Replace the AI-slop visual (indigo->purple gradient, glassmorphism, uniform rounded cards, hover-lift) with a deliberate warm-paper 'instrument panel': monospace tabular telemetry, GraphQL brand magenta accent, flat hairline cards, real type hierarchy, WCAG-AA contrast, focus-visible, prefers-reduced-motion and aria-live status. No new font/CDN deps.

Make the cluster-view toggle functional: the WebSocket stats stream now honours a ?view=local|cluster query param, so toggling streams this-node-only vs aggregated metrics instead of being cosmetic (the stream previously always sent cluster-aware data when an aggregator existed). The toggle is always visible -- disabled with a 'single node' hint when Redis cluster mode is off -- so it is discoverable.

Fix cluster-mode rendering bugs: derive backend health from healthy/total instances (health card no longer stuck on 'Loading'), show 'n/a' for Redis memory instead of a misleading 0 MB, render aggregated CB/retry/coalescing/connections on the polling path too, and surface the proxy version and backend-health detail.

Tests assert the served dashboard against stable markers instead of the cosmetic page title.
This commit is contained in:
2026-06-21 02:48:20 +01:00
parent 1bff79e4f4
commit 01d1de1f0b
3 changed files with 968 additions and 1345 deletions
+949 -1339
View File
File diff suppressed because it is too large Load Diff
+14 -4
View File
@@ -670,6 +670,16 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
return nil
})
// Determine the requested view. "local" forces this-instance stats; any
// other value (default) is cluster-aware and returns aggregated metrics
// when a metrics aggregator is available. This makes the dashboard's
// "Cluster view" toggle a real control rather than cosmetic, since the
// WebSocket stream is the primary data path.
gather := ad.gatherAllStatsClusterAware
if c.Query("view") == "local" {
gather = ad.gatherAllStats
}
// Channel to signal when to stop
done := make(chan struct{})
@@ -694,8 +704,8 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
// Send initial stats immediately (cluster-aware for dashboard)
if stats := ad.gatherAllStatsClusterAware(); stats != nil {
// Send initial stats immediately (honours the requested view)
if stats := gather(); stats != nil {
buf.Reset()
if err := enc.Encode(stats); err == nil {
// json.Encoder.Encode appends a trailing newline; strip it
@@ -708,8 +718,8 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
for {
select {
case <-ticker.C:
// Gather all stats (cluster-aware for dashboard)
stats := ad.gatherAllStatsClusterAware()
// Gather all stats (honours the requested view)
stats := gather()
// Encode into reused buffer (no per-tick allocation churn)
buf.Reset()
+5 -2
View File
@@ -76,10 +76,13 @@ func TestAdminDashboard_ServeDashboard(t *testing.T) {
contentType := resp.Header.Get("Content-Type")
assert.Contains(t, contentType, "text/html")
// Verify HTML content is returned
// Verify the dashboard HTML is returned (assert on stable markers, not
// the cosmetic page title which is part of the UI and may change).
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Contains(t, string(body), "GraphQL Proxy Admin Dashboard")
assert.Contains(t, string(body), "<!DOCTYPE html>")
assert.Contains(t, string(body), "graphql-proxy")
assert.Contains(t, string(body), "/admin/ws/stats")
}
func TestAdminDashboard_GetStats(t *testing.T) {