1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
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
}
|