You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The HTMX dashboard refreshes on a blind 5s JS interval (web/templates/index.html — startAutoRefresh → setInterval(refreshNow, …), added in #81). It works and the filter selection is preserved (#81) — but it's a second, timer-based freshness model for data the gRPC side already exposes as a live stream (WatchResources, #76).
Replace the timer with an informer-driven change signal over SSE: the server emits a lightweight "resources changed" event whenever a watched resource changes; the browser reacts by re-running its existing filtered fetch. The dashboard becomes event-driven (updates on actual change, not on a 5s tick) and the bug class behind #75 is structurally removed — there is no poll left to clobber a selection.
Approach — SSE as a "doorbell", not a data channel
Deliberately not streaming row-level deltas to the browser. The dashboard's render model — browser sends ?kind=&namespace=, server returns a fully-rendered filtered <table>, browser swaps it — is simple, and the client-side namespace filtering depends on it. Per-row SSE deltas would force the browser to maintain the row set, the namespace chips and the count itself: a real rewrite that fights the existing filtering.
Instead, SSE carries only a tiny coalesced "changed" tick. On a tick the browser calls the existing refreshNow() — same filtered fetch, same whole-table swap. The render/filter pipeline is unchanged; only the trigger changes.
Where it shows up
web.go — handler() registers the routes; newWebServer(srv) already holds *server, so the web layer can reach srv.informers directly.
main.go — server.informers (per-kind shared informers, #76a) and the watchHandler event-handler mechanism (#76b). The web layer taps these in-process — it must NOT dial its own gRPC WatchResources (a pointless web→gRPC→back loopback).
A process-wide change broadcaster: register one cache.ResourceEventHandler on each srv.informers at web-server init; on any Add/Update/Delete mark "dirty" and coalesce (debounce ~500ms–1s — ExternalSecrets churn status.refreshTime constantly, so un-debounced ticks would be noisy).
New endpoint GET /events — text/event-stream. Each connection subscribes to the broadcaster; on a coalesced change it writes one event: changed. Flush per event; drop on r.Context().Done(); periodic comment-ping to survive gateway idle timeouts.
Browser (web/templates/index.html)
Replace the setInterval with EventSource('/events') (or the htmx SSE extension). On a changed event → refreshNow().
EventSource auto-reconnects; keep a slow safety-net poll (~60s) for when /events is unreachable.
Row-level SSE delta rendering (per-row ADD/MODIFY/DELETE fragments) — a larger rewrite of the client render/filter model; the doorbell approach is deliberately chosen instead.
The detail page (detail.html) — its own small poll, carries no selection state.
Summary
The HTMX dashboard refreshes on a blind 5s JS interval (
web/templates/index.html—startAutoRefresh→setInterval(refreshNow, …), added in #81). It works and the filter selection is preserved (#81) — but it's a second, timer-based freshness model for data the gRPC side already exposes as a live stream (WatchResources, #76).Replace the timer with an informer-driven change signal over SSE: the server emits a lightweight "resources changed" event whenever a watched resource changes; the browser reacts by re-running its existing filtered fetch. The dashboard becomes event-driven (updates on actual change, not on a 5s tick) and the bug class behind #75 is structurally removed — there is no poll left to clobber a selection.
Approach — SSE as a "doorbell", not a data channel
Deliberately not streaming row-level deltas to the browser. The dashboard's render model — browser sends
?kind=&namespace=, server returns a fully-rendered filtered<table>, browser swaps it — is simple, and the client-side namespace filtering depends on it. Per-row SSE deltas would force the browser to maintain the row set, the namespace chips and the count itself: a real rewrite that fights the existing filtering.Instead, SSE carries only a tiny coalesced "changed" tick. On a tick the browser calls the existing
refreshNow()— same filtered fetch, same whole-table swap. The render/filter pipeline is unchanged; only the trigger changes.Where it shows up
web.go—handler()registers the routes;newWebServer(srv)already holds*server, so the web layer can reachsrv.informersdirectly.main.go—server.informers(per-kind shared informers, #76a) and thewatchHandlerevent-handler mechanism (#76b). The web layer taps these in-process — it must NOT dial its own gRPCWatchResources(a pointless web→gRPC→back loopback).web/templates/index.html— thestartAutoRefresh/setIntervalJS from fix(web): preserve filter selection across dashboard auto-refresh #81 is what gets replaced.Proposed changes
Server (
web.go+ a small change broadcaster)cache.ResourceEventHandleron eachsrv.informersat web-server init; on any Add/Update/Delete mark "dirty" and coalesce (debounce ~500ms–1s — ExternalSecrets churnstatus.refreshTimeconstantly, so un-debounced ticks would be noisy).GET /events—text/event-stream. Each connection subscribes to the broadcaster; on a coalesced change it writes oneevent: changed. Flush per event; drop onr.Context().Done(); periodic comment-ping to survive gateway idle timeouts.Browser (
web/templates/index.html)setIntervalwithEventSource('/events')(or the htmx SSE extension). On achangedevent →refreshNow().EventSourceauto-reconnects; keep a slow safety-net poll (~60s) for when/eventsis unreachable.refreshNow().Acceptance criteria
kind+namespacefilter on each refresh; filtering and the namespace chips behave exactly as today (fix(web): preserve filter selection across dashboard auto-refresh #81)./eventssurvives an idle period through the gateway (keep-alive ping) and the browser reconnects if it drops.Out of scope
detail.html) — its own small poll, carries no selection state.Related
WatchResources/ the informer event mechanism reused here