Dashboard polish: per-user modal, block-found animation, readable chart

Block-reward tile was cached for 60s, so users saw it sit still even
as bitcoind's CreateNewBlock fired every few seconds with updated
fee totals. Drop the template cache TTL to 15s — bitcoind caches
the template internally, so the extra RPC cost is trivial.

Hashrate chart was rendering 1440 raw per-minute points across a
~780px plot, which collapsed into a noisy smear. Bucket-average to
~96 points so the chart actually communicates a trend. Raw samples
under the target pass through unchanged (early process lifetime).

Layout: Blocks found is now full-width, with the Best shares
leaderboard stacked below it. The previous side-by-side was
squeezing both tables on typical displays.

New user-detail modal. Clicking a BTC address in the Miners table
opens a focused view: per-user aggregate hashrate (1m/5m/1h/24h),
best round / best ever, and a worker-level breakdown with the same
columns as the main table. Driven by a small selection store so any
component in the tree can open it. Closes on backdrop click or Esc.

New block-found animation. When the network tip advances, a brief
full-screen overlay plays: a radial Hinokami Kagura ember burst
above the header, a faint sun-ray sweep, and ~22 falling sakura
petals with randomised drift / rotation / delay so no two blocks
look identical. The existing block-height tile flash still fires
alongside; the overlay is pointer-events: none so nothing in the
UI becomes unreachable during the ~3.6s animation.
This commit is contained in:
satoshi
2026-04-24 00:46:16 +03:00
parent cbea202929
commit c104c1eefa
7 changed files with 557 additions and 10 deletions
+13
View File
@@ -0,0 +1,13 @@
// Cross-component selection state. Currently just tracks the user
// whose detail modal is open; App renders the modal off this store
// so any component in the tree can open it by setting `user`.
export const selection = $state<{ user: string | null }>({ user: null });
export function selectUser(address: string): void {
selection.user = address;
}
export function clearSelection(): void {
selection.user = null;
}