Phase 2b: log tailer, block history, stdlib WebSocket push

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.
This commit is contained in:
satoshi
2026-04-13 02:53:27 +03:00
parent bd0b1b0318
commit 0a40b8f84f
9 changed files with 633 additions and 14 deletions
+22 -2
View File
@@ -36,6 +36,9 @@ type Snapshot struct {
Chain *bitcoind.BlockchainInfo `json:"chain"`
NetworkHashrateHs float64 `json:"network_hashrate_hs"`
// Recent found blocks (in-memory history; persisted in Phase 2b.5).
RecentBlocks []BlockRecord `json:"recent_blocks,omitempty"`
// Health
CKPoolOK bool `json:"ckpool_ok"`
BitcoinOK bool `json:"bitcoin_ok"`
@@ -49,8 +52,13 @@ type Aggregator struct {
Interval time.Duration
Log *slog.Logger
mu sync.RWMutex
snap Snapshot
// OnRefresh, if set, is called (non-blocking) after each snapshot
// refresh. Used by the WebSocket hub to push updates to clients.
OnRefresh func(Snapshot)
mu sync.RWMutex
snap Snapshot
blocks []BlockRecord
}
func New(ck *ckpool.Client, rpc *bitcoind.RPC, interval time.Duration, log *slog.Logger) *Aggregator {
@@ -132,6 +140,18 @@ func (a *Aggregator) refresh(ctx context.Context) {
}
a.mu.Lock()
// Attach current block history so /api/snapshot and WebSocket
// pushes carry the same view.
if len(a.blocks) > 0 {
next.RecentBlocks = make([]BlockRecord, len(a.blocks))
copy(next.RecentBlocks, a.blocks)
}
a.snap = next
cb := a.OnRefresh
pushed := next
a.mu.Unlock()
if cb != nil {
cb(pushed)
}
}