Skip to content

feat(ui): Decision Flow — see your routing pipeline as one living diagram - #426

Open
prajjwalkumar17 wants to merge 21 commits into
mainfrom
feat/decision-flow-page
Open

feat(ui): Decision Flow — see your routing pipeline as one living diagram#426
prajjwalkumar17 wants to merge 21 commits into
mainfrom
feat/decision-flow-page

Conversation

@prajjwalkumar17

@prajjwalkumar17 prajjwalkumar17 commented Sep 7, 2026

Copy link
Copy Markdown
Member

What

A new read-only dashboard page, Routing → Decision Flow (/routing/flow), that shows a merchant how their payments actually move through the Decision Engine: what runs first, what runs next, what never runs at all, and how the pieces they configured compose.

New users land on the dashboard with five strategies to configure and no picture of how they interact (one activation slot shared by rules / volume splits / experiments; success-rate, elimination, cost and commitments layered on top; debit and preferred-gateway driven by the request). This page is that picture.

How it works

Your routing stack — a status board read live from the same endpoints Routing Hub already uses (/routing/list/active, /rule/get successRate|elimination, /merchant-account/…/features, the debit-routing flag). The active strategy shows its real saved name; off layers link to their configure pages; derived warnings surface config traps (empty slot, experiment holding the slot with the real-payments flag off, SR without elimination).

The journey of one payment — the 13 stages of the decide-gateway pipeline in true execution order, grouped into Entry / Experiment layer / Network branch / Candidates / Ordering / Decision & learning. Stage badges reflect configured state; clicking a stage expands what it does, when it runs, which page configures it, and the API behind it — including the merchant's actual rules (names, condition summaries, destinations) and split percentages.

Connector lanes — every connector the active strategy references is drawn as a continuous SVG ribbon flowing behind the stage cards: fanning out from the payment dot, lane thickness following volume-split shares, converging on the configured first choice when the strategy fully determines one (single / priority head). Measured with a ResizeObserver; draw-in animation respects prefers-reduced-motion.

Honest by construction

  • No simulation, no writes: what-if payments stay on the Decision Simulator. This page only mirrors saved configuration.
  • Anything payment-dependent (rule matching, eligibility outcomes, SR ordering) is described, never faked.
  • Empty slot → dashed ghost lanes + a warning that unfiltered lists go straight to scoring and ties break arbitrarily.
  • Fetch failures render as "couldn't load part of your configuration", not as false "Not set" facts (/rule/get 404s are treated as the legitimate not-configured state).

Verification

  • tsc + vite build clean; both themes checked in the dev server (empty-state merchant, expansions, lane canvas resize behavior).
  • Stage copy and data derivations were cross-checked against the backend (flow_new.rs pipeline order, ab_test interceptor flag semantics, externally-tagged Output serialization of default_selection, routing_algorithm_mapper slot semantics) and against existing frontend consumers (RoutingHubPage, euclid/summarize.ts). An adversarial multi-agent review over the diff produced 18 confirmed findings — all fixed in this commit (notably: default_selection shape unwrapping, SVG canvas height ratcheting after expand/collapse, ghost-lane dash pattern being overridden by the draw-in animation, SWR retry storms on expected 404s, and error states masquerading as "not configured").

Follow-ups (not in this PR)

  • Replay a real payment's /analytics/payment-audit trace onto the same lanes.
  • Optional backend niceties: an aggregate GET /routing/stack/{merchant_id}, and a read endpoint for the merchant_config PaymentFlow gates (SrBasedRouting / EliminationBasedRouting), which no API exposes today — the board approximates them the same way Routing Hub does.

Evidences

Screenshot 2026-09-08 at 14 14 07 Screenshot 2026-09-08 at 14 14 16 Screenshot 2026-09-08 at 14 14 23

Closes #427

prajjwalkumar17 and others added 3 commits September 7, 2026 19:56
…pipeline

New read-only page at /routing/flow (sidebar: Routing > Decision Flow) that
answers the question new merchants keep asking: what runs first, what runs
next, and what never runs at all.

- "Your routing stack" status board, read live from the same endpoints
  Routing Hub uses (/routing/list/active, /rule/get successRate and
  elimination, merchant features, debit-routing flag), with the active
  strategy named, off layers linking to their config pages, and derived
  warnings (empty slot, experiment holding the slot, SR without elimination).
- The pipeline rail: 13 stages in true decide-gateway execution order,
  grouped (Entry / Experiment / Network branch / Candidates / Ordering /
  Decision & learning). Each stage badges its configured state and expands
  inline with what it does, when it runs, and the API behind it — including
  the merchant's actual rules (name, conditions, destinations) and volume
  splits.
- Connector lanes: each connector the active strategy references is drawn
  as one continuous SVG ribbon behind the stage cards — fanning out from
  the payment, thickness following volume-split shares, converging on the
  configured first choice when the strategy determines one. Ghost lanes and
  honest empty states when nothing is configured; load failures render as
  "couldn't load" rather than as false "not set" facts.

No simulation and no writes: what-if payments stay on the Decision
Simulator; this page only mirrors saved configuration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unconfigured layers no longer render as greyed 'Not set' rows — the rail
draws just the pipeline this merchant has (always-run stages plus whatever
is configured), and whole groups disappear when empty. The routing-stack
board above remains the discovery surface: every layer stays listed there
with its state and a Configure link, and a stage joins the flow as soon as
its configuration lands (the reads revalidate on remount/focus, and the
feature/flag toggles mutate the shared SWR caches at write time).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A merchant with nothing configured gets 404s from several of the reads; the
load-failure banner should only appear for genuine errors on any of them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@prajjwalkumar17 prajjwalkumar17 self-assigned this Sep 8, 2026
jagan-jaya
jagan-jaya previously approved these changes Sep 8, 2026
A merchant with no active strategy used to get grey 'connector A/B/C'
placeholders — technically honest, visually dead. Show a plausible example
set (razorpay / payu / stripe) instead: full lane colors, dashed strokes,
and an inline 'example set — activate a strategy to see yours' note so it
can never be mistaken for real configuration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
prajjwalkumar17 and others added 2 commits September 8, 2026 15:49
…guration

Every stage's expansion prose is now computed from the merchant's actual
stack instead of fixed text. The Decision stage says who really wins —
best live score (+ cost promotion), the strategy's own order when scoring
is off, or an honest 'effectively arbitrary' when nothing is configured —
and carries a matching inline label. Baseline priority, health penalties,
cost, learning loop, eligibility, arrive and the strategy slot all reword
themselves the same way (e.g. the learning loop admits that nothing reads
reported outcomes when scoring and elimination are both off).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each callable stage's expansion now carries a minimal curl — required
fields only — rebuilt from the live configuration: real connector names in
eligibleGatewayList, /routing/evaluate parameters that satisfy the
merchant's first rule, and sensible defaults when nothing is configured.
The no-config Decision copy now says plainly that the payment goes to the
integration's /decide-gateway fallback list (arbitrary pick within it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
prajjwalkumar17 and others added 15 commits September 8, 2026 16:05
The eligibility check is driven by the server's pm_filters TOML, which no
API exposes — the expansion now shows a small excerpt of its real shape
(per connector + payment method, on country and currency, values from
config/development.toml) so the mechanism is concrete instead of abstract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on Flow

When a static strategy and success-rate scoring are both live, the payment
example switches to POST /routing/hybrid — static half carrying the rule
parameters, dynamic half without eligibleGatewayList so the strategy's
output feeds scoring — and the two stages tag themselves 'step 1 · hard
filter' and 'step 2 · re-rank'. Every stage card now carries a small
top-right tag naming its operation kind (input, traffic split, branch,
hard filter, weighted pick, ordering, re-rank, demote, override, pick
winner, feedback), tinted by family.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hub answers 'what is active'; the flow is its picture — they belong on
one screen. The hub gets the same URL-backed underline tabs the Multi
Objective page uses (Strategies | Decision Flow · New, ?tab=flow), the
standalone sidebar entry goes away, and /routing/flow redirects to
/routing?tab=flow so existing links keep working. DecisionFlowPage becomes
DecisionFlowView, embedded with its own reads (SWR keys shared with the
hub, so nothing fetches twice).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The feature never shipped standalone, so there are no bookmarks to honor —
the hub tab (/routing?tab=flow) is the only address.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every ribbon now carries a subtle downstream flow pulse (dash cycle,
disabled under prefers-reduced-motion). With a volume split active, the
gap below the strategy shows each lane's real share as a percentage chip
and the flow speed encodes it — 60% traffic visibly moves faster than 10%.
The eligibility gap teaches the drop mechanic: the example set genuinely
loses its last lane there, while real strategies get a clearly-dashed
illustrative '✕ if not eligible' fork, since actual drops depend on the
payment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The page is educational, not analytics — so let it move. Small glowing
'payment' dots now travel each ribbon end-to-end on a loop (SMIL
animateMotion along the real lane paths), with travel speed and particle
count following volume-split shares; busy lanes visibly carry more
traffic. The payment-origin dot breathes, and the decided first choice
gets a pulsing winner dot at the converge. All of it sits behind
prefers-reduced-motion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A leg configured at 0% receives nothing, so its ribbon now ends at the
strategy with a '✕ 0% — no traffic' marker instead of flowing to the end —
no flow pulse, no particles, and the lanes below genuinely narrow to the
legs that carry traffic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Below success-rate scoring the lanes now periodically shuffle into a new
order with smoothly animated crossings — the ranking genuinely changes
payment to payment, and the continuous re-sort makes that visible instead
of implied. Implemented as a rebuilt shared path builder: the sort gap
curves each ribbon from its slot to an animated below-sort slot; a rotation
among the traffic-carrying lanes retargets every ~4.6s and tweens over
~950ms by mutating path data in place (so the draw-in never replays), then
refreshes the particle motion paths so payments ride the new order. Dead
lanes (0% legs, dropped examples) never move; disabled entirely under
prefers-reduced-motion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n move

The ordering gaps now form a staggered wave that travels down the page
every cycle: success-rate rotates the whole field, health penalties
sometimes demote one lane to the back (amber '▼ penalized' flash — demoted,
never removed, matching the real semantics), and cost optimization swaps
the leading pair when the cheaper gateway overtakes. Each ordering gap
carries its own slot state consumed sequentially by the shared path
builder, phases tween one after another so crossings cascade, and particles
refresh onto the final geometry after each wave. Dead lanes never move;
everything gates off under prefers-reduced-motion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eration

Reworked the wave so every stage's motion matches its tag. The hard filter
(eligibility) now sometimes removes a connector outright — lane cut at the
gap with a persistent '✕ not eligible' marker — and re-admits it a cycle
later ('↩ back'): across payments, different connectors genuinely drop
there. Health penalties likewise take a connector out for a cycle
('▼ penalized') and let it back in as scores recover, instead of
reordering. Only the two re-rank stages sort (success-rate rotates, cost
swaps the leaders). Payment particles hide while lanes re-shape and
re-enter on the new geometry, fixing dots drifting off-ribbon mid-wave.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rail now ends the way it begins — with named chips. Below the Decision
stage each surviving lane carries its connector chip; after every wave the
chips glide to their final slots (leftmost = current leader) and hide while
their connector is filtered out or penalized. With a deterministic winner,
the converged lane is chip-named at the winner dot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n the break

Cuts no longer swap path geometry abruptly. A removed lane now retracts
smoothly to the exact break point (an animated stroke-dasharray mask over
pathLength-normalized paths, with the break fraction found by binary search
over real arc length) and regrows the same way when re-admitted; example
lanes recover their dash pattern after regrowing. The connector's
'✕ not eligible' / '▼ penalized' marker pops in timed to the retracting tip
reaching the gap, and the cut lane's flow pulse and payment particles fade
with it instead of floating on a hidden ribbon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A calmer cadence throughout: waves every ~9.5s with longer beats between
stages, sort crossings glide over 1.4s on an S-curve, lane cuts retract and
regrow over 1.3s with the break marker landing at ~1s, draw-in and endchip
glides lengthened, flow current and particles drift slower with a softer
bead pattern and wider glow, markers gain depth shadows, and the breathe
pulses relax.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four reported defects shared a root cause — the ribbons were driven by
competing mechanisms (a CSS draw-in class, a CSS transition, imperative
dash masks and appendChild markers), so state went stale and duplicated.

Now a single frame loop writes every ribbon's geometry and visible fraction
from one piece of state, and markers are keyed React state:

- a penalized or filtered connector really disappears below its stage; a
  React re-render can no longer restore a full-length dash over the cut
  (that was the lingering line);
- at most one marker per stage, so they cannot stack or contradict;
- lanes are solid — the example set now reads as example through opacity
  and its note, not a dash pattern that fought the mask;
- light mode is a first-class theme: lane colours deepen against white,
  opacities and marker tints adapt, and the canvas re-derives on theme
  change without losing an in-flight cut.

Also: a tab that never receives animation frames now renders complete
lanes instead of empty ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…changes

An adversarial review of the new animation layer confirmed ten defects, all
rooted in state that a pure layout event was allowed to destroy.

- measure() now separates re-measuring from re-seeding: it keys on the
  diagram's identity (lanes, gap kinds, ghost/winner), so expanding a stage
  or resizing the window re-anchors geometry while cuts, markers and
  in-flight tweens continue. Previously every such event wiped them and
  replayed the whole reveal.
- The page memoizes the lane model, so the canvas stops re-measuring on
  every unrelated render (an SWR revalidation used to churn it).
- Tweens carry a channel and are applied oldest-first, so a newer intent
  always wins; a cut can no longer be stranded by a stale tween into a lane
  that stays masked forever.
- The wave skips beats while the tab is hidden instead of queuing tweens
  that all resolve in one frame on return.
- Gap geometry is read lazily, so a re-measure can't leave cuts anchored to
  a stale y; the demote marker follows the sort directly above it rather
  than assuming index 0.
- A deterministic winner's end chip stays at the converge point, its win dot
  fades while that lane is cut, restored markers get a fresh key so their
  entrance animation replays, and a theme change repaints imperative styles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Decision Flow — visualize the routing pipeline in the dashboard

2 participants