Log ZMQ→mining.notify latency in ckpool (patch 0005), expose a raw
reject counter (patch 0006), and surface both in the dashboard:
- Block latency card shows avg/last ms, wasted work, and block count
- SharesBar component shows session + all-time accepted/rejected with
a Demon Slayer flame-slash animation on new shares
- Miners card info moved to MinersTable section header
- Latency stats and share counts persist across restarts via kv store
- Removed misleading pool-wide share stats from per-worker/user pages
(ckpool doesn't expose per-user raw counts)
Shows luck as best_share / total_work * 100%. Green for >100% (lucky),
red for <100% (unlucky), multiplier format for >=1000%. Hover tooltip
explains the metric.
Total work was summing pool.Shares (ckpool's accounted_shares — raw
per-share count) and multiplying by 2^32, which is nonsense: each
share's actual difficulty was ignored, so a pool running at any
real hashrate would show a tiny number. Switch to pool.Accepted,
which ckpool exposes as accounted_diff_shares (sum of each accepted
share's difficulty in diff-1-normalized units). cumulative_shares *
2^32 is now actually total hashes.
Bump the kv key from "cumulative_shares" to "cumulative_work" so
any value saved under the old name is orphaned rather than mixed
into the new (correctly-unit'd) counter on upgrade.
Total work tile's sub-info now shows the round effort:
cumulative_shares / network_difficulty * 100. Matches ckpool's own
formula at stratifier.c:8201 for the percent-of-block display.
Drop the duplicate effort text from the Expected block tile, which
was an uptime-based approximation of the same thing.
The diff-adjustment tile was showing raw epoch progress (blocks-so-far
/ 2016 * 100%), which is never what anyone wants to see there. Compute
the real predicted change server-side: fetch the timestamp of the
first block in the current retarget epoch once per epoch, then each
refresh derive (expected_interval / actual_interval - 1) * 100,
clamped to Bitcoin's consensus bounds of [-75%, +300%]. The tile now
renders the signed percent, green when positive, red when negative.
Tile layout is forced to an explicit 5x2 grid so the number of
columns doesn't depend on viewport width. Reordering puts the
difficulty tile at (row 1, col 5) with the next-adjustment tile
directly below it at (row 2, col 5); block height, block reward,
total work, and expected block fill the rest of the second row.
Mobile breakpoints fall back to 3 and then 2 columns.
Header keeps only a live/connecting/offline status dot — the chain
name and block height badges were redundant now that the height tile
exists and animates on its own.
Stat-sub text bumped from 0.85em to 0.95em for readability.
Background: a warm ember-glow radial at the top of the viewport
layered with two accent radials and a low-opacity haori-checker tile
pattern (inline SVG data URI). Cards get a translucent backdrop so
the texture peeks through between them.
Dashboard now renders 10 tiles in a 5x2 overview: hashrate, best
share, miners, network hashrate, and expected block on the top row;
difficulty, block height, block reward, total work, and the
difficulty-adjustment countdown on the bottom row. Difficulty is
rendered with T/P suffixes instead of scientific notation, the main
hashrate card shows the 1-minute value, and the block-height tile
pulses orange when the network tip advances.
Added a 24-hour hashrate area chart below the overview, sampled
once per minute. Samples are persisted to a new hashrate_samples
SQLite table and restored on startup so the chart doesn't reset
every time kamado-api is restarted.
Cumulative pool work (sum of accepted diff-1-normalized shares) is
now tracked across ckpool restarts. The aggregator integrates only
positive deltas on pool.Shares — a regression means ckpool's
counter reset to zero and the baseline is refreshed without losing
the running total. A hasPoolSharesBaseline flag prevents double-
counting on the first refresh after a kamado-api restart. The
value is persisted to a new kv table once per minute.
Next-block reward (subsidy + fees) is fetched from bitcoind
getblocktemplate at most once per minute and surfaced as a tile.
Header's block-height badge now reads prevHeight via untrack() so
the effect doesn't form a dependency cycle with its own write.
Svelte 5 + Vite + TypeScript dashboard that consumes kamado-api over
REST for the first paint and then subscribes to /api/ws for live
updates. Zero runtime deps beyond svelte itself; plain CSS, no
component library.
Layout:
- Header with brand, WebSocket status badge, chain + height
- PoolOverview: hashrate (1m/5m/1h/24h), miner count, network
hashrate + diff, pool share (ppb), expected time to block, uptime
- BlocksTable: recent solves from the in-memory ring (will be
SQLite-backed in Phase 2b.5)
- BestShares: top-10 workers by bestever, falling back to bestdiff
when the ckpool patch isn't present
- MinersTable: joined view of stratum clients and workers with
user-agent-based hardware detection (Bitaxe, NerdQAxe, Antminer,
...), hashrate, best-round, best-ever, last share
State is a single $state() snapshot store in svelte-runes form;
components read from it via $derived. The store does one initial
REST snapshot fetch, then owns the WebSocket with exponential
backoff reconnects.
Vite dev server on :5173 proxies /api and /api/ws to localhost:8080
so you can run `make ui-dev` alongside `make up` in development.
Production serving (bundled into the Go binary via embed, behind /
on :8080) lands in Phase 4.