StatifierUI.Live (StatifierUI v0.7.0)

Copy Markdown View Source

Read-only LiveView components for a host application's ops views: the current-state diagram and the run-history event log, side by side over one trace stream in wire format v1 (docs/wire-format.md), live or persisted.

Compiled only when the optional :phoenix_live_view dependency is present (ADR-0004); a host without it gets a stub whose every component raises with instructions. Nothing else in this package touches Phoenix.Component.

Every component is a function component over StatifierUI.Live.State - there is no LiveComponent, no process, and no mount/3 here. The host owns the socket, the subscription, and the events; this module owns markup. That is what makes these embeddable in a page the host already has rather than a page it has to give up.

Full fidelity

These are dev/ops surfaces and they render what the stream carries. If the stream was produced under an ADR-0012 projection profile, status/1 says so in a banner and redacted slots render as the stream's own sentinel - the redacted tenant-facing story is a different component (sui-hmn), not a flag on this one.

Embedding

Full worked example: docs/ops-embedding.md. The short version, for a live stream:

# mount/3
{:ok, subscriber} = Subscriber.start_link(machine: machine, source: source)
:ok = Subscriber.attach(subscriber, session, catch_up: true)
:ok = Subscriber.add_listener(subscriber, self())

socket =
  assign(socket, :trace, State.new(machine) |> State.sync(subscriber))

# handle_info/2 - the subscriber's fan-out shape
def handle_info({:statifier_ui, _session, message}, socket) do
  {:noreply, update(socket, :trace, &State.push(&1, message))}
end

# handle_event/3 - the two events the components emit
def handle_event("statifier_ui_scrub", %{"move" => move}, socket) do
  {:noreply, update(socket, :trace, &State.scrub(&1, String.to_existing_atom(move)))}
end

def handle_event("statifier_ui_select", %{"macrostep" => n}, socket) do
  {:noreply, update(socket, :trace, &State.select(&1, String.to_integer(n)))}
end

# render/1
<StatifierUI.Live.ops_view id="ops" state={@trace} />

A persisted stream drops the subscriber and the handle_info/2 clause: State.new(machine, messages: messages) is the whole difference.

The diagram is Mermaid source, and this package ships no JavaScript

diagram/1 emits StatifierUI.Diagram's stateDiagram-v2 source into a <pre class="mermaid">, the shape a Mermaid client renders in place. Attaching that client is the host's - pass hook={"MyMermaid"} and the element carries phx-hook. ADR-0008's client-side elkjs SVG renderer is the eventual full-fidelity diagram and is not built yet; when it is, it replaces this pane's body and not its contract.

Class names, not styles

Every element carries a statifier-ui-* class and the engine's own document-order identities as data-* attributes (data-macrostep, data-round, data-configuration), per ADR-0007's sync contract. No CSS ships: a host page styles these with its own design system, and the data-* stamps are what tests assert on.

Summary

Functions

The current-state diagram: StatifierUI.Diagram's Mermaid source for the configuration the selection implies, in a <pre class="mermaid"> for the host's Mermaid client to render in place.

The run history: one collapsible entry per macrostep, each holding its rounds and the effects that carry no round.

The composed ops view: status, scrubber, diagram, and event log.

The four scrubber controls - First, Prev, Next, Live - and the note saying which point is on screen.

The stream's own header: session id, subscriber status and counts, one line per diagnostic, and the ADR-0012 projection banner when the stream carries one.

Functions

diagram(assigns)

@spec diagram(map()) :: Phoenix.LiveView.Rendered.t()

The current-state diagram: StatifierUI.Diagram's Mermaid source for the configuration the selection implies, in a <pre class="mermaid"> for the host's Mermaid client to render in place.

The active configuration is also stamped on the element as data-configuration (a space-separated index list), so a test - or a host's own renderer - can read what is highlighted without parsing Mermaid.

Attributes

event_log(assigns)

@spec event_log(map()) :: Phoenix.LiveView.Rendered.t()

The run history: one collapsible entry per macrostep, each holding its rounds and the effects that carry no round.

Clicking an entry sends select_event with macrostep, which is the link StatifierUI.Kino's Markdown pane cannot have - a Markdown document has no click target to send back. The selected entry is open and marked, and its configuration is what the diagram is drawing.

A message list the log refuses - more than one session on one timeline, which docs/wire-format.md forbids - renders as a visible error rather than raising, so the rest of the ops view keeps working.

Attributes

  • id (:string) (required)
  • state (StatifierUI.Live.State) (required)
  • target (:any) - Defaults to nil.
  • select_event (:string) - Defaults to "statifier_ui_select".

ops_view(assigns)

@spec ops_view(map()) :: Phoenix.LiveView.Rendered.t()

The composed ops view: status, scrubber, diagram, and event log.

A host that wants a different layout composes status/1, scrubber/1, diagram/1, and event_log/1 itself - this component adds nothing but an arrangement.

Attributes

  • id (:string) (required) - DOM id root; panes derive theirs from it.
  • state (StatifierUI.Live.State) (required) - the read model - see StatifierUI.Live.State.
  • target (:any) - phx-target for the emitted events, when mounted inside a LiveComponent. Defaults to nil.
  • scrub_event (:string) - Defaults to "statifier_ui_scrub".
  • select_event (:string) - Defaults to "statifier_ui_select".
  • hook (:string) - phx-hook for the diagram element. Defaults to nil.
  • class (:string) - Defaults to nil.

scrubber(assigns)

@spec scrubber(map()) :: Phoenix.LiveView.Rendered.t()

The four scrubber controls - First, Prev, Next, Live - and the note saying which point is on screen.

Each button sends scrub_event with move set to first, prev, next, or live; StatifierUI.Live.State.scrub/2 takes it from there. The buttons are always enabled: with nothing to select every move resolves to :live, which is where the view already is.

Attributes

  • id (:string) (required)
  • state (StatifierUI.Live.State) (required)
  • target (:any) - Defaults to nil.
  • scrub_event (:string) - Defaults to "statifier_ui_scrub".

status(assigns)

@spec status(map()) :: Phoenix.LiveView.Rendered.t()

The stream's own header: session id, subscriber status and counts, one line per diagnostic, and the ADR-0012 projection banner when the stream carries one.

A persisted stream has no StatifierUI.Trace.Subscriber stats, and the pane says "persisted" rather than inventing a status.

Attributes