package server import ( "encoding/json" "fmt" "go-upkeep/internal/models" "go-upkeep/internal/monitor" "go-upkeep/internal/store" "html/template" "net/http" "sort" ) type ServerConfig struct { Port int EnableStatus bool Title string ClusterKey string // Shared Secret for Security } func Start(cfg ServerConfig) { mux := http.NewServeMux() // 1. Push Heartbeat mux.HandleFunc("/api/push", func(w http.ResponseWriter, r *http.Request) { token := r.URL.Query().Get("token") if token == "" { http.Error(w, "Missing token", 400); return } if monitor.RecordHeartbeat(token) { w.WriteHeader(http.StatusOK); w.Write([]byte("OK")) } else { http.Error(w, "Invalid Token", 404) } }) // 2. Health Check (For Cluster Follower) mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { if cfg.ClusterKey != "" && r.Header.Get("X-Upkeep-Secret") != cfg.ClusterKey { http.Error(w, "Unauthorized", 401) return } w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) }) // 3. Config Export mux.HandleFunc("/api/backup/export", func(w http.ResponseWriter, r *http.Request) { if cfg.ClusterKey == "" || r.Header.Get("X-Upkeep-Secret") != cfg.ClusterKey { http.Error(w, "Unauthorized: UPKEEP_CLUSTER_SECRET required", 401) return } data := store.Get().ExportData() json.NewEncoder(w).Encode(data) }) // 4. Config Import mux.HandleFunc("/api/backup/import", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "POST required", 405); return } if cfg.ClusterKey == "" || r.Header.Get("X-Upkeep-Secret") != cfg.ClusterKey { http.Error(w, "Unauthorized", 401) return } var data models.Backup if err := json.NewDecoder(r.Body).Decode(&data); err != nil { http.Error(w, "Invalid JSON", 400) return } if err := store.Get().ImportData(data); err != nil { http.Error(w, "Import Failed: "+err.Error(), 500) return } w.Write([]byte("Import Successful")) }) // 5. Status Page if cfg.EnableStatus { mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { renderStatusPage(w, cfg.Title) }) mux.HandleFunc("/status/json", func(w http.ResponseWriter, r *http.Request) { monitor.Mutex.RLock(); defer monitor.Mutex.RUnlock() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(monitor.LiveState) }) } go func() { addr := fmt.Sprintf(":%d", cfg.Port) fmt.Printf("HTTP Server listening on %s\n", addr) http.ListenAndServe(addr, mux) }() } func renderStatusPage(w http.ResponseWriter, title string) { monitor.Mutex.RLock() var sites []models.Site for _, s := range monitor.LiveState { sites = append(sites, s) } monitor.Mutex.RUnlock() sort.Slice(sites, func(i, j int) bool { if sites[i].Status != sites[j].Status { if sites[i].Status == "DOWN" { return true } if sites[j].Status == "DOWN" { return false } } return sites[i].Name < sites[j].Name }) const tpl = `