Skip to content

Commit a1d744d

Browse files
sarg3ntclaude
andcommitted
address Copilot review findings on PR #146
- Add statusMonitor.setAndBroadcast that always broadcasts to SSE subscribers; PokeBox now uses it so a config edit that flips only a non-level field (ConsoleEnabled, Enabled, Name, …) still pushes to open /bx tabs. The chatter-suppression in set() is desirable for the steady-state poll loop but defeats the purpose of an out-of-band config-change refresh. - PokeBox now distinguishes transient DB errors from "not found": on err != nil we log and keep the last-known-good snapshot; only box == nil drops the cached entry. Avoids poisoning the UI when SQLite hiccups. - Gear.Start stores the unsubscribe func returned by Subscribe and Gear.Stop now calls it before shutting the monitor down. Closes the events-adapter forwarder goroutine cleanly and stops new PokeBox calls from reaching a winding-down monitor. - Subscribe call now references events.EventTypeBoxConfigChanged (cast to string for the gear.EventPublisher API) instead of a raw "box.config_changed" literal — keeps publisher and subscriber on the same constant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e45b10d commit a1d744d

2 files changed

Lines changed: 44 additions & 10 deletions

File tree

gearbox/internal/gears/bx/gear.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/a-h/templ"
1717
"github.com/go-chi/chi/v5"
1818

19+
"github.com/sarg3nt/gearbox/internal/framework/events"
1920
"github.com/sarg3nt/gearbox/internal/framework/gear"
2021
)
2122

@@ -26,8 +27,9 @@ func init() {
2627
// Gear implements the Bx fleet-view gear.
2728
type Gear struct {
2829
gear.BaseGear
29-
handlers *Handlers
30-
monitor *statusMonitor
30+
handlers *Handlers
31+
monitor *statusMonitor
32+
configChangeUnsub func() // returned by EventHub.Subscribe; called in Stop
3133
}
3234

3335
// Info returns gear metadata.
@@ -66,7 +68,7 @@ func (g *Gear) Start(ctx context.Context) error {
6668
}
6769
if g.monitor != nil {
6870
if hub := g.GetEventHub(); hub != nil {
69-
hub.Subscribe("box.config_changed", func(e gear.Event) {
71+
g.configChangeUnsub = hub.Subscribe(string(events.EventTypeBoxConfigChanged), func(e gear.Event) {
7072
if e.ServerID == "" {
7173
return
7274
}
@@ -77,8 +79,15 @@ func (g *Gear) Start(ctx context.Context) error {
7779
return nil
7880
}
7981

80-
// Stop signals the background poller to wind down.
82+
// Stop signals the background poller to wind down and releases the
83+
// event subscription so the forwarder goroutine inside the events
84+
// adapter exits cleanly and no further PokeBox calls reach a
85+
// shutting-down monitor.
8186
func (g *Gear) Stop(ctx context.Context) error {
87+
if g.configChangeUnsub != nil {
88+
g.configChangeUnsub()
89+
g.configChangeUnsub = nil
90+
}
8291
if g.monitor != nil {
8392
g.monitor.Stop()
8493
}

gearbox/internal/gears/bx/status.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,24 @@ func (m *statusMonitor) run(ctx context.Context) {
121121
// box-config-changed event subscriber so settings edits (enable toggle,
122122
// console-enabled toggle, agent URL change, …) reflect on /bx without
123123
// waiting on the next 30s tick. Safe to call concurrently with the
124-
// regular poll loop — m.set is mutex-guarded.
124+
// regular poll loop — set/setAndBroadcast are mutex-guarded.
125125
func (m *statusMonitor) PokeBox(ctx context.Context, boxID string) {
126126
if m.db == nil {
127127
return
128128
}
129129
box, err := m.db.GetBoxByBoxID(boxID)
130-
if err != nil || box == nil {
131-
// Box was deleted, or the DB call failed — drop any stale
132-
// snapshot for that ID so /bx renders default-state rather
133-
// than the last-known-good values for a now-missing box.
130+
if err != nil {
131+
// Transient DB error — don't poison the snapshot by dropping
132+
// last-known-good values. Log and bail; the next 30s poll
133+
// will reconcile.
134+
if m.deps.Logger != nil {
135+
m.deps.Logger.Warn("bx PokeBox: db lookup failed; preserving last-known status", "box_id", boxID, "error", err)
136+
}
137+
return
138+
}
139+
if box == nil {
140+
// Box was deleted — drop the stale snapshot so /bx doesn't
141+
// keep rendering last-known-good values for a now-missing box.
134142
m.mu.Lock()
135143
delete(m.statuses, boxID)
136144
m.mu.Unlock()
@@ -143,7 +151,11 @@ func (m *statusMonitor) PokeBox(ctx context.Context, boxID string) {
143151
}
144152
}
145153
status := m.probe(ctx, box, apiKey)
146-
m.set(status)
154+
// Force-broadcast: a config edit may have flipped only a non-level
155+
// field (e.g. ConsoleEnabled). The default set() suppresses chatter
156+
// on equal-level updates, which is exactly the case we need to NOT
157+
// suppress here.
158+
m.setAndBroadcast(status)
147159
}
148160

149161
// pollAll fans out a reachability check per configured box. The check is
@@ -271,6 +283,19 @@ func (m *statusMonitor) set(s BoxStatus) {
271283
}
272284
}
273285

286+
// setAndBroadcast stores a status and ALWAYS broadcasts to subscribers,
287+
// even when level/reachable haven't changed. Used by PokeBox so a flip
288+
// of a non-level field (ConsoleEnabled, Enabled, Name, …) reaches open
289+
// /bx tabs immediately — the chatter-suppression in set() is desirable
290+
// for the steady-state poll loop but defeats the purpose of an
291+
// out-of-band config-change refresh.
292+
func (m *statusMonitor) setAndBroadcast(s BoxStatus) {
293+
m.mu.Lock()
294+
m.statuses[s.BoxID] = s
295+
m.mu.Unlock()
296+
m.broadcast(s)
297+
}
298+
274299
// Subscribe returns a channel of status events and an unsubscribe func.
275300
// Events are best-effort: a slow consumer that doesn't drain its channel
276301
// will simply miss events (we never block the publisher).

0 commit comments

Comments
 (0)