Funnel ForgePane — run history + bottleneck annotation (design)
The dwarves funnel pane (nornir-funnel-embed::FunnelPanel) already draws the plan-DAG growing live (auto-poll Funnel.Show ~1.5 s + submit strip) and keeps a de-duped size trajectory (FunnelHistory — the node-count trail). This adds the next layer of "addera historik och annat": a selectable run history and a bottleneck hint.
f0 — the whole
Two additive surfaces over the existing live DAG, both robot-observable in state_json and both reusing the shared facett-plandag renderer + its constraint analysis (no second renderer, no recomputed bottleneck — L5). The live view stays the default/"latest".
f1 — run history (FunnelRunLog)
- Capture: on every SUCCESSFUL poll,
refresh()callsrecord_run(model, outcome, at_s). The record carries the full capturedPlanModel(the DAG itself —PlanModelisClone), plusnode_count/edge_count(from the model), anoutcome(Running/Done/Failed, derived from the DAG's node statuses byrun_outcome_from_dag), a monotonicseq, andcaptured_at_s(the egui-input time — the "started" marker). - De-dup + cap: a capture whose
(node_count, edge_count, outcome)equals the previous one collapses (the log holds only distinct run states); the ring caps atRUN_CAP(32), dropping the oldest.seqis never reused. Pure ⇒ unit-testable without a clock or the network (theFunnelRunLogstore mirrorsFunnelHistory's discipline). - Picker: a
ComboBox—◷ latest (live)plus one entry per past run (newest first,run #seq · N nodes / M edges · outcome). Selecting an entry re-renders that run's DAG through a freshPlanDagView(view_from_model— the one place the view is named, so live and snapshot are byte-identical). A● livebutton (or picking◷ latest) returns to live. - Summary: when a past run is selected, a line shows
viewing run #seq · N nodes / M edges · outcome.
f2 — bottleneck annotation (CONSUMED, not recomputed)
- The pane reads the currently-shown view's
state_json(current_view()= the selected snapshot when picked, else the live view) and consumestop_bottleneck— the node that unblocks the most downstream work (pure sources filtered), computed byfacett-plandag'sResolved::bottlenecks()/top_bottleneck()(which itself mirrorsfunnel_core::PlanDag::bottlenecks()). It looks up that id'stitlein the same view'snodes[]. - Rendered as an amber
clear next · <title>hint above the DAG, so the user sees WHERE the funnel is constrained as it grows.critical_pathis surfaced as data too. - Nothing is recomputed dwarves-side — the ranking lives once, in the shared view (L5).
f3 — the seams (state_json / update_json)
Added to the funnel pane's state_json: runs (array of {seq,node_count,edge_count,outcome,captured_at_s}), run_count, selected_run (null = live), bottleneck (top id or null), bottleneck_title, critical_path, and plandag now reflects the shown DAG (live or snapshot). Added to update_json: {"select_run":<seq>|null} (view a past run / return to live) and {"clear_runs":true} (drop the log + back to live). Existing keys unchanged (additive, L2).
Update — the EVENT-SOURCED ledger now exists alongside this
funnel-core retains RunRecorded (it previously validated and dropped the payload), so the funnel's REAL run history is now readable from a replayed Funnel (runs() / runs_to_json()) and the pane surfaces it as event_runs — see funnel-live-graph.md. The two are complementary, not duplicates: event_runs answers "what actually RAN" (executions, actors, outcomes), the FunnelRunLog below answers "what did the DAG LOOK LIKE" (a re-viewable rendered snapshot per poll) and remains the picker's source, since a remote Funnel.Show still carries no run ledger on the wire.
Why a client-side snapshot log (and what stays deferred)
nornir-server's Funnel.Show RPC returns the current funnel only — there is no run-ledger on the wire. So the run history is a client-side capture of the DAG as it grows (each distinct poll = a re-viewable run). A true server-side funnel-run ledger (jera JobRecord over a new Funnel.Runs RPC) is the deferred leg; record_run is the insertion point (swap its caller for a wire-fed history without touching the picker/render).