feat(ui): Decision Flow — see your routing pipeline as one living diagram - #426
Open
prajjwalkumar17 wants to merge 42 commits into
Open
prajjwalkumar17 wants to merge 42 commits into
prajjwalkumar17 wants to merge 42 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>
Following the reference flow, each stage that can change the candidate list now carries its own column of connector chips — who is still in the running after that step. The gateway a stage knocks out turns red and strikes through at that stage, then disappears from every stage below it, and the chips slide when a re-rank reorders them. Also fixes a stale-measure bug the columns exposed: stages appear as each config read lands, which changes the gaps the canvas measures without changing the lanes, so with lanes memoized the canvas never re-measured and its labels described an older rail. The page now passes the rendered stage list, and the canvas re-measures whenever it changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The last column now reads as a verdict rather than another list: whichever connector holds the leading slot when the rail ends gets a green tick and a 'wins' caption in place of its lane swatch, and it updates as the re-ranks change who is in front. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-ranks used to cross the ribbons, so the colour sitting in a given column changed part-way down and two connectors read as swapping identity. A lane now runs straight from entry to decision — one connector, one column, one colour — and the re-rank states the new order on the chips instead: each re-rank column carries the rank it just produced, and the columns below it carry that order forward until something changes it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The '✕ not eligible' / '▼ penalized' markers were positioned on their connector's lane, which is exactly where that connector's chip now sits, so the two overlapped in the same row. The chip already carries the identity and the red struck-through state, so the marker only needs to supply the reason: it now sits clear to the right of every lane, on the chip's row, captioning the column. Their entrance animations no longer translate horizontally, which had re-offset them once they stopped being centred. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The winning chip was placed just below the converge gap, which is exactly where the 'outcomes feed tomorrow's scores' line starts, so the two overlapped. The arriving chips now sit inside the gap, the gap carries enough height for them, and the caption gets a little air above it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… lanes Chips were centred on their connector's lane, so the ribbon ran straight through them — and the eliminated/restored events landed in the same place again. Each stage now gets a single flex row placed to the right of every lane: its connector chips in rank order, then that stage's event at the end. Rank drives flex order, so a re-rank visibly reorders the row, and nothing is positioned by hand any more — a lane can no longer cross a chip and two labels can no longer claim the same spot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The names belong on the lanes — that is what ties a chip to its ribbon — so they are back on them, and only the stage events (penalized, back, not eligible) live in the clear space past the last lane. Lanes are spaced wider so a full name fits on each without touching its neighbour, and the chips (winner included) are painted opaque so the ribbon they sit on can no longer show through them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cut landed slightly above the connector chip, so the ribbon's rounded cap poked over the chip's top edge and read as the line crossing the text. Cuts now land at the chip's centre, so the chip covers the terminus and the name always sits on top of its line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stacking was already right — the chips paint above the lanes and are opaque — but a ribbon ran flush against the chip's edge with no separation, which reads as the line touching the text. Every label that sits on a lane (the entry names, the per-stage connector chips, the split percentages and the winner) now carries a ring in the card's own background, so the line is knocked out around it and visibly stops at the label. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The entry fan swept almost horizontally straight into the connector names, so the curves arrived at the chips at a shallow angle and read as running across them. Each lane now curves onto its own column high in the entry gap, then drops straight down to its name — the fan is finished well above the row, and the names sit on a clean vertical run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rank numbers stated the new order; showing it is better. A re-rank now moves the connector to its new position — the ribbons cross and the named chip travels with its lane, so you watch one gateway overtake another instead of reading '#1' and '#2'. The colour swap that made this confusing before is gone: every stage carries the connector's name on its lane, so a crossing reads as movement rather than two lanes trading identity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The names lagged the ribbons for two reasons. They carried a CSS transition on the left property while the canvas was also writing that property every frame, so the transition spent the whole animation chasing a moving target; and a re-rank row sits at the curve's midpoint, where the ribbon is half-way between the old and new column, while the chip was placed at the final column — half a lane-step ahead of its own line. Position is now frame-synced with no transition, and a chip on a re-rank row sits on the curve. Measured drift between a name and the ribbon at its row fell from 53px peak / 22px average to 5px / 1px. Chips below a cut now fade exactly as the retracting ribbon passes their row instead of blinking out, so the names fall away in step with the line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
prajjwalkumar17
force-pushed
the
feat/decision-flow-page
branch
from
September 15, 2026 10:35
80956ec to
75305aa
Compare
Putting a re-rank row on the curve's midpoint to keep it glued to its line had an obvious flaw once two lanes actually swapped: both curves pass through the same midpoint, so the two names landed on top of each other. The row now sits just below the crossing, where each lane has arrived at its new column and the ribbon is vertical again — the names stay locked to their lines, and they are side by side in their new order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The group headings sit above the ribbon (z-[2]) but paint no background of their own, so each lane showed through the gaps between the letters — two lanes cut straight across 'Candidates — who can process it'. The heading text now carries the surrounding background, the same knockout the stage badges and connector chips already use, so a lane passes behind the words and resumes below instead of through them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Knocking the ribbons out from behind the headings stopped them crossing the words, but it left every lane visibly broken at each heading row. Standing the heading beside the lanes instead keeps the ribbons running unbroken, which reads better on a diagram whose whole point is continuous flow. The indent follows the full lane set rather than the lanes still live, so it holds still while the animation cuts and re-admits connectors. Below 1024px there is no room to stand beside the lanes, so the heading stays left and the knockout behind the words carries it there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two things were repeating rather than informing. 'Always runs' sat on four
of the rows, but a stage is drawn at all only when it runs, so the pill
restated its own presence; it now renders only for stages that actually have
something to report ('Configured', 'Elimination on', a named strategy).
The group headings restated the operation tag directly beneath them —
'ORDERING — who should get it' above an ORDERING badge. The headings keep the
half that teaches and drop the half that echoes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
They named the group but the rail already reads in order, and each one sat directly above an operation tag that said the same thing more precisely. Removing them takes a row of text out of every group boundary and lets the ribbons run the length of the rail uninterrupted. This also retires the machinery that existed only to keep them off the lanes: the lane-band indent, its breakpoint rule, and the knockout behind the words. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wave fired its four events at 0s, 1.1s, 3.35s and 4.3s and then sat still for the rest of the 9.5s cycle, so the rail read as a burst followed by a pause. Events are now one EVENT_MS apart, the wrap into the next wave included, and the cycle length follows the number of events the merchant's configuration actually produces. The cost re-rank also stopped skipping its turn. It swapped only on a coin flip, so roughly a third of cycles animated to the positions already held — invisible at the old pace, but at five seconds it left a ten-second stall. It now always moves an adjacent pair, chosen at random for variety. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The decision row now names what the stage actually returns — [razorpay] [payu] — in the order the connectors would be offered, and it tracks the rail, so a connector dropping out leaves the bracket as it happens. This also fixes a connector with no traffic being presented as the choice. A 0% leg is truncated in path geometry rather than by the visibility mask, so the old winner test never excluded it, and a re-rank could rotate it into the lead. Zero-share lanes are now excluded from the output outright: their ribbon ends at the split and never reaches the decision at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The output reads as boxes on the middle of the decision row, each with its lane's colour, in the order the connectors would be offered — replacing the bracketed text. 'best score + cost wins' returns to the right-hand slot, which the output had been occupying. Naming the arriving connectors at the decision also fills the gap the end chips left behind: every other gap labels its lanes, so the last one running down into nothing read as the diagram having lost its ending. They are now ordinary chips, like every other row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rail had drifted to eight sizes — 9, 9.5, 10, 10.5, 11, 12 and 13px — four of them under the 11px floor the scale defines, which is why the small text read as faint next to the rest of the dashboard. Everything under the floor moves to 11px, and each size is now paired with its leading, so a chip no longer inherits whatever line-height sits above it. The page is left on 11/12/13, the same three sizes its sibling routing pages use. Larger chips need more room, so the gaps grow with them, and a re-rank row sits further off the gap's bottom edge. The sort gaps take the most: their lanes cross, and the names have to land far enough apart to stay legible. 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