Statifier inspector
Mix.install([
{:kino, "~> 0.14"},
{:statifier_ui, path: Path.join(__DIR__, "..")}
])
What this notebook is
The Livebook inspector end to end, driving a small card-authorization
chart - the same domain, and the same pending -> authorized -> captured
spine, the README's worked example uses. It is also the milestone's
manual acceptance test: each numbered step below says what to do and
what you should see, so walking it top to bottom verifies the assembled
widget (sui-t36.8) against a live session.
Livebook evaluates cells in order; use "Evaluate" on each code cell as you reach it.
1. Compile the chart
Text-first: the SCXML below is the source of truth, and everything the inspector shows is read from it or from the session's trace effects.
xml = """
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" initial="pending" version="1.0">
<datamodel>
<data id="amount_cents" expr="1999"/>
<data id="captured_cents" expr="0"/>
<data id="authorization_attempts" expr="0"/>
</datamodel>
<state id="pending">
<transition event="authorize.declined" target="pending">
<assign location="authorization_attempts" expr="authorization_attempts + 1"/>
</transition>
<transition event="authorize.approved" target="authorized">
<assign location="authorization_attempts" expr="authorization_attempts + 1"/>
</transition>
</state>
<state id="authorized">
<transition event="capture.settled" target="captured">
<assign location="captured_cents" expr="amount_cents"/>
</transition>
</state>
<final id="captured"/>
</scxml>
"""
{:ok, machine} = Statifier.compile(xml)
One deliberate difference from the README's chart: a declined
authorization here returns to pending rather than reaching a final
declined state, so the card can be retried. The walk below needs one
event that takes a real step without moving the highlight, and that
self-transition is it.
Expect: {:ok, %Statifier.Machine{...}}. A compile error here means
the chart text was edited into something invalid - fix it before going on.
2. Fixtures for the injection palette
One sample payload per event name (ADR-0003). These become the one-click buttons in the injection pane.
{:ok, fixtures} =
StatifierUI.Fixtures.new(
events: %{
"authorize.approved" => %{"amount_cents" => 1999, "currency" => "USD"},
"authorize.declined" => %{"reason" => "insufficient_funds"},
"capture.settled" => %{"captured_cents" => 1999}
}
)
3. Start a recorded session
trace: true puts the run on the wire; record: true is what lets the
inspector catch up on anything it missed (statifier ADR-0049). Without it
the inspector still works but labels itself Live-only. Replay cost
grows with run length, so a very long-lived session makes the inspector
cell slower to evaluate - not a concern at this notebook's scale.
{:ok, session} = Statifier.Session.start_link(machine, trace: true, record: true)
4. Open the inspector
StatifierUI.Kino.inspect(session, fixtures, source: xml)
Expect, immediately:
- Status header - the session id,
attached, a message count, and no "Live-only" warning. - Configuration diagram (left) - the chart as a Mermaid state
diagram with
pendinghighlighted. The initialize burst happened before this cell ran; catch-up is why the diagram still knows about it. - Datamodel explorer (right) -
amount_centsat1999,captured_centsat0, andauthorization_attemptsat0. - Injection pane - one button per fixture event, then a free-form name/payload form.
- Event log - macrostep 1 (the initialize burst), collapsed except the last macrostep.
5. Walk the chart from the palette
Do these in order, watching the panes after each click:
-
Click authorize.declined. Expect: feedback line "Sent
authorize.declined..."; the highlight stays onpending, and that is correct - the self-transition still exits and re-enters, so the datamodel showsauthorization_attemptsas1with a changed marker and the event log grows a macrostep whose cause isauthorize.declined. A step the diagram cannot show is exactly what the log is for. -
Click authorize.approved. Expect: highlight moves to
authorized;authorization_attemptsreads2; the log's newest macrostep namesauthorize.approved. -
Click capture.settled. Expect:
captured_centsreads1999with a changed marker, copied fromamount_centsby the transition's<assign>; the log's newest macrostep namescapture.settledand carries the entry ofcaptured; the status header still saysattached- a halted chart's session process is alive, and the inspector keeps its whole trace readable.Expect the highlight to stay on
authorized, not move tocaptured. That is today's real behavior, not a mistake in the walk. Entering a top-level<final>halts the run, so the macrostep ends intrace.donerather thantrace.macrostep_stable, andStatifierUI.Inspector.active_configuration/2reads only the newestmacrostep_stable. It therefore falls back to the previous quiescent configuration. What the configuration pane should show for a halted chart is an open question -sui-dc7. If you walk this notebook and the highlight does reachcaptured, that bead has been answered and this step needs updating.
6. The form, including its error path
- In the free-form fields, enter name
authorize.declinedand payload{"reason": "manual"}, press Send. Expect: "Sent" feedback, and no new trace message anywhere - not a macrostep, not even an event-received line. The chart halted when it enteredcaptured, so the run is over and the event is dropped rather than processed. "Sent" is honest about what the injection pane did (it handed the event to the session); the empty log is honest about what the session did with it. Worth seeing both, because they are two different facts. - Now enter payload
{not json. Expect: Not sent, with the JSON error ({:invalid_json, ...}) - the session was never touched, because the draft failed to build before anything was sent.
7. Re-evaluate the inspector cell
Re-evaluate the StatifierUI.Kino.inspect(...) cell (step 4).
Expect: the previous widget's processes are terminated with the old
cell evaluation (that is the clean detach - the session simply drops the
dead subscriber), and the fresh widget shows the entire history
again: every macrostep you drove above is in the log,
authorization_attempts is 2, captured_cents is 1999, and the
diagram highlights authorized for the reason step 5 gives (sui-dc7).
That round trip is catch-up doing its job: a fresh widget over the same
recorded session reproduces the previous one exactly, halt included.
To see the honest degraded mode: start a session without
record: true, inspect it, and note the status header's Live-only
warning - the inspector refuses to present a partial stream as whole.
8. Session death
Process.exit(session, :kill)
Expect: the status header moves to terminated, the log's footer
gains a session.terminated line, and every pane keeps rendering the
buffered trace - death is an observation, not a reset. (Because the
session was started from this notebook with start_link, killing it may
also take down the cell's evaluator; re-evaluate from step 3 to go
again.)
Where to go next
StatifierUI.Trace.Subscriber- the one process behind all four panes.Statifier.Session.invocations/1plus:inherit_observers(statifier ADR-0050) - attaching one subscriber to a whole invoke tree; the inspector composes per session today.docs/wire-format.md- every message type the panes fold over.