Files
AetherForge/server/internal/api/handlers.go
AetherForge 8466c7aa9b fix: 2026-06-04 audit pass — README, USB pack, multi-area fixes
WS ticket dashboard auth, builder universal signing/size limits/fusion obfuscation/dropper bundles, Path Tracer WireGuard topology, SessionGate degraded mode and download timeouts, server bootstrap (data dir, cloudflared dedupe, config port precedence), agent mesh/miner/spread fixes. README refreshed; usb bundle repacked; PROBLEMS.md audit log updated.
2026-06-04 20:41:44 -07:00

197 lines
4.9 KiB
Go

package api
import (
"encoding/json"
"net/http"
"strconv"
"time"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"github.com/go-chi/chi/v5"
)
var serverStartTime = time.Now()
type Handler struct {
db *db.Database
}
func NewHandler(database *db.Database) *Handler {
return &Handler{db: database}
}
// GET /api/v1/dashboard/stats
func (h *Handler) GetDashboardStats(w http.ResponseWriter, r *http.Request) {
stats, err := h.db.GetFleetStats()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, stats)
}
// GET /api/v1/agents
func (h *Handler) ListAgents(w http.ResponseWriter, r *http.Request) {
agents, err := h.db.ListAgents()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if agents == nil {
agents = []*models.Agent{}
}
writeJSON(w, agents)
}
// GET /api/v1/agents/{id}
func (h *Handler) GetAgent(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
agent, err := h.db.GetAgent(id)
if err != nil {
http.Error(w, "Agent not found", http.StatusNotFound)
return
}
writeJSON(w, agent)
}
// GET /api/v1/agents/{id}/stats
func (h *Handler) GetAgentStats(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if _, err := h.db.GetAgent(id); err != nil {
http.Error(w, "Agent not found", http.StatusNotFound)
return
}
limitStr := r.URL.Query().Get("limit")
limit := 100
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
limit = l
}
if limit > 1000 {
limit = 1000
}
samples, err := h.db.GetHashrateHistory(id, limit)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if samples == nil {
samples = []*models.HashrateSample{}
}
writeJSON(w, samples)
}
// GET /api/v1/shares
func (h *Handler) GetRecentShares(w http.ResponseWriter, r *http.Request) {
limitStr := r.URL.Query().Get("limit")
limit := 50
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
limit = l
}
if limit > 1000 {
limit = 1000
}
shares, err := h.db.GetRecentShares(limit)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if shares == nil {
shares = []*models.Share{}
}
writeJSON(w, shares)
}
// GET /api/v1/health
func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]string{"status": "ok"})
}
// GET /api/v1/server/ready
// Requires auth (not in the basicAuthMiddleware bypass list).
// Add ?verbose=1 to get full diagnostics; omit for a lightweight liveness check.
func (h *Handler) ServerReady(w http.ResponseWriter, r *http.Request) {
uptime := int64(time.Since(serverStartTime).Seconds())
resp := map[string]interface{}{
"status": "ok",
"uptime_s": uptime,
}
if r.URL.Query().Get("verbose") != "1" {
writeJSON(w, resp)
return
}
// DB ping
dbStatus := "ok"
if err := h.db.QueryRow("SELECT 1").Scan(new(int)); err != nil {
dbStatus = "error: " + err.Error()
resp["status"] = "degraded"
}
resp["db"] = dbStatus
// Count online agents
var onlineAgents int
if err := h.db.QueryRow("SELECT COUNT(*) FROM agents WHERE status = 'online'").Scan(&onlineAgents); err != nil {
onlineAgents = -1
}
resp["agents_online"] = onlineAgents
if resp["status"] == "degraded" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(resp)
return
}
writeJSON(w, resp)
}
// GET /api/v1/builds
func (h *Handler) ListBuilds(w http.ResponseWriter, r *http.Request) {
builds, err := h.db.ListBuilds(50)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if builds == nil {
builds = []*models.BuildRecord{}
}
writeJSON(w, builds)
}
// PUT /api/v1/builds/{id}/pin
// Pins the specified build as the active dropper target.
// Send an empty id or DELETE to a fake pin endpoint to unpin all.
func (h *Handler) PinBuild(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if err := h.db.SetPinnedBuild(id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{"ok": true, "pinned_id": id})
}
// DELETE /api/v1/builds/pin (unpin all without deleting anything)
func (h *Handler) UnpinAll(w http.ResponseWriter, r *http.Request) {
if err := h.db.SetPinnedBuild(""); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{"ok": true})
}
// DELETE /api/v1/builds/{id}
func (h *Handler) DeleteBuild(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if err := h.db.DeleteBuild(id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{"ok": true, "deleted_id": id})
}
func writeJSON(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
}