Skip to content

web: replace the dashboard poll with an informer-driven SSE change-signal #83

Description

@patrick-hermann-sva

Summary

The HTMX dashboard refreshes on a blind 5s JS interval (web/templates/index.htmlstartAutoRefreshsetInterval(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.gohandler() registers the routes; newWebServer(srv) already holds *server, so the web layer can reach srv.informers directly.
  • main.goserver.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).
  • web/templates/index.html — the startAutoRefresh / setInterval JS from fix(web): preserve filter selection across dashboard auto-refresh #81 is what gets replaced.

Proposed changes

Server (web.go + a small change broadcaster)

  • 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 /eventstext/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.
  • The pause / manual-refresh controls from fix(web): preserve filter selection across dashboard auto-refresh #81 stay — "pause" now means "ignore live ticks"; the manual button still forces a refreshNow().

Acceptance criteria

  • A create/update/delete of a watched kind updates the dashboard within ~1s, with no fixed-interval poll.
  • The browser still sends its current kind+namespace filter on each refresh; filtering and the namespace chips behave exactly as today (fix(web): preserve filter selection across dashboard auto-refresh #81).
  • Change ticks are coalesced — a burst of informer events triggers at most one refresh per debounce window.
  • /events survives an idle period through the gateway (keep-alive ping) and the browser reconnects if it drops.
  • Pause stops live updates; manual refresh still works; neither disturbs the filter selection.
  • No regression of web: auto-refresh resets the namespace filter selection #75 — there is no blind poll left to revert a selection.

Out of scope

  • 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.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions