package webapp import ( "encoding/json" "errors" "net/http" ) // clusterStatusResponse is the JSON the operator-facing // /web/api/cluster/status endpoint returns. The shape is stable so the // e2e shell script and any future frontend pages can pin against it. // // FSMConfigVersion / FSMOutboxLen / FSMPartition surface the FSM-side // operator signals from plan section 6.1 (config_version, outbox // length, partition_state) so a single GET tells the operator what // config the cluster has adopted, whether the notification outbox is // draining, and whether the cluster sees itself as partitioned. type clusterStatusResponse struct { SelfID string `json:"self_id"` Role string `json:"role"` Term uint64 `json:"term"` LeaderID string `json:"leader_id"` Voters []string `json:"voters"` AppliedIndex uint64 `json:"applied_index"` CommitIndex uint64 `json:"commit_index"` FSMChecks int `json:"fsm_checks"` FSMMembers int `json:"fsm_membership"` FSMConfigVersion uint64 `json:"fsm_config_version"` FSMOutboxLen int `json:"fsm_outbox_len"` FSMPartition string `json:"fsm_partition"` ClusterID string `json:"cluster_id"` LocalAddr string `json:"local_addr"` } // handleClusterStatus serializes the current cluster state for the // operator. Returns 503 if no cluster is attached; 200 otherwise. // // Admin-only: the worker webapp is single-tenant so the session // middleware (requireSession) is the admin check. Cross-tenant // protection is not required at this layer. // // The `r` parameter is unused but kept so the signature matches // http.HandlerFunc (the route is registered via requireSession). func (s *Server) handleClusterStatus(w http.ResponseWriter, _ *http.Request) { writeNoStore(w) if s.cluster == nil { http.Error(w, "cluster not configured", http.StatusServiceUnavailable) return } stats := s.cluster.Stats() resp := clusterStatusResponse{ SelfID: stats.NodeID, Role: stats.State, Term: stats.Term, LeaderID: stats.Leader, Voters: stats.Voters, AppliedIndex: stats.AppliedIndex, CommitIndex: stats.LastIndex, FSMChecks: stats.FSMChecks, FSMMembers: stats.FSMMembers, FSMConfigVersion: stats.FSMConfigVersion, FSMOutboxLen: stats.FSMOutboxLen, FSMPartition: stats.FSMPartition, ClusterID: s.cluster.ClusterID(), LocalAddr: s.cluster.LocalAddr(), } if resp.Voters == nil { resp.Voters = []string{} } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(resp); err != nil { s.deps.Logger.Printf("cluster status encode: %v", err) } } // ErrClusterNotConfigured is returned when a cluster-admin endpoint is // hit on a server without a cluster attached. var ErrClusterNotConfigured = errors.New("webapp: cluster not configured")