diff --git a/agent/agent.go b/agent/agent.go index 4e11ccdc..97cea1a0 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -7,6 +7,7 @@ package agent import ( "context" + "encoding/json" "fmt" "log/slog" "net/http" @@ -548,7 +549,9 @@ func toFloat(v interface{}) float64 { case float32: return float64(x) case int: return float64(x) case int64: return float64(x) + case int32: return float64(x) case string: f, _ := strconv.ParseFloat(x, 64); return f + case json.Number: f, _ := x.Float64(); return f } return 0 } diff --git a/api/server.go b/api/server.go index 25a950e1..c79d716f 100644 --- a/api/server.go +++ b/api/server.go @@ -98,7 +98,14 @@ func corsMiddleware() gin.HandlerFunc { return func(c *gin.Context) { origin := c.Request.Header.Get("Origin") if allowAll { - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + if origin != "" { + // When credentials are needed, we must echo the specific origin + // instead of "*" (browsers reject Allow-Credentials with wildcard). + c.Writer.Header().Set("Access-Control-Allow-Origin", origin) + c.Writer.Header().Set("Vary", "Origin") + } else { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + } } else if origin != "" && allowed[origin] { c.Writer.Header().Set("Access-Control-Allow-Origin", origin) c.Writer.Header().Set("Vary", "Origin") diff --git a/api/strategy.go b/api/strategy.go index 2fc9c9fb..91637b15 100644 --- a/api/strategy.go +++ b/api/strategy.go @@ -56,8 +56,11 @@ func (s *Server) handlePublicStrategies(c *gin.Context) { // Only include config if config_visible is true if st.ConfigVisible { var config store.StrategyConfig - json.Unmarshal([]byte(st.Config), &config) - item["config"] = config + if err := json.Unmarshal([]byte(st.Config), &config); err != nil { + item["config_error"] = "invalid config format" + } else { + item["config"] = config + } } result = append(result, item) @@ -86,9 +89,9 @@ func (s *Server) handleGetStrategies(c *gin.Context) { result := make([]gin.H, 0, len(strategies)) for _, st := range strategies { var config store.StrategyConfig - json.Unmarshal([]byte(st.Config), &config) + configErr := json.Unmarshal([]byte(st.Config), &config) - result = append(result, gin.H{ + item := gin.H{ "id": st.ID, "name": st.Name, "description": st.Description, @@ -96,10 +99,15 @@ func (s *Server) handleGetStrategies(c *gin.Context) { "is_default": st.IsDefault, "is_public": st.IsPublic, "config_visible": st.ConfigVisible, - "config": config, "created_at": st.CreatedAt, "updated_at": st.UpdatedAt, - }) + } + if configErr != nil { + item["config_error"] = "invalid config format" + } else { + item["config"] = config + } + result = append(result, item) } c.JSON(http.StatusOK, gin.H{ @@ -124,18 +132,23 @@ func (s *Server) handleGetStrategy(c *gin.Context) { } var config store.StrategyConfig - json.Unmarshal([]byte(strategy.Config), &config) + configErr := json.Unmarshal([]byte(strategy.Config), &config) - c.JSON(http.StatusOK, gin.H{ + result := gin.H{ "id": strategy.ID, "name": strategy.Name, "description": strategy.Description, "is_active": strategy.IsActive, "is_default": strategy.IsDefault, - "config": config, "created_at": strategy.CreatedAt, "updated_at": strategy.UpdatedAt, - }) + } + if configErr != nil { + result["config_error"] = "invalid config format" + } else { + result["config"] = config + } + c.JSON(http.StatusOK, result) } // handleCreateStrategy Create strategy. @@ -383,18 +396,23 @@ func (s *Server) handleGetActiveStrategy(c *gin.Context) { } var config store.StrategyConfig - json.Unmarshal([]byte(strategy.Config), &config) + configErr := json.Unmarshal([]byte(strategy.Config), &config) - c.JSON(http.StatusOK, gin.H{ + result := gin.H{ "id": strategy.ID, "name": strategy.Name, "description": strategy.Description, "is_active": strategy.IsActive, "is_default": strategy.IsDefault, - "config": config, "created_at": strategy.CreatedAt, "updated_at": strategy.UpdatedAt, - }) + } + if configErr != nil { + result["config_error"] = "invalid config format" + } else { + result["config"] = config + } + c.JSON(http.StatusOK, result) } // handleGetDefaultStrategyConfig Get default strategy configuration template diff --git a/store/decision.go b/store/decision.go index 6926f326..e4c74c67 100644 --- a/store/decision.go +++ b/store/decision.go @@ -138,9 +138,10 @@ func (db *DecisionRecordDB) toRecord() *DecisionRecord { ErrorMessage: db.ErrorMessage, AIRequestDurationMs: db.AIRequestDurationMs, } - json.Unmarshal([]byte(db.CandidateCoins), &record.CandidateCoins) - json.Unmarshal([]byte(db.ExecutionLog), &record.ExecutionLog) - json.Unmarshal([]byte(db.Decisions), &record.Decisions) + // Best-effort unmarshal — empty/malformed JSON fields are left as zero values. + _ = json.Unmarshal([]byte(db.CandidateCoins), &record.CandidateCoins) + _ = json.Unmarshal([]byte(db.ExecutionLog), &record.ExecutionLog) + _ = json.Unmarshal([]byte(db.Decisions), &record.Decisions) return record }