From b4cedfdfc5ae7c04381d13dc87e320e302fa8a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Alvarez?= Date: Tue, 26 May 2026 20:46:49 -0600 Subject: [PATCH 1/3] Polish request-log rows and fix uncolored method pills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The METHOD column rendered several event types as bare, uncolored text because only HTTP verbs and a subset of socket events had color variants. Add color coverage for the socket lifecycle events that were missing (DROP, LIVE_CONNECT, LIVE_RECONNECT, DISPATCH_BURST) plus HTTP HEAD/OPTIONS, and give .method a neutral tinted fallback so no method can ever render without a pill background. Also fix layout inconsistencies in the log table: - Widen the METHOD column (62px → 104px) and add white-space: nowrap so long socket event names (LIVE_RECONNECT, DISPATCH_BURST) no longer wrap or overflow their cell. - Unify pill sizing: matching padding and a min-width on .tag-type so the TYPE and METHOD pills align. - Give rows breathing room (height 32px → min-height 36px, padding and gap bumped) for a more consistent vertical rhythm. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/index.css | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index ae10ba8..4ba3cbc 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -425,14 +425,21 @@ svg { font-family: var(--font-mono); font-size: 10px; font-weight: 700; - padding: 2px 5px; + padding: 2px 7px; border-radius: 3px; letter-spacing: 0.02em; min-width: 42px; text-align: center; display: inline-block; + white-space: nowrap; + /* Neutral fallback so any method without a specific variant still + renders as a tinted pill instead of bare, uncolored text. */ + color: var(--fg-2); + background: color-mix(in oklch, var(--fg-2) 12%, transparent); } -.method.GET { +.method.GET, +.method.HEAD, +.method.OPTIONS { color: var(--proxy); background: color-mix(in oklch, var(--proxy) 14%, transparent); } @@ -690,11 +697,11 @@ svg { .log-row-head, .log-row { display: grid; - grid-template-columns: 78px 70px 62px 1fr 70px 80px 70px; - gap: 10px; - padding: 0 14px; + grid-template-columns: 78px 72px 104px 1fr 70px 80px 70px; + gap: 12px; + padding: 7px 16px; align-items: center; - height: 32px; + min-height: 36px; border-bottom: 1px solid color-mix(in oklch, var(--line) 60%, transparent); } .log-row-head { @@ -745,12 +752,14 @@ svg { .tag-type { font-size: 10px; font-weight: 700; - padding: 2px 6px; + padding: 2px 7px; border-radius: 3px; + min-width: 52px; text-align: center; letter-spacing: 0.04em; font-family: var(--font-mono); display: inline-block; + white-space: nowrap; } .tag-type.MOCK { color: var(--mock); @@ -774,10 +783,23 @@ svg { .method.SUBSCRIBE, .method.UNSUBSCRIBE, .method.DISPATCH, -.method.ERROR { +.method.DISPATCH_BURST { color: var(--accent); background: color-mix(in oklch, var(--accent) 14%, transparent); } +.method.LIVE_CONNECT { + color: var(--ok); + background: color-mix(in oklch, var(--ok) 14%, transparent); +} +.method.LIVE_RECONNECT { + color: var(--warn); + background: color-mix(in oklch, var(--warn) 14%, transparent); +} +.method.DROP, +.method.ERROR { + color: var(--err); + background: color-mix(in oklch, var(--err) 14%, transparent); +} .status-200, .status-201, From 86a51a977a6e16304e8f41ee28d4308b73ffce87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Alvarez?= Date: Tue, 26 May 2026 20:50:51 -0600 Subject: [PATCH 2/3] Default channels to mixed mode and forward proxy headers to live upstream - Introduce DefaultChannelMode (mixed) and use it everywhere a channel lacks an explicit config, so channels both dispatch locally and forward to the live target by default instead of defaulting to mock-only. - Forward standard reverse-proxy headers (X-Forwarded-For/-Host/-Proto, X-Real-IP) when dialing the live target, and stop stripping Origin so the upstream sees the same request the client would send directly. - Add a LIVE_DIAL log event summarizing the dial (target, client host/addr, subprotocols, redacted headers) to make upstream connection issues debuggable without leaking credentials. Co-Authored-By: Claude Opus 4.7 (1M context) --- channel_modes.go | 11 +++- channel_modes_test.go | 4 +- frontend/src/components/Sidebar.tsx | 6 +- frontend/src/components/SocketPanel.tsx | 2 +- frontend/src/stores/useChannelModeStore.ts | 2 +- socket.go | 47 +++++++++++++- ws_proxy.go | 74 +++++++++++++++++++++- ws_proxy_test.go | 14 +++- 8 files changed, 146 insertions(+), 14 deletions(-) diff --git a/channel_modes.go b/channel_modes.go index 8ee805a..c5e7484 100644 --- a/channel_modes.go +++ b/channel_modes.go @@ -24,6 +24,11 @@ const ( ModeMixed ChannelMode = "mixed" ) +// DefaultChannelMode is applied to channels that do not yet have an explicit +// configuration. Mixed lets the dashboard dispatch locally while still +// forwarding client frames to the live target when one is configured. +const DefaultChannelMode = ModeMixed + type ChannelConfig struct { Channel string `json:"channel"` Mode ChannelMode `json:"mode"` @@ -72,7 +77,7 @@ func (r *ChannelModeRegistry) Get(channel string) ChannelConfig { if ok { return cfg } - return ChannelConfig{Channel: channel, Mode: ModeMock} + return ChannelConfig{Channel: channel, Mode: DefaultChannelMode} } func (r *ChannelModeRegistry) Set(cfg ChannelConfig) error { @@ -101,7 +106,7 @@ func (r *ChannelModeRegistry) Set(cfg ChannelConfig) error { } } if cfg.Mode == "" { - cfg.Mode = ModeMock + cfg.Mode = DefaultChannelMode } r.channels[cfg.Channel] = cfg snapshot := r.snapshotLocked() @@ -140,7 +145,7 @@ func (r *ChannelModeRegistry) Delete(channel string) error { r.mu.Unlock() listenerCfg := previous - listenerCfg.Mode = ModeMock + listenerCfg.Mode = DefaultChannelMode listenerCfg.UpdatedAt = now eventCfg := listenerCfg diff --git a/channel_modes_test.go b/channel_modes_test.go index 7f13c5f..8b39ffa 100644 --- a/channel_modes_test.go +++ b/channel_modes_test.go @@ -18,8 +18,8 @@ func TestChannelModeRegistryGetSetSnapshotAndPersistence(t *testing.T) { if err != nil { t.Fatalf("NewChannelModeRegistry() error = %v", err) } - if got := reg.Get("/scores").Mode; got != ModeMock { - t.Fatalf("default mode = %s, want mock", got) + if got := reg.Get("/scores").Mode; got != DefaultChannelMode { + t.Fatalf("default mode = %s, want %s", got, DefaultChannelMode) } if err := reg.Set(ChannelConfig{Channel: "/scores", Mode: ModeMixed, RateCapHz: 25}); err != nil { t.Fatalf("Set() error = %v", err) diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index e06658f..28a0bd6 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -329,7 +329,7 @@ function SavedChannelsPanel({ showToast }: { showToast: (message: string, kind?: const confirm = useConfirm() const [adding, setAdding] = useState(false) const [channel, setChannel] = useState('') - const [mode, setMode] = useState('mock') + const [mode, setMode] = useState('mixed') const [saving, setSaving] = useState(false) const savedChannels = useMemo( @@ -339,7 +339,7 @@ function SavedChannelsPanel({ showToast }: { showToast: (message: string, kind?: const resetForm = useCallback(() => { setChannel('') - setMode('mock') + setMode('mixed') setAdding(false) }, []) @@ -452,7 +452,7 @@ function SavedChannelsPanel({ showToast }: { showToast: (message: string, kind?: {cfg.channel} - {cfg.mode || 'mock'} + {cfg.mode || 'mixed'}