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; +}