fix: 注册 PUT /api/manager/settings 使设置保存生效

server.go NewServeMux 按方法分发到 handleSettingsPut(原仅注册 GET,PUT 静默返回旧值)

新增 TestSettingsPutRoundTrip 回归测试;gofmt 清理历史格式问题
This commit is contained in:
2026-08-17 12:00:34 +08:00
parent 1cd0ab8ad1
commit 5237e3b594
5 changed files with 66 additions and 22 deletions

View File

@ -257,8 +257,6 @@ func (h *Handler) handleProfile(w http.ResponseWriter, r *http.Request) {
// ---- helpers ----
// tailFile returns the last maxBytes bytes of a file.
func tailFile(path string, maxBytes int64) (string, error) {
info, err := os.Stat(path)
@ -280,4 +278,3 @@ func tailFile(path string, maxBytes int64) (string, error) {
}
return string(buf), nil
}

View File

@ -61,7 +61,13 @@ func NewServeMux(h *Handler) (http.Handler, error) {
// API routes (basic auth).
mux.HandleFunc(apiPrefix+"/status", auth(h.handleStatus))
mux.HandleFunc(apiPrefix+"/canvas", auth(h.handleCanvasSave))
mux.HandleFunc(apiPrefix+"/settings", auth(h.handleSettingsGet))
mux.HandleFunc(apiPrefix+"/settings", auth(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPut {
h.handleSettingsPut(w, r)
return
}
h.handleSettingsGet(w, r)
}))
mux.HandleFunc(apiPrefix+"/binary/status", auth(h.handleBinaryStatus))
mux.HandleFunc(apiPrefix+"/binary/install", auth(h.handleBinaryInstall))
@ -135,10 +141,10 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
settings, _ := h.Store.Settings()
type profileStatus struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
Status process.Status `json:"process"`
HasProc bool `json:"hasProcess"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Status process.Status `json:"process"`
HasProc bool `json:"hasProcess"`
Forwards []store.Forward `json:"forwards"`
}
@ -155,7 +161,7 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
})
}
// Local status: each local plus its forwarding targets and whether each
// Local status: each local plus its forwarding targets and whether each
// target's worker is healthy (running).
type localTargetStatus struct {
Remote string `json:"remote"`
@ -163,8 +169,8 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
WorkerState string `json:"workerState"`
}
type localStatus struct {
Local store.Local `json:"local"`
Targets []localTargetStatus `json:"targets"`
Local store.Local `json:"local"`
Targets []localTargetStatus `json:"targets"`
}
localStatuses := make([]localStatus, 0, len(locals))
@ -186,13 +192,13 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
}
resp := map[string]any{
"version": "0.1.0",
"workDir": h.WorkDir,
"settings": settings,
"services": locals,
"remotes": remotes,
"binaryPath": binary,
"profiles": profiles,
"version": "0.1.0",
"workDir": h.WorkDir,
"settings": settings,
"services": locals,
"remotes": remotes,
"binaryPath": binary,
"profiles": profiles,
"localStatus": localStatuses,
}
writeJSON(w, http.StatusOK, resp)

View File

@ -97,3 +97,44 @@ func TestCanvasRoundTrip(t *testing.T) {
t.Fatalf("remotes after reload = %+v", got2.Remotes)
}
}
func TestSettingsPutRoundTrip(t *testing.T) {
_, ts := newTestHandler(t)
client := ts.Client()
// PUT updated settings
body := `{"autoStartProfiles":false,"restartOnExit":false,"restartIntervalSeconds":12}`
req, _ := http.NewRequest(http.MethodPut, ts.URL+"/api/manager/settings", bytes.NewBufferString(body))
req.SetBasicAuth("admin", "pw")
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("PUT settings status = %d, want 200", resp.StatusCode)
}
// Confirm response body reflects saved values
var saved store.Settings
if err := json.NewDecoder(resp.Body).Decode(&saved); err != nil {
t.Fatal(err)
}
if saved.AutoStartProfiles || saved.RestartOnExit || saved.RestartIntervalSeconds != 12 {
t.Fatalf("saved settings = %+v", saved)
}
// GET again to confirm persistence
req2, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/manager/settings", nil)
req2.SetBasicAuth("admin", "pw")
resp2, err2 := client.Do(req2)
if err2 != nil {
t.Fatal(err2)
}
defer resp2.Body.Close()
var got store.Settings
_ = json.NewDecoder(resp2.Body).Decode(&got)
if got.AutoStartProfiles || got.RestartOnExit || got.RestartIntervalSeconds != 12 {
t.Fatalf("settings after reload = %+v", got)
}
}

View File

@ -24,7 +24,7 @@ type Local struct {
type Remote struct {
Name string `json:"name"`
IP string `json:"ip"`
Port int `json:"port"` // port to connect to frps
Port int `json:"port"` // port to connect to frps
Token string `json:"token,omitempty"`
URL string `json:"url,omitempty"`
Enabled bool `json:"enabled"`
@ -32,7 +32,7 @@ type Remote struct {
// Link connects one local to one remote.
type Link struct {
ID int64 `json:"id,omitempty"`
ID int64 `json:"id,omitempty"`
Local string `json:"local"`
Remote string `json:"remote"`
RemotePort int `json:"remotePort"`

View File

@ -30,7 +30,7 @@
## 已知问题(审查发现,待修复)
1. **设置保存失效P0**`internal/httpapi/handlers.go` 定义了 `handleSettingsPut`,但 `server.go``NewServeMux` 只注册了 GET `/api/manager/settings`**未注册 PUT**。PUT 请求会落入 `handleSettingsGet` 静默返回旧值,SettingsView 保存生效。
1. ~~设置保存失效P0~~ **已修复**`server.go``NewServeMux` 现在对 `PUT /api/manager/settings` 分发到 `handleSettingsPut`,新增 `TestSettingsPutRoundTrip` 回归测试,前端 SettingsView 保存生效。
2. **dev proxy 端口不一致**`web/vite.config.ts` 的 proxy 指向 `127.0.0.1:17650`,与后端默认监听 `127.0.0.1:7500` 不符,前端 dev 联调需对齐。
3. **冗余依赖**`vue-router` 在 package.json 依赖中但未被使用App.vue 以视图 ref 手动切页,无路由)。
4. **README 笔误**:架构图写 `cmds/webui4frpc`,实际目录为 `cmd/webui4frpc`
@ -76,7 +76,7 @@
## 建议推进顺序
1. 修复「设置保存失效」——给 `PUT /api/manager/settings` 补注册(约 10 分钟
1. [x] 修复「设置保存失效」——给 `PUT /api/manager/settings` 补注册(server.go 方法分发 + httpapi 单测
2. 对齐 dev proxy 端口vite.config.ts → 7500
3. M1 高级传输参数store 加列 → render 输出 → 前端折叠表单 → 单测回归)
4. M5 补一键构建脚本 / CI