package main import ( "encoding/json" "fmt" "net/http" "strconv" ) // newAdminHandler exposes the rotation control surface. It is intended to bind // to localhost only (WEIRCON_ADMIN_LISTEN) and is called by rotate.sh: // // POST /drain?id=N take slot N out of service; blocks until in-flight // drains (bounded by WEIRCON_DRAIN_TIMEOUT_SEC) + settle. // 200 {"id":N,"drained":true,"inflight":0} on a clean drain, // "drained":false with the residual count on timeout. // POST /undrain?id=N return slot N to service after the warmup window. // GET /status snapshot of every slot. func newAdminHandler(cfg Config, mgr *proxyManager) http.Handler { mux := http.NewServeMux() slotID := func(w http.ResponseWriter, r *http.Request) (int, bool) { raw := r.URL.Query().Get("id") id, err := strconv.Atoi(raw) if err != nil || id < 0 || id >= mgr.n { http.Error(w, fmt.Sprintf("id must be 0-%d", mgr.n-1), http.StatusBadRequest) return 0, false } return id, true } writeJSON := func(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(v) } mux.HandleFunc("/drain", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } id, ok := slotID(w, r) if !ok { return } residual := mgr.drain(id, cfg.DrainTimeout) writeJSON(w, map[string]any{"id": id, "drained": residual == 0, "inflight": residual}) }) mux.HandleFunc("/undrain", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } id, ok := slotID(w, r) if !ok { return } mgr.undrain(id) writeJSON(w, map[string]any{"id": id, "warmup_sec": int(cfg.DrainWarmup.Seconds())}) }) mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, mgr.snapshot()) }) return mux }