Skip to content

Miner dashboard: /miners route, leaderboard, crown grid + rate chart#90

Merged
LandynDev merged 11 commits into
testfrom
feat/miner-dashboard
May 17, 2026
Merged

Miner dashboard: /miners route, leaderboard, crown grid + rate chart#90
LandynDev merged 11 commits into
testfrom
feat/miner-dashboard

Conversation

@LandynDev
Copy link
Copy Markdown
Collaborator

@LandynDev LandynDev commented May 12, 2026

Problem → Solution

Problem — miners can't see why they aren't earning without DMing the team

  • The classic Luis-style case: a miner posts a rate, sees no earnings, has no public surface to diagnose.
  • New /miners route (and /miners/:hotkey filtered 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, ← earlier pan, 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

  • FilteredMinerSection with a blue-rail surface treatment + scroll-into-view on mount. EarningNowBanner shows the top-severity diagnostic with a copy-to-clipboard action; EarningDiagnostic shows the full rule list; MinerSwapHistory shows recent swaps with timed-out / completed pills.

URL state contract

All view state serializes to useSearchParams so debuggers can share an exact view: range, pair, crownRange, rateRange, pan. Recently-viewed miners are cached in localStorage under allways.recentMiners (capped at 5).

Test plan

  • npm run build — clean, MinersPage chunk is 24 kB / 8 kB gzip
  • Direct port from miner-dashboard-mock.html — colors, typography, and spacing come from existing theme tokens
  • Empty-state copy for every component (no miners / no rate activity / no swaps yet / no rate history yet)
  • After merge: manual sweep against staging — network view + filtered view + sticky header

Related PRs

Part of the miner-dashboard rollout — merge in dependency order:

  1. allways-db#15 — schema
  2. alw-utils#56 — sync utility
  3. das-allways#17 — API
  4. allways-ui (this PR) — page + components

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.
LandynDev added 7 commits May 12, 2026 17:22
…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.
@LandynDev LandynDev force-pushed the feat/miner-dashboard branch from 53eab93 to e42a3e4 Compare May 16, 2026 23:51
github-actions Bot and others added 3 commits May 16, 2026 23:52
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.
@LandynDev LandynDev changed the title Miner dashboard: /miners route, leaderboard, crown grid + rate chart, diagnostic Miner dashboard: /miners route, leaderboard, crown grid + rate chart May 17, 2026
@LandynDev LandynDev merged commit 2a83b6b into test May 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant