# Phoenix LiveView Dashboard Observability in OpenAI Symphony: A Deep Dive

> Unlock real-time observability in your Phoenix LiveView dashboard. Discover how Symphony uses PubSub events for automatic re-rendering and effortless state updates. Learn more!

- Repository: [OpenAI/symphony](https://github.com/openai/symphony)
- Tags: deep-dive
- Published: 2026-05-08

---

**The Phoenix LiveView dashboard provides real-time observability by subscribing to PubSub events and automatically re-rendering whenever the Symphony orchestrator broadcasts state updates, eliminating the need for manual refreshes.**

The `openai/symphony` repository implements a reactive observability interface using Phoenix LiveView to expose the runtime state of the orchestration engine. By leveraging Elixir's PubSub capabilities and LiveView's reactive programming model, the dashboard delivers a live, read-only view of system health, session counts, and rate-limiting metrics without polling or page reloads.

## How the LiveView Initializes the Observability Stream

When a client connects to the dashboard, the LiveView process mounts and immediately establishes a subscription to the runtime's state changes.

### Mounting and Subscribing to PubSub

In [`elixir/lib/symphony_elixir_web/live/dashboard_live.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir_web/live/dashboard_live.ex), the `mount/3` callback initializes the connection by loading the current state payload and subscribing to update notifications:

```elixir
def mount(_params, _session, socket) do
  if connected?(socket) do
    ObservabilityPubSub.subscribe()
    :timer.send_interval(1_000, self(), :runtime_tick)
  end

  socket =
    socket
    |> assign(:now, DateTime.utc_now())
    |> load_payload()

  {:ok, socket}
end

```

The `ObservabilityPubSub.subscribe/0` function (defined in [`elixir/lib/symphony_elixir_web/observability_pubsub.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir_web/observability_pubsub.ex)) registers the LiveView process to receive messages broadcast on the `"observability:dashboard"` topic, establishing the real-time data pipeline.

## Reactive State Updates Without Page Refreshes

The dashboard updates automatically through message-passing rather than HTTP polling, ensuring minimal latency and server load.

### Handling Runtime Ticks and State Changes

The `handle_info/2` callback in [`dashboard_live.ex`](https://github.com/openai/symphony/blob/main/dashboard_live.ex) processes two distinct message types:

1. **`:runtime_tick`** – Dispatched every 1,000 milliseconds by `:timer.send_interval/3` to update the local clock used for runtime calculations (`assign(:now, DateTime.utc_now())`).

2. **`:observability_updated`** – Broadcast by the orchestrator whenever the internal state snapshot changes, triggering a complete payload reload via `load_payload/0`.

```elixir
def handle_info(:runtime_tick, socket) do
  {:noreply, assign(socket, :now, DateTime.utc_now())}
end

def handle_info(:observability_updated, socket) do
  {:noreply, load_payload(socket)}
end

```

This architecture ensures the UI reflects the current runtime state within milliseconds of any state transition.

## Generating the Observability Snapshot

The dashboard consumes structured data produced by the `Presenter` module, which aggregates information from the core orchestrator.

### Presenter.state_payload/2 and Orchestrator.snapshots

In [`elixir/lib/symphony_elixir_web/presenter.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir_web/presenter.ex), the `state_payload/2` function generates a consistent snapshot by calling `Orchestrator.snapshot/2`. This payload includes:

- **Session counts** – Active and completed sessions
- **Retry queue entries** – Failed operations awaiting reprocessing
- **Codex token totals** – Aggregate token consumption metrics
- **Rate-limit data** – Current throttle status and limits

The presenter ensures data consistency by pulling from the orchestrator's single source of truth while formatting raw values for human readability.

## The PubSub Broadcast Mechanism

State changes propagate through the system via explicit broadcast calls embedded in the orchestrator's mutation paths.

### Broadcasting State Changes from the Orchestrator

Whenever the orchestrator updates its internal state, it invokes `ObservabilityPubSub.broadcast_update/0` (located in [`elixir/lib/symphony_elixir_web/observability_pubsub.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir_web/observability_pubsub.ex)):

```elixir
def broadcast_update do
  Phoenix.PubSub.broadcast(
    SymphonyElixir.PubSub,
    "observability:dashboard",
    :observability_updated
  )
end

```

This broadcasts the `:observability_updated` atom to all subscribed LiveView processes, triggering immediate UI synchronization across all connected dashboard clients.

## Rendering the Real-Time Dashboard UI

The `render/1` function in [`dashboard_live.ex`](https://github.com/openai/symphony/blob/main/dashboard_live.ex) transforms the payload into HTML displaying summary metrics, rate-limit status, and detailed tables for active sessions and retry queues.

Helper functions like `format_int/1`, `format_runtime_seconds/1`, and `state_badge_class/1` convert raw Erlang terms into formatted strings and CSS classes, presenting runtime duration, integer counters, and session state indicators without requiring client-side JavaScript processing.

## End-to-End Data Flow

The complete observability pipeline operates as follows:

- **Orchestrator updates** (state change) → **ObservabilityPubSub.broadcast_update/0** → **PubSub dissemination** → **DashboardLive.handle_info/2** receives `:observability_updated` → **Presenter.state_payload/2** reloads snapshot → **LiveView re-renders** the template instantly.

Concurrently, the `:runtime_tick` message ensures the "Now" timestamp remains current for runtime calculations via `total_runtime_seconds/2`, keeping elapsed time displays accurate without requiring state mutations.

## Implementation Examples

To expose the dashboard in your router, mount the LiveView module:

```elixir

# elixir/lib/symphony_elixir_web/router.ex

live "/dashboard", SymphonyElixirWeb.DashboardLive, :index

```

Broadcast updates from custom orchestrator logic:

```elixir
defmodule SymphonyElixir.Orchestrator do
  def handle_state_change(state) do
    # ... process state update ...

    SymphonyElixirWeb.ObservabilityPubSub.broadcast_update()
    {:ok, state}
  end
end

```

Force a manual refresh via the API controller:

```elixir
defmodule SymphonyElixirWeb.Controllers.ObservabilityApiController do
  def refresh(conn, _params) do
    {:ok, _payload} = SymphonyElixirWeb.Presenter.refresh_payload(orchestrator())
    SymphonyElixirWeb.ObservabilityPubSub.broadcast_update()
    send_resp(conn, 200, "refreshed")
  end
end

```

## Summary

- The **Phoenix LiveView dashboard** in Symphony mounts at `/dashboard` and subscribes to `ObservabilityPubSub` for real-time updates.
- **State snapshots** are generated by `Presenter.state_payload/2` pulling from `Orchestrator.snapshot/2`, ensuring consistent data aggregation.
- **PubSub broadcasts** via `ObservabilityPubSub.broadcast_update/0` push `:observability_updated` messages to all connected clients instantly.
- **Dual message handling** in `handle_info/2` combines periodic tick updates (for timestamps) with observability events (for state changes).
- **Zero client-side polling** is required, as LiveView's reactive model handles all UI synchronization server-side.

## Frequently Asked Questions

### How does the dashboard update without manual refreshes?

The dashboard uses Phoenix PubSub to push state changes directly to the LiveView process. When the orchestrator calls `ObservabilityPubSub.broadcast_update/0`, all subscribed dashboard instances receive the `:observability_updated` message and immediately reload their payloads, triggering a server-side re-render of the HTML diff.

### What metrics does the Symphony dashboard display?

According to the `Presenter.state_payload/2` implementation in [`elixir/lib/symphony_elixir_web/presenter.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir_web/presenter.ex), the dashboard displays running and retrying session counts, Codex token usage totals, rate-limit snapshots, detailed active session tables, and retry queue entries with precise runtime calculations.

### Can I customize the update frequency or subscribe to specific events?

The `:runtime_tick` interval is hardcoded to 1,000 milliseconds in the `mount/3` function using `:timer.send_interval/3`. While the current implementation broadcasts all observability events on a single topic, the PubSub architecture in `ObservabilityPubSub` can be extended to support topic partitioning or custom broadcast patterns by modifying the topic string and subscription logic.

### What happens if the orchestrator restarts while the dashboard is open?

The LiveView maintains its subscription to the PubSub topic independently of the orchestrator process. When the orchestrator restarts and resumes broadcasting updates via `broadcast_update/0`, connected dashboards will receive the new `:observability_updated` messages and refresh their payloads accordingly, though there may be a brief gap in updates during the orchestrator's restart sequence.