|
17e2c1d…
|
lmata
|
1 |
package api |
|
17e2c1d…
|
lmata
|
2 |
|
|
17e2c1d…
|
lmata
|
3 |
import ( |
|
17e2c1d…
|
lmata
|
4 |
"encoding/json" |
|
17e2c1d…
|
lmata
|
5 |
"net/http" |
|
17e2c1d…
|
lmata
|
6 |
"path/filepath" |
|
17e2c1d…
|
lmata
|
7 |
|
|
17e2c1d…
|
lmata
|
8 |
"github.com/conflicthq/scuttlebot/internal/config" |
|
17e2c1d…
|
lmata
|
9 |
) |
|
17e2c1d…
|
lmata
|
10 |
|
|
17e2c1d…
|
lmata
|
11 |
// configView is the JSON shape returned by GET /v1/config. |
|
17e2c1d…
|
lmata
|
12 |
// Secrets are masked — zero values mean "no change" on PUT. |
|
17e2c1d…
|
lmata
|
13 |
type configView struct { |
|
763c873…
|
lmata
|
14 |
APIAddr string `json:"api_addr"` |
|
763c873…
|
lmata
|
15 |
MCPAddr string `json:"mcp_addr"` |
|
763c873…
|
lmata
|
16 |
Bridge bridgeConfigView `json:"bridge"` |
|
763c873…
|
lmata
|
17 |
Ergo ergoConfigView `json:"ergo"` |
|
763c873…
|
lmata
|
18 |
TLS tlsConfigView `json:"tls"` |
|
763c873…
|
lmata
|
19 |
LLM llmConfigView `json:"llm"` |
|
763c873…
|
lmata
|
20 |
Topology config.TopologyConfig `json:"topology"` |
|
763c873…
|
lmata
|
21 |
History config.ConfigHistoryConfig `json:"config_history"` |
|
763c873…
|
lmata
|
22 |
AgentPolicy config.AgentPolicyConfig `json:"agent_policy"` |
|
763c873…
|
lmata
|
23 |
Logging config.LoggingConfig `json:"logging"` |
|
17e2c1d…
|
lmata
|
24 |
} |
|
17e2c1d…
|
lmata
|
25 |
|
|
17e2c1d…
|
lmata
|
26 |
type bridgeConfigView struct { |
|
17e2c1d…
|
lmata
|
27 |
Enabled bool `json:"enabled"` |
|
17e2c1d…
|
lmata
|
28 |
Nick string `json:"nick"` |
|
17e2c1d…
|
lmata
|
29 |
Channels []string `json:"channels"` |
|
17e2c1d…
|
lmata
|
30 |
BufferSize int `json:"buffer_size"` |
|
17e2c1d…
|
lmata
|
31 |
WebUserTTLMinutes int `json:"web_user_ttl_minutes"` |
|
17e2c1d…
|
lmata
|
32 |
// Password intentionally omitted — use PUT with non-empty value to change |
|
17e2c1d…
|
lmata
|
33 |
} |
|
17e2c1d…
|
lmata
|
34 |
|
|
17e2c1d…
|
lmata
|
35 |
type ergoConfigView struct { |
|
17e2c1d…
|
lmata
|
36 |
External bool `json:"external"` |
|
17e2c1d…
|
lmata
|
37 |
DataDir string `json:"data_dir"` |
|
17e2c1d…
|
lmata
|
38 |
NetworkName string `json:"network_name"` |
|
17e2c1d…
|
lmata
|
39 |
ServerName string `json:"server_name"` |
|
17e2c1d…
|
lmata
|
40 |
IRCAddr string `json:"irc_addr"` |
|
17e2c1d…
|
lmata
|
41 |
// APIAddr and APIToken omitted (internal/secret) |
|
17e2c1d…
|
lmata
|
42 |
} |
|
17e2c1d…
|
lmata
|
43 |
|
|
17e2c1d…
|
lmata
|
44 |
type tlsConfigView struct { |
|
17e2c1d…
|
lmata
|
45 |
Domain string `json:"domain"` |
|
17e2c1d…
|
lmata
|
46 |
Email string `json:"email"` |
|
17e2c1d…
|
lmata
|
47 |
AllowInsecure bool `json:"allow_insecure"` |
|
17e2c1d…
|
lmata
|
48 |
} |
|
17e2c1d…
|
lmata
|
49 |
|
|
17e2c1d…
|
lmata
|
50 |
type llmConfigView struct { |
|
17e2c1d…
|
lmata
|
51 |
Backends []llmBackendView `json:"backends"` |
|
17e2c1d…
|
lmata
|
52 |
} |
|
17e2c1d…
|
lmata
|
53 |
|
|
17e2c1d…
|
lmata
|
54 |
type llmBackendView struct { |
|
17e2c1d…
|
lmata
|
55 |
Name string `json:"name"` |
|
17e2c1d…
|
lmata
|
56 |
Backend string `json:"backend"` |
|
17e2c1d…
|
lmata
|
57 |
BaseURL string `json:"base_url,omitempty"` |
|
17e2c1d…
|
lmata
|
58 |
Model string `json:"model,omitempty"` |
|
17e2c1d…
|
lmata
|
59 |
Region string `json:"region,omitempty"` |
|
17e2c1d…
|
lmata
|
60 |
Allow []string `json:"allow,omitempty"` |
|
17e2c1d…
|
lmata
|
61 |
Block []string `json:"block,omitempty"` |
|
17e2c1d…
|
lmata
|
62 |
Default bool `json:"default,omitempty"` |
|
17e2c1d…
|
lmata
|
63 |
// APIKey / AWSKeyID / AWSSecretKey omitted — blank = no change on PUT |
|
17e2c1d…
|
lmata
|
64 |
} |
|
17e2c1d…
|
lmata
|
65 |
|
|
17e2c1d…
|
lmata
|
66 |
func configToView(cfg config.Config) configView { |
|
17e2c1d…
|
lmata
|
67 |
backends := make([]llmBackendView, len(cfg.LLM.Backends)) |
|
17e2c1d…
|
lmata
|
68 |
for i, b := range cfg.LLM.Backends { |
|
17e2c1d…
|
lmata
|
69 |
backends[i] = llmBackendView{ |
|
17e2c1d…
|
lmata
|
70 |
Name: b.Name, |
|
17e2c1d…
|
lmata
|
71 |
Backend: b.Backend, |
|
17e2c1d…
|
lmata
|
72 |
BaseURL: b.BaseURL, |
|
17e2c1d…
|
lmata
|
73 |
Model: b.Model, |
|
17e2c1d…
|
lmata
|
74 |
Region: b.Region, |
|
17e2c1d…
|
lmata
|
75 |
Allow: b.Allow, |
|
17e2c1d…
|
lmata
|
76 |
Block: b.Block, |
|
17e2c1d…
|
lmata
|
77 |
Default: b.Default, |
|
17e2c1d…
|
lmata
|
78 |
} |
|
17e2c1d…
|
lmata
|
79 |
} |
|
17e2c1d…
|
lmata
|
80 |
return configView{ |
|
17e2c1d…
|
lmata
|
81 |
APIAddr: cfg.APIAddr, |
|
17e2c1d…
|
lmata
|
82 |
MCPAddr: cfg.MCPAddr, |
|
17e2c1d…
|
lmata
|
83 |
Bridge: bridgeConfigView{ |
|
17e2c1d…
|
lmata
|
84 |
Enabled: cfg.Bridge.Enabled, |
|
17e2c1d…
|
lmata
|
85 |
Nick: cfg.Bridge.Nick, |
|
17e2c1d…
|
lmata
|
86 |
Channels: cfg.Bridge.Channels, |
|
17e2c1d…
|
lmata
|
87 |
BufferSize: cfg.Bridge.BufferSize, |
|
17e2c1d…
|
lmata
|
88 |
WebUserTTLMinutes: cfg.Bridge.WebUserTTLMinutes, |
|
17e2c1d…
|
lmata
|
89 |
}, |
|
17e2c1d…
|
lmata
|
90 |
Ergo: ergoConfigView{ |
|
17e2c1d…
|
lmata
|
91 |
External: cfg.Ergo.External, |
|
17e2c1d…
|
lmata
|
92 |
DataDir: cfg.Ergo.DataDir, |
|
17e2c1d…
|
lmata
|
93 |
NetworkName: cfg.Ergo.NetworkName, |
|
17e2c1d…
|
lmata
|
94 |
ServerName: cfg.Ergo.ServerName, |
|
17e2c1d…
|
lmata
|
95 |
IRCAddr: cfg.Ergo.IRCAddr, |
|
17e2c1d…
|
lmata
|
96 |
}, |
|
17e2c1d…
|
lmata
|
97 |
TLS: tlsConfigView{ |
|
17e2c1d…
|
lmata
|
98 |
Domain: cfg.TLS.Domain, |
|
17e2c1d…
|
lmata
|
99 |
Email: cfg.TLS.Email, |
|
17e2c1d…
|
lmata
|
100 |
AllowInsecure: cfg.TLS.AllowInsecure, |
|
17e2c1d…
|
lmata
|
101 |
}, |
|
763c873…
|
lmata
|
102 |
LLM: llmConfigView{Backends: backends}, |
|
763c873…
|
lmata
|
103 |
Topology: cfg.Topology, |
|
763c873…
|
lmata
|
104 |
History: cfg.History, |
|
763c873…
|
lmata
|
105 |
AgentPolicy: cfg.AgentPolicy, |
|
763c873…
|
lmata
|
106 |
Logging: cfg.Logging, |
|
17e2c1d…
|
lmata
|
107 |
} |
|
17e2c1d…
|
lmata
|
108 |
} |
|
17e2c1d…
|
lmata
|
109 |
|
|
17e2c1d…
|
lmata
|
110 |
// handleGetConfig handles GET /v1/config. |
|
17e2c1d…
|
lmata
|
111 |
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) { |
|
17e2c1d…
|
lmata
|
112 |
cfg := s.cfgStore.Get() |
|
17e2c1d…
|
lmata
|
113 |
writeJSON(w, http.StatusOK, configToView(cfg)) |
|
17e2c1d…
|
lmata
|
114 |
} |
|
17e2c1d…
|
lmata
|
115 |
|
|
17e2c1d…
|
lmata
|
116 |
// configUpdateRequest is the body accepted by PUT /v1/config. |
|
17e2c1d…
|
lmata
|
117 |
// Only the mutable, hot-reloadable sections. Restart-required fields (ergo IRC |
|
17e2c1d…
|
lmata
|
118 |
// addr, TLS domain, api_addr) are accepted but flagged in the response. |
|
17e2c1d…
|
lmata
|
119 |
type configUpdateRequest struct { |
|
763c873…
|
lmata
|
120 |
Bridge *bridgeConfigUpdate `json:"bridge,omitempty"` |
|
763c873…
|
lmata
|
121 |
Topology *config.TopologyConfig `json:"topology,omitempty"` |
|
763c873…
|
lmata
|
122 |
History *config.ConfigHistoryConfig `json:"config_history,omitempty"` |
|
763c873…
|
lmata
|
123 |
LLM *llmConfigUpdate `json:"llm,omitempty"` |
|
763c873…
|
lmata
|
124 |
AgentPolicy *config.AgentPolicyConfig `json:"agent_policy,omitempty"` |
|
763c873…
|
lmata
|
125 |
Logging *config.LoggingConfig `json:"logging,omitempty"` |
|
763c873…
|
lmata
|
126 |
Ergo *ergoConfigUpdate `json:"ergo,omitempty"` |
|
763c873…
|
lmata
|
127 |
TLS *tlsConfigUpdate `json:"tls,omitempty"` |
|
17e2c1d…
|
lmata
|
128 |
// These fields trigger a restart_required notice but are still persisted. |
|
17e2c1d…
|
lmata
|
129 |
APIAddr *string `json:"api_addr,omitempty"` |
|
17e2c1d…
|
lmata
|
130 |
MCPAddr *string `json:"mcp_addr,omitempty"` |
|
763c873…
|
lmata
|
131 |
} |
|
763c873…
|
lmata
|
132 |
|
|
763c873…
|
lmata
|
133 |
type ergoConfigUpdate struct { |
|
763c873…
|
lmata
|
134 |
NetworkName *string `json:"network_name,omitempty"` |
|
763c873…
|
lmata
|
135 |
ServerName *string `json:"server_name,omitempty"` |
|
763c873…
|
lmata
|
136 |
IRCAddr *string `json:"irc_addr,omitempty"` |
|
763c873…
|
lmata
|
137 |
External *bool `json:"external,omitempty"` |
|
763c873…
|
lmata
|
138 |
} |
|
763c873…
|
lmata
|
139 |
|
|
763c873…
|
lmata
|
140 |
type tlsConfigUpdate struct { |
|
763c873…
|
lmata
|
141 |
Domain *string `json:"domain,omitempty"` |
|
763c873…
|
lmata
|
142 |
Email *string `json:"email,omitempty"` |
|
763c873…
|
lmata
|
143 |
AllowInsecure *bool `json:"allow_insecure,omitempty"` |
|
17e2c1d…
|
lmata
|
144 |
} |
|
17e2c1d…
|
lmata
|
145 |
|
|
17e2c1d…
|
lmata
|
146 |
type bridgeConfigUpdate struct { |
|
17e2c1d…
|
lmata
|
147 |
Enabled *bool `json:"enabled,omitempty"` |
|
17e2c1d…
|
lmata
|
148 |
Nick *string `json:"nick,omitempty"` |
|
17e2c1d…
|
lmata
|
149 |
Channels []string `json:"channels,omitempty"` |
|
17e2c1d…
|
lmata
|
150 |
BufferSize *int `json:"buffer_size,omitempty"` |
|
17e2c1d…
|
lmata
|
151 |
WebUserTTLMinutes *int `json:"web_user_ttl_minutes,omitempty"` |
|
17e2c1d…
|
lmata
|
152 |
Password *string `json:"password,omitempty"` // blank = no change |
|
17e2c1d…
|
lmata
|
153 |
} |
|
17e2c1d…
|
lmata
|
154 |
|
|
17e2c1d…
|
lmata
|
155 |
type llmConfigUpdate struct { |
|
17e2c1d…
|
lmata
|
156 |
Backends []config.LLMBackendConfig `json:"backends"` |
|
17e2c1d…
|
lmata
|
157 |
} |
|
17e2c1d…
|
lmata
|
158 |
|
|
17e2c1d…
|
lmata
|
159 |
type configUpdateResponse struct { |
|
17e2c1d…
|
lmata
|
160 |
Saved bool `json:"saved"` |
|
17e2c1d…
|
lmata
|
161 |
RestartRequired []string `json:"restart_required,omitempty"` |
|
17e2c1d…
|
lmata
|
162 |
} |
|
17e2c1d…
|
lmata
|
163 |
|
|
17e2c1d…
|
lmata
|
164 |
// handlePutConfig handles PUT /v1/config. |
|
17e2c1d…
|
lmata
|
165 |
func (s *Server) handlePutConfig(w http.ResponseWriter, r *http.Request) { |
|
17e2c1d…
|
lmata
|
166 |
var req configUpdateRequest |
|
17e2c1d…
|
lmata
|
167 |
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
|
17e2c1d…
|
lmata
|
168 |
writeError(w, http.StatusBadRequest, "invalid request body") |
|
17e2c1d…
|
lmata
|
169 |
return |
|
17e2c1d…
|
lmata
|
170 |
} |
|
17e2c1d…
|
lmata
|
171 |
|
|
17e2c1d…
|
lmata
|
172 |
next := s.cfgStore.Get() |
|
17e2c1d…
|
lmata
|
173 |
var restartRequired []string |
|
17e2c1d…
|
lmata
|
174 |
|
|
17e2c1d…
|
lmata
|
175 |
if req.Bridge != nil { |
|
17e2c1d…
|
lmata
|
176 |
b := req.Bridge |
|
17e2c1d…
|
lmata
|
177 |
if b.Enabled != nil { |
|
17e2c1d…
|
lmata
|
178 |
next.Bridge.Enabled = *b.Enabled |
|
17e2c1d…
|
lmata
|
179 |
} |
|
17e2c1d…
|
lmata
|
180 |
if b.Nick != nil { |
|
17e2c1d…
|
lmata
|
181 |
next.Bridge.Nick = *b.Nick |
|
17e2c1d…
|
lmata
|
182 |
restartRequired = appendUniq(restartRequired, "bridge.nick") |
|
17e2c1d…
|
lmata
|
183 |
} |
|
17e2c1d…
|
lmata
|
184 |
if b.Channels != nil { |
|
17e2c1d…
|
lmata
|
185 |
next.Bridge.Channels = b.Channels |
|
17e2c1d…
|
lmata
|
186 |
} |
|
17e2c1d…
|
lmata
|
187 |
if b.BufferSize != nil { |
|
17e2c1d…
|
lmata
|
188 |
next.Bridge.BufferSize = *b.BufferSize |
|
17e2c1d…
|
lmata
|
189 |
} |
|
17e2c1d…
|
lmata
|
190 |
if b.WebUserTTLMinutes != nil { |
|
17e2c1d…
|
lmata
|
191 |
next.Bridge.WebUserTTLMinutes = *b.WebUserTTLMinutes |
|
17e2c1d…
|
lmata
|
192 |
} |
|
17e2c1d…
|
lmata
|
193 |
if b.Password != nil && *b.Password != "" { |
|
17e2c1d…
|
lmata
|
194 |
next.Bridge.Password = *b.Password |
|
17e2c1d…
|
lmata
|
195 |
restartRequired = appendUniq(restartRequired, "bridge.password") |
|
17e2c1d…
|
lmata
|
196 |
} |
|
17e2c1d…
|
lmata
|
197 |
} |
|
17e2c1d…
|
lmata
|
198 |
|
|
17e2c1d…
|
lmata
|
199 |
if req.Topology != nil { |
|
17e2c1d…
|
lmata
|
200 |
next.Topology = *req.Topology |
|
17e2c1d…
|
lmata
|
201 |
} |
|
17e2c1d…
|
lmata
|
202 |
|
|
17e2c1d…
|
lmata
|
203 |
if req.History != nil { |
|
17e2c1d…
|
lmata
|
204 |
if req.History.Keep > 0 { |
|
17e2c1d…
|
lmata
|
205 |
next.History.Keep = req.History.Keep |
|
17e2c1d…
|
lmata
|
206 |
} |
|
17e2c1d…
|
lmata
|
207 |
if req.History.Dir != "" { |
|
17e2c1d…
|
lmata
|
208 |
next.History.Dir = req.History.Dir |
|
17e2c1d…
|
lmata
|
209 |
} |
|
17e2c1d…
|
lmata
|
210 |
} |
|
17e2c1d…
|
lmata
|
211 |
|
|
17e2c1d…
|
lmata
|
212 |
if req.LLM != nil { |
|
17e2c1d…
|
lmata
|
213 |
next.LLM.Backends = req.LLM.Backends |
|
763c873…
|
lmata
|
214 |
} |
|
763c873…
|
lmata
|
215 |
|
|
763c873…
|
lmata
|
216 |
if req.AgentPolicy != nil { |
|
763c873…
|
lmata
|
217 |
next.AgentPolicy = *req.AgentPolicy |
|
763c873…
|
lmata
|
218 |
} |
|
763c873…
|
lmata
|
219 |
|
|
763c873…
|
lmata
|
220 |
if req.Logging != nil { |
|
763c873…
|
lmata
|
221 |
next.Logging = *req.Logging |
|
763c873…
|
lmata
|
222 |
} |
|
763c873…
|
lmata
|
223 |
|
|
763c873…
|
lmata
|
224 |
if req.Ergo != nil { |
|
763c873…
|
lmata
|
225 |
e := req.Ergo |
|
763c873…
|
lmata
|
226 |
if e.NetworkName != nil { |
|
763c873…
|
lmata
|
227 |
next.Ergo.NetworkName = *e.NetworkName |
|
763c873…
|
lmata
|
228 |
restartRequired = appendUniq(restartRequired, "ergo.network_name") |
|
763c873…
|
lmata
|
229 |
} |
|
763c873…
|
lmata
|
230 |
if e.ServerName != nil { |
|
763c873…
|
lmata
|
231 |
next.Ergo.ServerName = *e.ServerName |
|
763c873…
|
lmata
|
232 |
restartRequired = appendUniq(restartRequired, "ergo.server_name") |
|
763c873…
|
lmata
|
233 |
} |
|
763c873…
|
lmata
|
234 |
if e.IRCAddr != nil { |
|
763c873…
|
lmata
|
235 |
next.Ergo.IRCAddr = *e.IRCAddr |
|
763c873…
|
lmata
|
236 |
restartRequired = appendUniq(restartRequired, "ergo.irc_addr") |
|
763c873…
|
lmata
|
237 |
} |
|
763c873…
|
lmata
|
238 |
if e.External != nil { |
|
763c873…
|
lmata
|
239 |
next.Ergo.External = *e.External |
|
763c873…
|
lmata
|
240 |
restartRequired = appendUniq(restartRequired, "ergo.external") |
|
763c873…
|
lmata
|
241 |
} |
|
763c873…
|
lmata
|
242 |
} |
|
763c873…
|
lmata
|
243 |
|
|
763c873…
|
lmata
|
244 |
if req.TLS != nil { |
|
763c873…
|
lmata
|
245 |
t := req.TLS |
|
763c873…
|
lmata
|
246 |
if t.Domain != nil { |
|
763c873…
|
lmata
|
247 |
next.TLS.Domain = *t.Domain |
|
763c873…
|
lmata
|
248 |
restartRequired = appendUniq(restartRequired, "tls.domain") |
|
763c873…
|
lmata
|
249 |
} |
|
763c873…
|
lmata
|
250 |
if t.Email != nil { |
|
763c873…
|
lmata
|
251 |
next.TLS.Email = *t.Email |
|
763c873…
|
lmata
|
252 |
} |
|
763c873…
|
lmata
|
253 |
if t.AllowInsecure != nil { |
|
763c873…
|
lmata
|
254 |
next.TLS.AllowInsecure = *t.AllowInsecure |
|
763c873…
|
lmata
|
255 |
} |
|
17e2c1d…
|
lmata
|
256 |
} |
|
17e2c1d…
|
lmata
|
257 |
|
|
17e2c1d…
|
lmata
|
258 |
if req.APIAddr != nil && *req.APIAddr != "" { |
|
17e2c1d…
|
lmata
|
259 |
next.APIAddr = *req.APIAddr |
|
17e2c1d…
|
lmata
|
260 |
restartRequired = appendUniq(restartRequired, "api_addr") |
|
17e2c1d…
|
lmata
|
261 |
} |
|
17e2c1d…
|
lmata
|
262 |
if req.MCPAddr != nil && *req.MCPAddr != "" { |
|
17e2c1d…
|
lmata
|
263 |
next.MCPAddr = *req.MCPAddr |
|
17e2c1d…
|
lmata
|
264 |
restartRequired = appendUniq(restartRequired, "mcp_addr") |
|
17e2c1d…
|
lmata
|
265 |
} |
|
17e2c1d…
|
lmata
|
266 |
|
|
17e2c1d…
|
lmata
|
267 |
if err := s.cfgStore.Save(next); err != nil { |
|
17e2c1d…
|
lmata
|
268 |
s.log.Error("config save failed", "err", err) |
|
17e2c1d…
|
lmata
|
269 |
writeError(w, http.StatusInternalServerError, "failed to save config") |
|
17e2c1d…
|
lmata
|
270 |
return |
|
17e2c1d…
|
lmata
|
271 |
} |
|
17e2c1d…
|
lmata
|
272 |
|
|
17e2c1d…
|
lmata
|
273 |
writeJSON(w, http.StatusOK, configUpdateResponse{ |
|
17e2c1d…
|
lmata
|
274 |
Saved: true, |
|
17e2c1d…
|
lmata
|
275 |
RestartRequired: restartRequired, |
|
17e2c1d…
|
lmata
|
276 |
}) |
|
17e2c1d…
|
lmata
|
277 |
} |
|
17e2c1d…
|
lmata
|
278 |
|
|
17e2c1d…
|
lmata
|
279 |
// handleGetConfigHistory handles GET /v1/config/history. |
|
17e2c1d…
|
lmata
|
280 |
func (s *Server) handleGetConfigHistory(w http.ResponseWriter, r *http.Request) { |
|
17e2c1d…
|
lmata
|
281 |
entries, err := s.cfgStore.ListHistory() |
|
17e2c1d…
|
lmata
|
282 |
if err != nil { |
|
17e2c1d…
|
lmata
|
283 |
s.log.Error("list config history", "err", err) |
|
17e2c1d…
|
lmata
|
284 |
writeError(w, http.StatusInternalServerError, "failed to list history") |
|
17e2c1d…
|
lmata
|
285 |
return |
|
17e2c1d…
|
lmata
|
286 |
} |
|
17e2c1d…
|
lmata
|
287 |
writeJSON(w, http.StatusOK, map[string]any{"entries": entries}) |
|
17e2c1d…
|
lmata
|
288 |
} |
|
17e2c1d…
|
lmata
|
289 |
|
|
17e2c1d…
|
lmata
|
290 |
// handleGetConfigHistoryEntry handles GET /v1/config/history/{filename}. |
|
17e2c1d…
|
lmata
|
291 |
func (s *Server) handleGetConfigHistoryEntry(w http.ResponseWriter, r *http.Request) { |
|
17e2c1d…
|
lmata
|
292 |
filename := filepath.Base(r.PathValue("filename")) |
|
17e2c1d…
|
lmata
|
293 |
data, err := s.cfgStore.ReadHistoryFile(filename) |
|
17e2c1d…
|
lmata
|
294 |
if err != nil { |
|
17e2c1d…
|
lmata
|
295 |
writeError(w, http.StatusNotFound, "snapshot not found") |
|
17e2c1d…
|
lmata
|
296 |
return |
|
17e2c1d…
|
lmata
|
297 |
} |
|
17e2c1d…
|
lmata
|
298 |
// Parse snapshot to return as JSON (same masked view as GET /v1/config). |
|
17e2c1d…
|
lmata
|
299 |
var snapped config.Config |
|
17e2c1d…
|
lmata
|
300 |
snapped.Defaults() |
|
17e2c1d…
|
lmata
|
301 |
if err := snapped.LoadFromBytes(data); err != nil { |
|
17e2c1d…
|
lmata
|
302 |
writeError(w, http.StatusInternalServerError, "failed to parse snapshot") |
|
17e2c1d…
|
lmata
|
303 |
return |
|
17e2c1d…
|
lmata
|
304 |
} |
|
17e2c1d…
|
lmata
|
305 |
writeJSON(w, http.StatusOK, configToView(snapped)) |
|
17e2c1d…
|
lmata
|
306 |
} |
|
17e2c1d…
|
lmata
|
307 |
|
|
17e2c1d…
|
lmata
|
308 |
func appendUniq(s []string, v string) []string { |
|
17e2c1d…
|
lmata
|
309 |
for _, x := range s { |
|
17e2c1d…
|
lmata
|
310 |
if x == v { |
|
17e2c1d…
|
lmata
|
311 |
return s |
|
17e2c1d…
|
lmata
|
312 |
} |
|
17e2c1d…
|
lmata
|
313 |
} |
|
17e2c1d…
|
lmata
|
314 |
return append(s, v) |
|
17e2c1d…
|
lmata
|
315 |
} |