jonnifyelixir
F6 / F6.09 / build

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

METRICS counts seeded from contexts — courses, enrollments numbers THE FEED a capped stream of recent events — phx-update stream a list ASSIGNS the socket holds the read model — derived, not stored the state

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

CONTEXTS (READ) Catalog.count_courses() Enrollment.count() mount SOCKET ASSIGNS (READ MODEL) courses_count · enrollments_count viewers stream(:events) — starts empty render/1 metric cards + feed a projection over the domain — if the process restarts, it re-seeds from the contexts

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.