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
Patch cycle 0 queued
server-sent event delivered to this tab
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}.
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
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 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