F6.09 · part 1 of 3
Build the dashboard
Before anything is live, the dashboard is an ordinary LiveView with a carefully chosen
read model on its socket. A read model is derived state — numbers and a list you compute
from the domain, not a new place data lives. Here it is three things: the metric counts (how
many courses, how many enrollments), a viewers number, and a capped feed of
recent activity. mount/3 seeds the counts by asking the F6.04 contexts —
Catalog.count_courses/0, Enrollment.count/0 — so the page
is correct the instant it loads, and it initialises a stream for the feed, which keeps a long,
append-only list out of socket memory exactly as in F6.06.3. render/1 then draws metric
cards from the assigns and the feed from the stream. There is deliberately no event handling yet: this dive
builds a dashboard that is accurate on load but static, so the live part in F6.09.2 has a clean shape to plug
into. The discipline to hold is that the dashboard reads — every number traces back to the
contexts, and the socket is a cache of derived values, never a source of truth.
Four parts: the three pieces of the read model, the dashboard as a projection, seeding it in
mount/3, and rendering cards and a feed in HEEx.
Metrics, feed, assigns
The read model is three pieces. The metrics are counts seeded from the contexts. The feed is a capped stream of recent events. The assigns are where the socket holds it all — derived, never stored. Select one.
The read model · select one
piece: metrics
is: counts seeded from contexts
Keeping the feed in a stream rather than an assign matters as much here as in F6.06.3: a busy dashboard could see thousands of events, and a stream holds them in the DOM, not in the process’s memory.
A read model, not a store
The dashboard is a projection: a view computed from the domain. On mount it reads the current counts from the contexts — the same read functions any page would call — and holds them in assigns. The feed stream starts empty and fills from broadcasts later. Nothing here is authoritative; if the process restarts, it re-seeds from the contexts and carries on.
contexts (read side) → mount seeds → the socket
Because the read model is derived, it is disposable, and that is a feature: there is no migration, no separate store to keep consistent. The contexts remain the single source of truth, and the dashboard is one view of them.
Seeding it at mount
mount/3 pipes the socket through a few assign/3 calls to set
the counts and a starting viewers of zero, then stream/3 to
initialise an empty feed. The counts come straight from context read functions, so the first paint already
shows real numbers — no loading state, no placeholder.
def mount(_params, _session, socket) do socket = socket |> assign(:courses_count, Catalog.count_courses()) |> assign(:enrollments_count, Enrollment.count()) |> assign(:viewers, 0) |> stream(:events, []) # filled by broadcasts in F6.09.2 {:ok, socket} end
Seeding from the contexts is what separates an accurate dashboard from a misleading one. The numbers are not guesses kept in sync by hand — they are read from the domain at mount and then maintained by the events that change them.
Rendering cards and a feed
The template is plain HEEx (F6.05): metric cards interpolate the count assigns, and the feed iterates the
stream under a phx-update="stream" container with a :for
comprehension. The dom_id the stream provides becomes each row’s
id, so LiveView can insert and update rows efficiently.
<div class="dashboard"> <section class="metrics"> <article><h3>Courses</h3><p>{@courses_count}</p></article> <article><h3>Enrollments</h3><p>{@enrollments_count}</p></article> <article><h3>Watching</h3><p>{@viewers}</p></article> </section> <ul id="feed" phx-update="stream"> <li :for={{dom_id, event} <- @streams.events} id={dom_id}> {event.label} </li> </ul> </div>
seed from contexts
mount/3 reads counts; the feed stream starts empty.
render the read model
Cards from assigns, feed rows from the stream — accurate, but still static.
Next: broadcast engine events — the shape is ready; now subscribe to the domain and fold each event into these counts and this feed.