package api import ( "encoding/json" "net/http" "strconv" "time" "github.com/thepeterstone/claudomator/internal/role" "github.com/thepeterstone/claudomator/internal/storage" ) // roleVersionView is the JSON shape returned for a single role_configs // version — decodes the stored config_json back into a role.RoleConfig so // clients get structured fields rather than an opaque string. type roleVersionView struct { ID string `json:"id"` Role string `json:"role"` Version int `json:"version"` Status string `json:"status"` Config role.RoleConfig `json:"config"` CreatedAt string `json:"created_at"` ActivatedAt string `json:"activated_at,omitempty"` RetiredAt string `json:"retired_at,omitempty"` ProposedBy string `json:"proposed_by,omitempty"` } // handleListRoleNames handles GET /api/roles — every distinct role name // that has at least one role_configs row. There is no other way to discover // which roles exist short of guessing names; the role/config management // panel uses this as its entry point before fetching each role's version // history via GET /api/roles/{role}/versions. func (s *Server) handleListRoleNames(w http.ResponseWriter, _ *http.Request) { names, err := s.store.ListRoleNames() if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if names == nil { names = []string{} } writeJSON(w, http.StatusOK, names) } // handleCreateRoleVersion handles POST /api/roles/{role}/versions. The // request body is a role.RoleConfig (config_json shape, plus an optional // proposed_by field); a new draft version is created with the next version // number for that role. Mirrors handleCreateProject's shape (see // internal/api/projects.go). func (s *Server) handleCreateRoleVersion(w http.ResponseWriter, r *http.Request) { roleName := r.PathValue("role") if roleName == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "role is required"}) return } var input struct { role.RoleConfig ProposedBy string `json:"proposed_by"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()}) return } input.RoleConfig.Role = roleName proposedBy := input.ProposedBy if proposedBy == "" { proposedBy = "human" } configJSON, err := json.Marshal(input.RoleConfig) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } created, err := s.store.CreateRoleConfig(roleName, string(configJSON), proposedBy) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusCreated, toRoleVersionView(created)) } // handleListRoleVersions handles GET /api/roles/{role}/versions. func (s *Server) handleListRoleVersions(w http.ResponseWriter, r *http.Request) { roleName := r.PathValue("role") rows, err := s.store.ListRoleConfigVersions(roleName) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } views := make([]*roleVersionView, 0, len(rows)) for _, row := range rows { views = append(views, toRoleVersionView(row)) } writeJSON(w, http.StatusOK, views) } // handleActivateRoleVersion handles POST /api/roles/{role}/activate?version=N. func (s *Server) handleActivateRoleVersion(w http.ResponseWriter, r *http.Request) { roleName := r.PathValue("role") versionStr := r.URL.Query().Get("version") version, err := strconv.Atoi(versionStr) if err != nil || version < 1 { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid or missing version query param"}) return } if err := s.store.ActivateRoleConfigVersion(roleName, version); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "message": "role config version activated", "role": roleName, "version": version, }) } func toRoleVersionView(row *storage.RoleConfigRow) *roleVersionView { v := &roleVersionView{ ID: row.ID, Role: row.Role, Version: row.Version, Status: row.Status, CreatedAt: row.CreatedAt.Format(time.RFC3339), ProposedBy: row.ProposedBy, } if row.ActivatedAt != nil { v.ActivatedAt = row.ActivatedAt.Format(time.RFC3339) } if row.RetiredAt != nil { v.RetiredAt = row.RetiredAt.Format(time.RFC3339) } _ = json.Unmarshal([]byte(row.ConfigJSON), &v.Config) return v }