package nodes import ( "context" "encoding/json" "fmt" "net/http" "strings" "time" ) type RegistryServer struct { addr string token string mgr *Manager server *http.Server } func NewRegistryServer(host string, port int, token string, mgr *Manager) *RegistryServer { addr := strings.TrimSpace(host) if addr == "" { addr = "0.0.0.0" } if port <= 0 { port = 7788 } return &RegistryServer{addr: fmt.Sprintf("%s:%d", addr, port), token: strings.TrimSpace(token), mgr: mgr} } func (s *RegistryServer) Start(ctx context.Context) error { if s.mgr == nil { return nil } mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) mux.HandleFunc("/nodes/register", s.handleRegister) mux.HandleFunc("/nodes/heartbeat", s.handleHeartbeat) s.server = &http.Server{Addr: s.addr, Handler: mux} go func() { <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = s.server.Shutdown(shutdownCtx) }() go func() { _ = s.server.ListenAndServe() }() return nil } func (s *RegistryServer) handleRegister(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if !s.checkAuth(r) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } var n NodeInfo if err := json.NewDecoder(r.Body).Decode(&n); err != nil { http.Error(w, "invalid json", http.StatusBadRequest) return } if strings.TrimSpace(n.ID) == "" { http.Error(w, "id required", http.StatusBadRequest) return } s.mgr.Upsert(n) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "id": n.ID}) } func (s *RegistryServer) handleHeartbeat(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if !s.checkAuth(r) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } var body struct{ ID string `json:"id"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.ID) == "" { http.Error(w, "id required", http.StatusBadRequest) return } n, ok := s.mgr.Get(body.ID) if !ok { http.Error(w, "node not found", http.StatusNotFound) return } n.LastSeenAt = time.Now().UTC() n.Online = true s.mgr.Upsert(n) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "id": body.ID}) } func (s *RegistryServer) checkAuth(r *http.Request) bool { if s.token == "" { return true } auth := strings.TrimSpace(r.Header.Get("Authorization")) return auth == "Bearer "+s.token }