New internal/zmqmon package subscribes to bitcoind's hashblock
ZMQ topic and emits TipEvents. Uses github.com/go-zeromq/zmq4
(pure Go, builds with CGO_ENABLED=0). Exponential backoff on
connection failure so bitcoind restarts don't kill the
subscriber permanently; event channel drops rather than blocks
if the consumer is slow (signals are advisory, not logs).
Aggregator.Run now takes a <-chan TipEvent; when a tip arrives
it fires an immediate refresh() outside the normal ticker
cadence. With a 5s poll interval and 0.5-1s ZMQ latency from
bitcoind, dashboards now reflect new tips roughly 4x faster.
Endpoint comes from BITCOIN_ZMQ_BLOCK — empty disables ZMQ
entirely and the aggregator just runs on the ticker alone.
New internal/store package wraps modernc.org/sqlite (pure Go,
no CGO) with a BlockStore that exposes Open/Close/InsertBlock/
Recent. The aggregator now accepts an optional *store.BlockStore;
on Run() it loads up to maxBlockHistory rows from the store before
the first refresh, and each ingested block gets written to the
DB before being appended to the in-memory ring.
main.go opens the store at cfg.DBPath and logs a warning + falls
back to in-memory-only if the file can't be created — a broken
data volume shouldn't stop the pool from running.
InsertBlock uses INSERT OR IGNORE on the height primary key so
replayed log events after a restart are harmless.
The stratifier can accept a connection and close it without
writing a response during early startup — the refresh loop
then logs 'ckpool poolstats failed: read len: EOF' on every
tick until the stratifier is ready, which is noisy and looks
like a real fault.
- client.Send retries once after 200ms if the server closed
the connection before any bytes were read (io.EOF anywhere
in the wrapped chain).
- Aggregator tracks a ckFailStreak counter: first two
consecutive failures log at DEBUG, third and beyond escalate
to WARN. Successful refreshes reset the streak.
The production image is now a single Go binary that serves both the
JSON/WebSocket API under /api and the Svelte dashboard at /.
- New internal/webui package embeds a dist/ subdir via //go:embed.
A placeholder index.html is committed so `go build` works on a
fresh checkout; anything else in dist/ is regenerated per build
and gitignored.
- httpapi.Server.Handler mounts the embed.FS at / with SPA-style
fallback: unknown non-/api paths serve index.html so client-side
routes survive a reload. /api/* is carved out explicitly so POSTs
or typos never accidentally shadow API semantics with HTML.
- api/Dockerfile grows a node:22 builder stage that runs
`npm ci && npm run build`, and the Go stage copies ui/dist/ into
internal/webui/dist/ before `go build`. Build context moves to
the repo root (docker-compose + `make api` both updated) so the
Dockerfile can see both api/ and ui/.
With this in place, `make up` brings the whole stack online at
http://localhost:8080 — API under /api, dashboard at /. The Vite
dev server on :5173 with the /api proxy is still available via
`make ui-dev` for hot-reload development.
Upstream ckpool tracks user/worker best_ever across block solves but
only emits it in the on-disk users.json/workers.json persistence files,
not in the runtime socket API (userinfo() / workerinfo() in
stratifier.c). Consumers that poll the socket — like kamado-api — see
bestdiff reset to zero on every block solve via reset_bestshares()
with no all-time field to fall back on. That is the "best share stuck
at zero after a block" UX we want to fix in Kamado.
0001-expose-bestever-in-runtime-json.patch adds bestever to the two
JSON_CPACK calls so the UI can show current-round and all-time side
by side. No behavioral change, no impact on share validation or block
handling. Candidate for upstreaming.
Also surfaces BestEver in api/internal/ckpool types (User, Worker).
Gracefully degrades to 0 on an unpatched ckpool.
Adds real-time block detection via ckpool log tailing and a push
channel for the upcoming Svelte UI, all stdlib-only:
- logmon.Tailer follows ckpool.log with rotation/truncation survival
(inode + size tracking) and parses "Solved and confirmed block N"
into BlockEvent values.
- state.Aggregator grows a 256-entry block ring, an OnRefresh hook,
and IngestBlockEvents which best-effort enriches events with the
block hash via bitcoind getblockhash.
- httpapi.Hub implements RFC 6455 from scratch (SHA1 handshake,
unmasked text frames out, masked frames in, ping keepalive,
per-client write mutex, slow-client drop) so we don't pull in a
ws dependency before we can go mod tidy.
- New routes: GET /api/blocks and GET /api/ws. Snapshot pushes fire
on every poll tick and immediately on block-solve.
ZMQ hashblock subscription and SQLite persistence are deferred to
Phase 2b.5 once the s9pk packaging repo exists and we have a real
build environment for adding Go deps.
Go 1.22 module that polls CKPool's Unix socket control API, queries
Bitcoin Core over JSON-RPC, merges both into a thread-safe snapshot, and
serves it over REST. Layout:
api/
├── cmd/kamado-api/main.go signal-aware entrypoint
└── internal/
├── config/ env var loader with validation
├── ckpool/ socket client (4-byte LE length-prefixed wire
│ protocol verified against libckpool.c), typed
│ response models for poolstats/users/workers/
│ clients/uptime, + unit tests using a fake
│ unix socket server
├── bitcoind/ minimal JSON-RPC client, getblockchaininfo
│ and getnetworkhashps
├── state/ Aggregator that refreshes a merged Snapshot
│ on a ticker; readers get a copy under RWMutex
└── httpapi/ REST handlers on Go 1.22 ServeMux:
/api/health /api/pool /api/users
/api/workers /api/clients /api/snapshot
CKPool stores hashrate as "dsps" (diff shares per second); we convert
to H/s via the 2^32 constant used by GoBrrr-Pool and other clients.
Every stat CKPool exposes to its socket API is surfaced — useragent,
IP, per-client diff, per-worker best diff — closing the gap against
Bassin which only reads the 60-second stats files.
Dockerfile does a CGO_ENABLED=0 static build on golang:1.22-bookworm
with -trimpath -ldflags=-s -w. docker-compose now runs both ckpool and
kamado-api, sharing a named volume for /run/ckpool so the API can
dial the stratifier socket directly.
Deferred to Phase 2b (called out in README):
- Bitcoin Core ZMQ hashblock subscriber
- WebSocket push for real-time UI updates
- SQLite persistence (block history, best-share history)
- CKPool log tailer for "Solved and confirmed block" detection
Tests and build NOT run in this commit — Go isn't installed in the
dev environment. Run `make api-test` or `make api` to verify.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>