From a0e2df615ce4b27d4e93b938eb0e20854bddc70d Mon Sep 17 00:00:00 2001 From: NanamiAdmin Date: Tue, 8 Sep 2026 22:28:49 +0800 Subject: [PATCH] feat: add `/api/server/getInfo` to handle frontend get server info. chore: rebuild `/api/server/getStatus` to the same logic as `getInfo`. --- README.md | 8 ++- handler/server.go | 97 +++++++++++++++++++++---- handler/server_test.go | 158 +++++++++++++++++++++++++++++++++++++++++ router.go | 1 + 4 files changed, 249 insertions(+), 15 deletions(-) create mode 100644 handler/server_test.go diff --git a/README.md b/README.md index 6430ebe..8913c3a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ nukumizu-backend/ │ └── variables.go # Software build metadata (name/version/developer) ├── handler/ │ ├── user.go # /api/user/login, /api/user/register -│ ├── server.go # /api/server/list, getStatus, exec +│ ├── server.go # /api/server/list, getInfo, getStatus, exec +│ ├── settings.go # /api/settings/get, set │ └── health.go # /health ├── database/ │ └── user.go # user.db (SQLite) user store @@ -276,8 +277,11 @@ Tokens idle for more than 1 hour are expired (cleaned every 10 minutes); any aut | `/api/user/login` | POST | None | Log in. Body `{username, password}`. Returns `{token, userID, username, level, registerDate}`. | | `/api/user/register` | POST | None | Register the first user. Body `{username, password}`. Only allowed while no user exists; otherwise `403`. Returns `{token, userID, username, level}`. | | `/api/server/list` | GET | bot / admin | List all monitored servers. | -| `/api/server/getStatus` | GET | bot / admin | Recent live status for a server. Query `?uuid=`. Returns `{uuid, report}` or `404`. | +| `/api/server/getInfo` | GET | admin | Static server info (mirrors the Bot's `/info`). Query `?uuid=` (or `all`). Returns `{success, : {uuid, name, info}}` — one entry per requested server. `404` for an unknown single uuid. | +| `/api/server/getStatus` | GET | admin | Live server status (mirrors the Bot's `/status`). Query `?uuid=` (or `all`). Returns `{success, : {uuid, name, online, report}}`; `report` is `null` when the node has not reported yet. `404` for an unknown single uuid. | | `/api/server/exec` | POST | bot / admin | Execute a command. Body `{uuid: [...], command}`. Dispatches a Komari task and polls until completion (or timeout). Returns `{taskID, results}`. | +| `/api/settings/get` | GET | admin | `?type=global\|bot_user_config\|bot_node_config` | Returns `{success, config}`, where `config` is the selected config file's content (same layout as the JSON file). | +| `/api/settings/set` | POST | admin | `?type=` + JSON body of partial updates, e.g. `{"system":{"debugMode":true}}` | Deep-merges the body into the selected config file, persists it, and reloads it in memory. Only the given keys change; arrays replace. | | `/health` | GET | None | Health check. Returns `{status, database}`. | | `/api/system/getLogs` | WebSocket | None | Streams logs. Sends the last 100 buffered entries, then live `{level, content, timestamp}` events. | diff --git a/handler/server.go b/handler/server.go index 9ca4ae8..4efd1b6 100644 --- a/handler/server.go +++ b/handler/server.go @@ -18,6 +18,38 @@ type ServerExecRequest struct { Command string `json:"command"` } +// ServerInfoValue is the per-server payload returned by GET /api/server/getInfo, +// mirroring the static server info the Bot prints for /info. +type ServerInfoValue struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Info *node.Info `json:"info"` +} + +// ServerStatusValue is the per-server payload returned by GET /api/server/getStatus, +// mirroring the live status the Bot prints for /status. Report is null when the +// node is known but has not delivered a status report yet. +type ServerStatusValue struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Online bool `json:"online"` + Report *node.Report `json:"report"` +} + +// collectTargetNodes resolves the uuid query parameter into the list of nodes +// the caller asked for. uuid "all" selects every tracked node; any other uuid +// selects that single node. A boolean reports whether the uuid was found. +func collectTargetNodes(tracker *node.Tracker, uuid string) ([]*node.Node, bool) { + if uuid == "all" { + return tracker.GetAllNodes(), true + } + n, exists := tracker.GetNode(uuid) + if !exists { + return nil, false + } + return []*node.Node{n}, true +} + // ServerListHandler handles GET /api/server/list. func ServerListHandler(w http.ResponseWriter, r *http.Request) { if !utils.Auth(w, r, "GET", "bot") { @@ -38,9 +70,11 @@ func ServerListHandler(w http.ResponseWriter, r *http.Request) { }) } -// ServerGetStatusHandler handles GET /api/server/getStatus?uuid=xxx. -func ServerGetStatusHandler(w http.ResponseWriter, r *http.Request) { - if !utils.Auth(w, r, "GET", "bot") { +// ServerGetInfoHandler handles GET /api/server/getInfo?uuid=xxx|all. +// Returns the static info of the requested server(s) as a uuid-keyed object, +// mirroring the data the Bot uses for /info. +func ServerGetInfoHandler(w http.ResponseWriter, r *http.Request) { + if !utils.Auth(w, r, "GET", "admin") { return } @@ -50,19 +84,56 @@ func ServerGetStatusHandler(w http.ResponseWriter, r *http.Request) { return } - // First try to get data from the local tracker. tracker := node.GetTracker() - if tracker != nil { - if n, exists := tracker.GetNode(uuid); exists && n.LatestReport != nil { - utils.SendSuccessResponse(w, "", map[string]interface{}{ - "uuid": uuid, - "report": n.LatestReport, - }) - return - } + if tracker == nil { + utils.SendErrorResponse(w, http.StatusInternalServerError, "node tracker not initialized") + return } - utils.SendErrorResponse(w, http.StatusNotFound, fmt.Sprintf("no recent data for uuid: %s", uuid)) + nodes, found := collectTargetNodes(tracker, uuid) + if !found { + utils.SendErrorResponse(w, http.StatusNotFound, fmt.Sprintf("server with uuid %s not found", uuid)) + return + } + + result := make(map[string]interface{}, len(nodes)) + for _, n := range nodes { + result[n.UUID] = ServerInfoValue{UUID: n.UUID, Name: n.Name, Info: n.Info} + } + utils.SendSuccessResponse(w, "", result) +} + +// ServerGetStatusHandler handles GET /api/server/getStatus?uuid=xxx|all. +// Returns the live status of the requested server(s) as a uuid-keyed object, +// mirroring the data the Bot uses for /status. +func ServerGetStatusHandler(w http.ResponseWriter, r *http.Request) { + if !utils.Auth(w, r, "GET", "admin") { + return + } + + uuid := r.URL.Query().Get("uuid") + if uuid == "" { + utils.SendErrorResponse(w, http.StatusBadRequest, "missing uuid parameter") + return + } + + tracker := node.GetTracker() + if tracker == nil { + utils.SendErrorResponse(w, http.StatusInternalServerError, "node tracker not initialized") + return + } + + nodes, found := collectTargetNodes(tracker, uuid) + if !found { + utils.SendErrorResponse(w, http.StatusNotFound, fmt.Sprintf("server with uuid %s not found", uuid)) + return + } + + result := make(map[string]interface{}, len(nodes)) + for _, n := range nodes { + result[n.UUID] = ServerStatusValue{UUID: n.UUID, Name: n.Name, Online: n.Online, Report: n.LatestReport} + } + utils.SendSuccessResponse(w, "", result) } // ServerExecHandler handles POST /api/server/exec. diff --git a/handler/server_test.go b/handler/server_test.go new file mode 100644 index 0000000..e5480ec --- /dev/null +++ b/handler/server_test.go @@ -0,0 +1,158 @@ +package handler + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "nukumizu-backend/internal/node" + "nukumizu-backend/utils" +) + +// adminGet builds an authenticated GET request for the given path using an +// admin token and a fresh X-Timestamp. +func adminGet(t *testing.T, path string) *http.Request { + t.Helper() + req := httptest.NewRequest(http.MethodGet, path, nil) + req.Header.Set("X-Token", "test-admin-token") + req.Header.Set("X-Timestamp", strconv.FormatInt(time.Now().Unix(), 10)) + return req +} + +// seedTracker initializes the global node tracker with two nodes, one online +// with a status report and one offline that has not reported yet. +func seedTracker(t *testing.T) { + t.Helper() + node.InitTracker() + + tracker := node.GetTracker() + tracker.UpdateNodeList(map[string]node.NodeListEntry{ + "u1": { + Name: "alpha", + Info: &node.Info{}, + }, + "u2": { + Name: "beta", + Info: &node.Info{}, + }, + }) + + report := node.Report{} + report.CPU.Usage = 12.5 + report.RAM.Total = 1024 + report.RAM.Used = 512 + tracker.UpdateStatus([]string{"u1"}, map[string]node.Report{"u1": report}) +} + +func setupAdminToken() { + utils.AddToken("test-admin-token", 1, "admin", "tester") +} + +func decodeResponse(t *testing.T, w *httptest.ResponseRecorder) map[string]json.RawMessage { + t.Helper() + var body map[string]json.RawMessage + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { + t.Fatalf("decode response: %v; body=%s", err, w.Body.String()) + } + return body +} + +func TestServerGetInfoAll(t *testing.T) { + setupAdminToken() + seedTracker(t) + + w := httptest.NewRecorder() + ServerGetInfoHandler(w, adminGet(t, "/api/server/getInfo?uuid=all")) + + if w.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) + } + + body := decodeResponse(t, w) + for _, uuid := range []string{"u1", "u2"} { + if _, ok := body[uuid]; !ok { + t.Errorf("response missing uuid %q: %s", uuid, w.Body.String()) + } + } + + var one struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Info *node.Info `json:"info"` + } + if err := json.Unmarshal(body["u1"], &one); err != nil { + t.Fatalf("decode u1: %v", err) + } + if one.UUID != "u1" || one.Name != "alpha" { + t.Errorf("u1 = %+v", one) + } + if one.Info == nil { + t.Error("expected static info present for u1") + } +} + +func TestServerGetInfoSingleAndMissing(t *testing.T) { + setupAdminToken() + seedTracker(t) + + // Single existing uuid: response is keyed by that uuid (uniform shape). + w := httptest.NewRecorder() + ServerGetInfoHandler(w, adminGet(t, "/api/server/getInfo?uuid=u1")) + if w.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) + } + body := decodeResponse(t, w) + if _, ok := body["u1"]; !ok { + t.Errorf("single-uuid response missing key u1: %s", w.Body.String()) + } + + // Unknown uuid yields 404. + w2 := httptest.NewRecorder() + ServerGetInfoHandler(w2, adminGet(t, "/api/server/getInfo?uuid=ghost")) + if w2.Code != http.StatusNotFound { + t.Errorf("missing uuid status = %d, want 404", w2.Code) + } +} + +func TestServerGetStatusAll(t *testing.T) { + setupAdminToken() + seedTracker(t) + + w := httptest.NewRecorder() + ServerGetStatusHandler(w, adminGet(t, "/api/server/getStatus?uuid=all")) + if w.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) + } + + body := decodeResponse(t, w) + + var online struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Online bool `json:"online"` + Report *node.Report `json:"report"` + } + if err := json.Unmarshal(body["u1"], &online); err != nil { + t.Fatalf("decode u1: %v", err) + } + if !online.Online || online.Report == nil { + t.Errorf("u1 should be online with a report: %+v", online) + } + + var offline struct { + Online bool `json:"online"` + Report *node.Report `json:"report"` + } + if err := json.Unmarshal(body["u2"], &offline); err != nil { + t.Fatalf("decode u2: %v", err) + } + if offline.Online { + t.Error("u2 should be offline") + } + if offline.Report != nil { + t.Errorf("u2 report should be null, got %+v", offline.Report) + } +} diff --git a/router.go b/router.go index c015d44..56b46e8 100644 --- a/router.go +++ b/router.go @@ -20,6 +20,7 @@ func SetupRouter() *http.ServeMux { // Server endpoints (authenticated). mux.HandleFunc("/api/server/list", handler.ServerListHandler) + mux.HandleFunc("/api/server/getInfo", handler.ServerGetInfoHandler) mux.HandleFunc("/api/server/getStatus", handler.ServerGetStatusHandler) mux.HandleFunc("/api/server/exec", handler.ServerExecHandler)