Miner dashboard: /miners route, leaderboard, crown grid + rate chart#90
Merged
Conversation
New /miners route (and /miners/:hotkey detail variant) lands the dashboard described in plans/miner-dashboard.md and matches the miner-dashboard-mock.html visual reference. Components live under components/miners: StickyNetworkHeader (current block + crown holders + halt banner), NetworkOverviewStats (volume / swaps / active miners / pair mix), MinerLeaderboard (rail-crown, range chips, click-row to filter), CrownHistoryGrid (60-col per-block grid, direction tabs, 1h/4h range, uid highlight), CrownRateChart (step-line SVG, hover crosshair, optional miner overlay), FilteredMinerSection (EarningNowBanner + EarningDiagnostic + per-miner rate chart + swap history). URL state is the source of truth — range, pair, crownRange, rateRange, pan all live in useSearchParams. Recent miners are cached in localStorage at allways.recentMiners.
4 tasks
…add 2h scoring-window range MinersPage wraps in Page rather than a 100vh Box so the site Footer renders correctly below the content. NetworkOverviewStats switches from a Stack-with-bg-as-divider trick to a CSS Grid with per-cell borders so a shorter tile never reveals a grey strip beneath the row. CrownHistoryGrid's empty-state + as-of Typography elements get component='div' so they stack on separate lines instead of collapsing inline. Adds a 2h range chip that snaps to multiples of SCORING_WINDOW_BLOCKS (600) so the grid mirrors the validator's actual scoring window; pan moves back one window at a time and a 'latest →' shortcut returns to the in-progress window.
- dedicated /miners/:hotkey page; drop FilteredMinerSection - leaderboard: sortable headers, uid/hotkey search, drop rank col - swap history: block-number initiated, swap# links, drop ext col - network overview: pending stripe on current block, swap-direction bars, TileValue suffix pattern (volume τ, success rate) - crown rate chart: miner-mode flips primary line, visible legend, flipped TAO→BTC equation - detail header: hotkey/collateral/activated grid + crown chip - stats strip with range chips on detail page - drop EarningDiagnostic and MinerStatusStrip - ref-based useOnNavigate to stop search-param scroll jumps
- detail page reads uid + crownDirections from MinerStats; drops the full-leaderboard refetch - CrownHistoryGrid: fix dead-ternary useCrownHistory pan args; hover positioned against a gridRef so wrapping the grid doesn't drift it - CrownRateChart: memoize per-direction data shaping so hover-driven re-renders don't refilter the window every mouse event; cursor dot stroke now theme-aware; reduce-based head computation - theme-aware backgrounds throughout (NetworkOverviewStats track, MinerLeaderboard row tint + progress track, detail page status pill, CrownHistoryGrid active chip) — light theme was rendering invisible - keyboard handlers on sortable headers and leaderboard rows (Enter / Space activate, aria-sort on headers, aria-pressed on chips) - MinerSwapHistory: TableContainer for narrow-viewport scroll; null source/dest chain renders as "—" not "undefined→undefined" - StickyNetworkHeader guards null per-direction crown rows - drop dead localStorage.recentMiners write (no consumer)
- New CrownHistoryPanel wraps the crown grid + windowed score factors in one bordered card. Single header + formula; banner + 0.4 dim when the current window's crown share is 0. - CrownHistoryGrid: split label column from cell grid so labels stay sharp under blur; new customFrom/customTo + onCustomRangeChange props with Enter-to-apply inputs (cap = SCORING_WINDOW_BLOCKS); "← earlier" disabled when lo === 0 with "no earlier data" hint. - Leaderboard: replace capacity column with collateral (formatTao τ); search now filters rows and exact-matches numeric uid; "x of y shown" chip. - StickyNetworkHeader: rate.toFixed(2) τ. NetworkOverviewStats: use replaceAll for direction arrows. MinerDetailPage: drop "then Xm to complete" sub; collapse setParam + setParamsBatch into updateParams. - MinersPage default crown range → 2h (was 1h) to match detail page. - Dedup: shortHotkey hoisted to utils/format; ScoreFactorsStrip's dead non-embedded branch dropped now that only the panel renders it; CrownHistoryPanel emits one header instead of two; MAX_CUSTOM_SPAN reuses SCORING_WINDOW_BLOCKS; customInputError moved to useMemo.
Surgical extractions to shrink the two largest files in the dashboard without touching behavior: - New api/models/searchParams.ts owns isRange/isDirection/isCrownRange/ isRateRange/parseBlockParam + CrownRange/RateRange types. MinersPage and MinerDetailPage both import instead of redefining identical guards. - New components/miners/MinerDetailHeader.tsx owns the per-miner header card (HeaderField, PerformanceMetric, PerformanceGrid, RangeChips + fmtDuration + the actual card markup). MinerDetailPage drops from 516 → 156 LOC. - New components/miners/SortHeader.tsx is a generic sortable-column header (typed over the consumer's SortKey union). MinerLeaderboard imports it. - CrownHistoryGrid (1054 → 716 LOC) split into siblings: - crownGridCells.ts: TIER_PALETTE, CellState, buildCells, buildTiers - CrownGridHoverCard.tsx: hover tooltip + HoverLine - CrownGridRangeInputs.tsx: from/to inputs + validation + draft state
The /miners/:hotkey/stats response only started returning uid in the recent polish commit; before that it was undefined, which rendered as "Miner uid ?" in the header. Fall back to the live-miners list (always populated, uid is non-nullable there) so the page is correct against both API versions and while stats is still loading.
53eab93 to
e42a3e4
Compare
eslint react-hooks/exhaustive-deps flagged the inline data ?? [] as making the dependent useMemos rerun every render. Hoist to a useMemo so the array identity is stable across renders with no data changes.
The grid called useCrownHistory({ direction }) with no bounds, so it
always got the API's default ~300-block window. When the user panned
backward (or set a custom range), the grid drew cells for the panned
range but only had data for the most-recent window — every cell read
"no holder" and the locked-uid subjectAbsent banner kicked in falsely.
Meanwhile the score-factors strip queried with explicit bounds and
reported the correct crown share, producing the contradictory "100%
crown share + empty grid" state.
Fix:
- Pass fromBlock=lo, toBlock=hi to useCrownHistory so the fetch matches
what's drawn.
- Anchor pan/snap math on halt.asOfBlock (chain head) instead of the
max block of the fetched data. The two diverged once the fetch
became window-bounded.
- Update "as of #N" footer + isCurrent cell flag to use headBlock.
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.
Problem → Solution
Problem — miners can't see why they aren't earning without DMing the team
/minersroute (and/miners/:hotkeyfiltered detail). Surfaces who holds the crown right now, who held it historically, leaderboard, per-block crown grid, rate chart, and a per-miner diagnostic that explains the current state.Problem — historical crown derivation isn't visible
CrownHistoryGrid— 60-col per-block grid, direction tabs (BTC→TAO / TAO→BTC), 1h / 4h ranges,← earlierpan, uid search highlight, tie indicator (half-tone overlay), tooltip on every cell.CrownRateChart— step-line SVG with hover crosshair + tooltip and an optional miner-rate overlay (dashed) when filtered.Problem — diagnostic / banner / swap history needs to be discoverable from the leaderboard
FilteredMinerSectionwith a blue-rail surface treatment + scroll-into-view on mount.EarningNowBannershows the top-severity diagnostic with a copy-to-clipboard action;EarningDiagnosticshows the full rule list;MinerSwapHistoryshows recent swaps with timed-out / completed pills.URL state contract
All view state serializes to
useSearchParamsso debuggers can share an exact view:range,pair,crownRange,rateRange,pan. Recently-viewed miners are cached inlocalStorageunderallways.recentMiners(capped at 5).Test plan
npm run build— clean, MinersPage chunk is 24 kB / 8 kB gzipminer-dashboard-mock.html— colors, typography, and spacing come from existing theme tokensRelated PRs
Part of the miner-dashboard rollout — merge in dependency order: