package dnf90 import ( "encoding/json" "io" "net/http" "strings" appkit "longheng.io/server/internal/platform/app" httpx "longheng.io/server/internal/platform/httpx" "longheng.io/server/internal/services/dnfbridge" ) // companionPanelAdminPath is the local-only endpoint used by the GM tool's // "支援兵属性" page. It is intentionally unauthenticated: the admin mux binds // to loopback, like the existing /local endpoints, and the page is served by // the local GM tool. const companionPanelAdminPath = "/local/companion-panel" func registerCompanionPanelAdmin(env *appkit.Env, bridge *dnfbridge.Service) { if env == nil || env.AdminMux == nil || bridge == nil { return } env.AdminMux.HandleFunc(companionPanelAdminPath, func(w http.ResponseWriter, r *http.Request) { applyCompanionPanelCORS(w) if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } switch r.Method { case http.MethodGet: characterID := strings.TrimSpace(r.URL.Query().Get("character_id")) if characterID == "" { characterID = strings.TrimSpace(r.URL.Query().Get("characterId")) } values, found, err := bridge.CompanionPanelValues(r.Context(), characterID) if err != nil { httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } httpx.WriteJSON(w, http.StatusOK, map[string]any{ "character_id": characterID, "found": found, "values": values, }) case http.MethodPost: var body struct { CharacterID string `json:"character_id"` Values map[string]string `json:"values"` } decoder := json.NewDecoder(io.LimitReader(r.Body, 1<<20)) if err := decoder.Decode(&body); err != nil { httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } if strings.TrimSpace(body.CharacterID) == "" { httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{ "error": "character_id is required", }) return } if err := bridge.SaveCompanionPanelValues(r.Context(), body.CharacterID, body.Values); err != nil { httpx.WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } values, found, _ := bridge.CompanionPanelValues(r.Context(), body.CharacterID) httpx.WriteJSON(w, http.StatusOK, map[string]any{ "ok": true, "character_id": body.CharacterID, "found": found, "values": values, }) default: w.Header().Set("Allow", "GET, POST, OPTIONS") httpx.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{ "error": "method not allowed", }) } }) // Base-estimate generation: PVF growth + worn equipment + weapon forging. // Enchant cards, amplification, reinforcement, quality, sets, titles, auras // and pets are intentionally not modelled; the GM page marks the result as // an estimate and lets the operator correct it. env.AdminMux.HandleFunc(companionPanelGeneratePath, func(w http.ResponseWriter, r *http.Request) { applyCompanionPanelCORS(w) if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } if r.Method != http.MethodPost { w.Header().Set("Allow", "POST, OPTIONS") httpx.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{ "error": "method not allowed", }) return } var body struct { CharacterID string `json:"character_id"` } if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil { httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } values, err := bridge.GenerateCompanionPanelSnapshot(r.Context(), body.CharacterID) if err != nil { httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } httpx.WriteJSON(w, http.StatusOK, map[string]any{ "ok": true, "character_id": body.CharacterID, "estimate": true, "values": values, }) }) } const companionPanelGeneratePath = "/local/companion-panel/generate" func applyCompanionPanelCORS(w http.ResponseWriter) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Admin-Token") w.Header().Set("Access-Control-Allow-Private-Network", "true") }