From c104c1eefa46b1d2ce943f73cbe2bf83773990ac Mon Sep 17 00:00:00 2001 From: satoshi Date: Fri, 24 Apr 2026 00:46:16 +0300 Subject: [PATCH] Dashboard polish: per-user modal, block-found animation, readable chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- api/internal/state/aggregator.go | 6 +- ui/src/App.svelte | 11 +- ui/src/lib/BlockFoundAnimation.svelte | 183 ++++++++++++++++ ui/src/lib/HashrateChart.svelte | 27 ++- ui/src/lib/MinersTable.svelte | 25 ++- ui/src/lib/UserDetailModal.svelte | 302 ++++++++++++++++++++++++++ ui/src/stores/selection.svelte.ts | 13 ++ 7 files changed, 557 insertions(+), 10 deletions(-) create mode 100644 ui/src/lib/BlockFoundAnimation.svelte create mode 100644 ui/src/lib/UserDetailModal.svelte create mode 100644 ui/src/stores/selection.svelte.ts diff --git a/api/internal/state/aggregator.go b/api/internal/state/aggregator.go index b4447b8..5db4bbc 100644 --- a/api/internal/state/aggregator.go +++ b/api/internal/state/aggregator.go @@ -340,9 +340,11 @@ func (a *Aggregator) refresh(ctx context.Context) { copy(next.HashrateHistory, a.hrHistory) } - // --- next-block reward (at most once per minute) --- + // --- next-block reward (refreshed every ~15s so users see the + // fee component tick up as mempool grows; bitcoind caches the + // template internally, so the RPC is cheap even at this cadence). next.NextBlockRewardBTC = a.nextBlockReward - needTemplate := now.Sub(a.lastTemplateFetch) >= time.Minute + needTemplate := now.Sub(a.lastTemplateFetch) >= 15*time.Second a.mu.Unlock() if needTemplate && a.RPC != nil && next.BitcoinOK { diff --git a/ui/src/App.svelte b/ui/src/App.svelte index ec4fb4d..33d1568 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -7,6 +7,8 @@ import MinersTable from "./lib/MinersTable.svelte"; import BlocksTable from "./lib/BlocksTable.svelte"; import BestShares from "./lib/BestShares.svelte"; + import UserDetailModal from "./lib/UserDetailModal.svelte"; + import BlockFoundAnimation from "./lib/BlockFoundAnimation.svelte"; onMount(() => { connect(); @@ -27,14 +29,15 @@ {:else} -
- - -
+ + {/if} + + + diff --git a/ui/src/lib/HashrateChart.svelte b/ui/src/lib/HashrateChart.svelte index 8f4501d..dfc63e2 100644 --- a/ui/src/lib/HashrateChart.svelte +++ b/ui/src/lib/HashrateChart.svelte @@ -8,7 +8,32 @@ const plotW = W - PAD.left - PAD.right; const plotH = H - PAD.top - PAD.bottom; - const points = $derived(snap.data?.hashrate_history ?? []); + const rawPoints = $derived(snap.data?.hashrate_history ?? []); + + // Bucket raw per-minute samples into ~96 time buckets for a readable + // 24h chart. Averaging each bucket smooths out short-term jitter + // without losing the trend. When we have fewer raw points than the + // target (early in the process's life), just pass them through. + const TARGET_POINTS = 96; + const points = $derived.by(() => { + if (rawPoints.length <= TARGET_POINTS) return rawPoints; + const bucketSize = Math.ceil(rawPoints.length / TARGET_POINTS); + const out: { t: number; v: number }[] = []; + for (let i = 0; i < rawPoints.length; i += bucketSize) { + let sum = 0; + let count = 0; + let tSum = 0; + for (let j = i; j < Math.min(i + bucketSize, rawPoints.length); j++) { + sum += rawPoints[j].v; + tSum += rawPoints[j].t; + count++; + } + if (count > 0) { + out.push({ t: Math.round(tSum / count), v: sum / count }); + } + } + return out; + }); const chart = $derived.by(() => { if (points.length < 2) return null; diff --git a/ui/src/lib/MinersTable.svelte b/ui/src/lib/MinersTable.svelte index c4441d9..6b293e2 100644 --- a/ui/src/lib/MinersTable.svelte +++ b/ui/src/lib/MinersTable.svelte @@ -1,5 +1,6 @@ + + + +{#if address} + + + +{/if} + + diff --git a/ui/src/stores/selection.svelte.ts b/ui/src/stores/selection.svelte.ts new file mode 100644 index 0000000..de18a09 --- /dev/null +++ b/ui/src/stores/selection.svelte.ts @@ -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; +}