From 428c24898ea36d2afc5cf55344f7aad8dc6da119 Mon Sep 17 00:00:00 2001 From: e35ventura Date: Sun, 17 May 2026 13:59:04 -0500 Subject: [PATCH] Dashboard: status-grouped rate sort + drop inactive miners from depth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Active Rates table now sorts with status as the primary key (Available → Reserved → Exchanging → Inactive) so the panel reads top-to-bottom as "who can I trade with right now." Inactive miners stay visible for transparency, just at the bottom. The rate sort is now direction-aware. Forward (BTC→TAO) and reverse (TAO→BTC) are both quoted as TAO per 1 BTC, but "good" runs in opposite directions: higher forward = customer receives more TAO, lower reverse = customer pays less TAO. The previous max(forward, reverse) heuristic treated quote-rejecting reverse values (e.g. 1,000,000 τ) as the best rate and floated those miners to the top. Score by whichever side matches the active direction filter; "Both" sorts by tightest spread. Depth of Market was iterating every miner regardless of state, so dust collateral from inactive UIDs produced ghost rows that stretched the rate axis (visible as a 1,000,000 τ row in TAO→BTC depth). Filter to isActive miners, matching the panel's "active miners" tooltip framing. --- src/components/dashboard/MinerRatesTable.tsx | 45 +++++++++++++++----- src/components/dashboard/OrderbookDepth.tsx | 5 +++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/components/dashboard/MinerRatesTable.tsx b/src/components/dashboard/MinerRatesTable.tsx index 1ecc7ab..ddb973c 100644 --- a/src/components/dashboard/MinerRatesTable.tsx +++ b/src/components/dashboard/MinerRatesTable.tsx @@ -49,21 +49,39 @@ const pairStr = (m: Miner) => const statusRank = (m: Miner) => !m.isActive ? 3 : m.hasActiveSwap ? 2 : m.isReserved ? 1 : 0; -// Sort by the stronger of the two rates so bidirectional miners aren't penalized -// by a low counter side, and one-way miners still sort by their single quote. -const maxRate = (m: Miner) => - Math.max(parseRate(m.rate), parseRate(m.counterRate)); +// Forward (BTC→TAO) and reverse (TAO→BTC) are both quoted as TAO per 1 BTC, but +// "good" runs in opposite directions: higher forward = customer receives more TAO, +// lower reverse = customer pays less TAO. A naive max() of the two treats a +// quote-rejecting reverse value (e.g. 1,000,000 τ) as "the best rate" and sorts +// the miner to the top. Score by whichever side matches the active direction +// filter so sort desc always means "most attractive counterparty first." +const rateScore = (m: Miner, filter: DirectionFilter): number => { + const fwd = parseRate(m.rate); + const rev = parseRate(m.counterRate); + switch (filter) { + case 'reverse': + return rev > 0 ? -rev : -Infinity; + case 'both': + return fwd > 0 && rev > 0 ? -(rev - fwd) : -Infinity; + case 'forward': + case 'all': + default: + return fwd > 0 ? fwd : -Infinity; + } +}; -const getSortValue = (m: Miner, key: SortKey): string | number => { +const getSortValue = ( + m: Miner, + key: SortKey, + filter: DirectionFilter, +): string | number => { switch (key) { case 'uid': return m.uid; case 'pair': return pairStr(m); - case 'rate': { - const v = maxRate(m); - return v > 0 ? v : -1; - } + case 'rate': + return rateScore(m, filter); case 'collateral': return parseInt(m.collateralRao, 10) || 0; case 'status': @@ -163,8 +181,13 @@ const MinerRatesTable: React.FC = () => { } }); const sorted = [...directionFiltered].sort((a, b) => { - const av = getSortValue(a, sortKey); - const bv = getSortValue(b, sortKey); + // Status is always the primary key so the dashboard reads top-to-bottom + // as "who can I trade with right now": Available → Reserved → Exchanging + // → Inactive. The user's chosen column orders within each group. + const statusCmp = statusRank(a) - statusRank(b); + if (statusCmp !== 0) return statusCmp; + const av = getSortValue(a, sortKey, direction); + const bv = getSortValue(b, sortKey, direction); const cmp = av < bv ? -1 : av > bv ? 1 : 0; return sortDir === 'asc' ? cmp : -cmp; }); diff --git a/src/components/dashboard/OrderbookDepth.tsx b/src/components/dashboard/OrderbookDepth.tsx index 6fa12bf..80a4ce0 100644 --- a/src/components/dashboard/OrderbookDepth.tsx +++ b/src/components/dashboard/OrderbookDepth.tsx @@ -159,6 +159,11 @@ const OrderbookDepth: React.FC = () => { const groups: Record = {}; // key = rate, val = collateral TAO miners.forEach((m) => { + // Inactive miners aren't tradeable depth — they still have a quote + // stored on-chain but no one can hit it. Including them produces + // ghost rows (dust collateral at extreme rates) that stretch the + // rate axis and contradict this panel's "active miners" framing. + if (!m.isActive) return; if (!m.collateralRao) return; const s = m.sourceChain?.toLowerCase(); const d = m.destChain?.toLowerCase();