Streaming dashboard

A bounded SSE loop sends live metrics and activity patches. Starting it again in the same tab replaces the prior stream.

Demo

Live operations lane

Bounded demo stream, safe to restart repeatedly.

Throughput

118/min

ingested events

Latency

42 ms

edge to patch

Progress

0%

stream idle

Activity stream

latest tick: 0

idle

Patch cycle 0 queued

server-sent event delivered to this tab

t+0

How it works

  • connect/1 posts to the page path and opens the stream.
  • stream_key/1 deduplicates by demo scope plus tabId.
  • handle_info/2 receives ticks from the library-owned loop.
  • Metric tiles and activity rows are patched independently.

connect/1 posts to the page path and Dstar.Page.Plug opens a server-sent event stream, then owns the receive loop. stream_key/1 registers it per demo scope plus tab, so starting it again in the same tab replaces the old stream instead of running two. Each tick patches the three metric tiles and prepends one activity row; the final tick returns {:halt, conn}.

lib/dstar_demo_web/pages/streaming_dashboard_page.ex
def stream_key(_conn), do: {:dstar_demo, :streaming_dashboard}

@impl Dstar.Page
def handle_connect(conn, _params) do
  Process.send_after(self(), {:tick, 1}, @stream_delay)

  conn
  |> patch_signals(%{streamStatus: "live", progress: 0, tick: 0})
  |> patch(&stream_frame/1, [tick: 0, status: "live"], selector: "#stream-frame")
end

If this were LiveView

Both sides push eight ticks at 280ms, update the same three metrics, and prepend one activity row per tick.

Dstar

lib/dstar_demo_web/pages/streaming_dashboard_page.ex
def stream_key(_conn), do: {:dstar_demo, :streaming_dashboard}

@impl Dstar.Page
def handle_connect(conn, _params) do
  Process.send_after(self(), {:tick, 1}, @stream_delay)

  conn
  |> patch_signals(%{streamStatus: "live", progress: 0, tick: 0})
  |> patch(&stream_frame/1, [tick: 0, status: "live"], selector: "#stream-frame")
end

LiveView

illustrative
# Illustrative LiveView equivalent — not compiled or run.
@stream_steps 8
@stream_delay 280

def mount(_params, _session, socket) do
  if connected?(socket), do: Process.send_after(self(), {:tick, 1}, @stream_delay)

  {:ok,
   assign(socket,
     status: "live",
     tick: 0,
     throughput: 118,
     latency: 42,
     progress: 0,
     activity: []
   )}
end

def handle_info({:tick, tick}, socket) when tick <= @stream_steps do
  Process.send_after(self(), {:tick, tick + 1}, @stream_delay)

  {:noreply,
   assign(socket,
     tick: tick,
     throughput: 118 + tick * 9 + rem(tick, 3) * 4,
     latency: 42 - min(tick * 2, 18),
     progress: trunc(tick / @stream_steps * 100),
     activity: [tick | socket.assigns.activity]
   )}
end

def handle_info({:tick, _done}, socket) do
  {:noreply, assign(socket, status: "complete", progress: 100)}
end