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