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
@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
id(:string) (required)state(StatifierUI.Live.State) (required)hook(:string) - Defaults tonil.
@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 tonil.select_event(:string) - Defaults to"statifier_ui_select".
@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 - seeStatifierUI.Live.State.target(:any) -phx-targetfor the emitted events, when mounted inside a LiveComponent. Defaults tonil.scrub_event(:string) - Defaults to"statifier_ui_scrub".select_event(:string) - Defaults to"statifier_ui_select".hook(:string) -phx-hookfor the diagram element. Defaults tonil.class(:string) - Defaults tonil.
@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 tonil.scrub_event(:string) - Defaults to"statifier_ui_scrub".
@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
id(:string) (required)state(StatifierUI.Live.State) (required)