package api import ( "encoding/json" "io" "log" "net/http" "strings" "time" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // Handler handles API endpoints for report ingest and customer queries. type Handler struct { store *store.Store apiKey string logger *log.Logger } // New creates a new API handler. func New(store *store.Store, apiKey string, logger *log.Logger) *Handler { return &Handler{ store: store, apiKey: apiKey, logger: logger, } } // ServeHTTP routes API requests. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/v1") switch { case r.Method == http.MethodPost && path == "/report": h.handleReport(w, r) case r.Method == http.MethodGet && path == "/customers": h.handleCustomers(w, r) case r.Method == http.MethodGet && strings.HasPrefix(path, "/customers/"): parts := strings.Split(strings.TrimPrefix(path, "/customers/"), "/") customerID := parts[0] if len(parts) > 1 && parts[1] == "history" { h.handleCustomerHistory(w, r, customerID) } else { h.handleCustomer(w, r, customerID) } default: http.NotFound(w, r) } } func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request) { // Verify bearer token if h.apiKey != "" { auth := r.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") || strings.TrimPrefix(auth, "Bearer ") != h.apiKey { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } } body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1MB limit if err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } // Extract customer_id from JSON var payload struct { CustomerID string `json:"customer_id"` } if err := json.Unmarshal(body, &payload); err != nil || payload.CustomerID == "" { http.Error(w, "Invalid payload: customer_id required", http.StatusBadRequest) return } if err := h.store.SaveReport(payload.CustomerID, body); err != nil { h.logger.Printf("[ERROR] Failed to save report from %s: %v", payload.CustomerID, err) http.Error(w, "Internal error", http.StatusInternalServerError) return } h.logger.Printf("[INFO] Received report from %s (%d bytes)", payload.CustomerID, len(body)) w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"ok"}`)) } func (h *Handler) handleCustomers(w http.ResponseWriter, r *http.Request) { customers, err := h.store.GetCustomers() if err != nil { h.logger.Printf("[ERROR] Failed to get customers: %v", err) http.Error(w, "Internal error", http.StatusInternalServerError) return } type customerJSON struct { ID string `json:"id"` Name string `json:"name"` ControllerVersion string `json:"controller_version"` HealthStatus string `json:"health_status"` LastSeen time.Time `json:"last_seen"` CPUPercent float64 `json:"cpu_percent"` MemoryPercent float64 `json:"memory_percent"` ContainerTotal int `json:"container_total"` ContainerRunning int `json:"container_running"` BackupLastSnapshot *time.Time `json:"backup_last_snapshot"` } result := make([]customerJSON, 0, len(customers)) for _, c := range customers { result = append(result, customerJSON{ ID: c.CustomerID, Name: c.CustomerName, ControllerVersion: c.ControllerVersion, HealthStatus: c.HealthStatus, LastSeen: c.ReceivedAt, CPUPercent: c.CPUPercent, MemoryPercent: c.MemoryPercent, ContainerTotal: c.ContainerTotal, ContainerRunning: c.ContainerRunning, BackupLastSnapshot: c.BackupLastSnapshot, }) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) } func (h *Handler) handleCustomer(w http.ResponseWriter, r *http.Request, customerID string) { customer, err := h.store.GetCustomer(customerID) if err != nil { h.logger.Printf("[ERROR] Failed to get customer %s: %v", customerID, err) http.Error(w, "Internal error", http.StatusInternalServerError) return } if customer == nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/json") // Return the full report JSON directly w.Write([]byte(customer.ReportJSON)) } func (h *Handler) handleCustomerHistory(w http.ResponseWriter, r *http.Request, customerID string) { period := r.URL.Query().Get("period") var since time.Duration switch period { case "7d": since = 7 * 24 * time.Hour case "30d": since = 30 * 24 * time.Hour default: since = 24 * time.Hour } history, err := h.store.GetCustomerHistory(customerID, since) if err != nil { h.logger.Printf("[ERROR] Failed to get history for %s: %v", customerID, err) http.Error(w, "Internal error", http.StatusInternalServerError) return } type historyEntry struct { ReceivedAt time.Time `json:"received_at"` HealthStatus string `json:"health_status"` CPUPercent float64 `json:"cpu_percent"` MemoryPercent float64 `json:"memory_percent"` } result := make([]historyEntry, 0, len(history)) for _, h := range history { result = append(result, historyEntry{ ReceivedAt: h.ReceivedAt, HealthStatus: h.HealthStatus, CPUPercent: h.CPUPercent, MemoryPercent: h.MemoryPercent, }) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) }