Files
KamadoPool/api/internal/httpapi/server.go
T
satoshi 0a40b8f84f 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.
2026-04-13 02:53:27 +03:00

93 lines
2.7 KiB
Go

// Package httpapi serves the Kamado REST API. WebSocket push and static
// UI serving land in a follow-up commit.
package httpapi
import (
"encoding/json"
"log/slog"
"net/http"
"github.com/kamadopool/kamado-api/internal/state"
)
type Server struct {
Agg *state.Aggregator
Hub *Hub
Log *slog.Logger
}
func New(agg *state.Aggregator, log *slog.Logger) *Server {
return &Server{Agg: agg, Hub: NewHub(), Log: log}
}
// Handler returns an http.Handler with all kamado routes mounted under /api.
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/health", s.health)
mux.HandleFunc("GET /api/pool", s.pool)
mux.HandleFunc("GET /api/users", s.users)
mux.HandleFunc("GET /api/workers", s.workers)
mux.HandleFunc("GET /api/clients", s.clients)
mux.HandleFunc("GET /api/blocks", s.blocks)
mux.HandleFunc("GET /api/snapshot", s.snapshot)
mux.HandleFunc("GET /api/ws", s.handleWS)
return mux
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
// ---- handlers ---------------------------------------------------------
func (s *Server) health(w http.ResponseWriter, r *http.Request) {
snap := s.Agg.Snapshot()
status := http.StatusOK
if !snap.CKPoolOK || !snap.BitcoinOK {
status = http.StatusServiceUnavailable
}
writeJSON(w, status, map[string]any{
"ok": snap.CKPoolOK && snap.BitcoinOK,
"ckpool": snap.CKPoolOK,
"bitcoin": snap.BitcoinOK,
"last_error": snap.LastError,
})
}
func (s *Server) snapshot(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, s.Agg.Snapshot())
}
func (s *Server) pool(w http.ResponseWriter, r *http.Request) {
snap := s.Agg.Snapshot()
writeJSON(w, http.StatusOK, map[string]any{
"pool": snap.Pool,
"uptime_seconds": snap.Uptime,
"hashrate_hs_1m": snap.HashrateHs,
"hashrate_hs_5m": snap.HashrateHs5m,
"hashrate_hs_1h": snap.HashrateHs1h,
"hashrate_hs_24h": snap.HashrateHs24h,
"chain": snap.Chain,
"network_hashrate_hs": snap.NetworkHashrateHs,
})
}
func (s *Server) users(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, s.Agg.Snapshot().Users)
}
func (s *Server) workers(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, s.Agg.Snapshot().Workers)
}
func (s *Server) clients(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, s.Agg.Snapshot().Clients)
}
func (s *Server) blocks(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, s.Agg.Blocks())
}