feat(ui): Decision Flow — see your routing pipeline as one living diagram - #426
Open
prajjwalkumar17 wants to merge 21 commits into
Open
feat(ui): Decision Flow — see your routing pipeline as one living diagram#426prajjwalkumar17 wants to merge 21 commits into
prajjwalkumar17 wants to merge 21 commits into
Conversation
…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>
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>
…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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 respectsprefers-reduced-motion.Honest by construction
/rule/get404s are treated as the legitimate not-configured state).Verification
tsc+vite buildclean; both themes checked in the dev server (empty-state merchant, expansions, lane canvas resize behavior).flow_new.rspipeline order,ab_testinterceptor flag semantics, externally-taggedOutputserialization ofdefault_selection,routing_algorithm_mapperslot 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_selectionshape 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)
/analytics/payment-audittrace onto the same lanes.GET /routing/stack/{merchant_id}, and a read endpoint for themerchant_configPaymentFlow gates (SrBasedRouting/EliminationBasedRouting), which no API exposes today — the board approximates them the same way Routing Hub does.Evidences
Closes #427