package main import ( "net/http" "sort" ) // GET /character-stats/{name} — latest full stats. Phase 1 reads the DB // (character_stats is authoritative); the live_character_stats overlay is an // ingest-only freshness layer we don't have yet. (main.py:4137) func (s *Server) handleCharacterStats(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") // Live overlay first (ingest mode), like Python's live_character_stats check. if s.ingestor != nil { if v, ok := s.ingestor.getCharacterStats(name); ok { writeJSON(w, http.StatusOK, v) return } } ctx, cancel := reqCtx(r) defer cancel() row, err := queryRowAsMap(ctx, s.pool, `SELECT * FROM character_stats WHERE character_name = $1`, name) if err != nil { s.dbErr(w, "character-stats", err) return } if row == nil { writeJSON(w, http.StatusNotFound, map[string]any{"error": "No stats available for this character"}) return } // Merge stats_data JSONB up to the top level, matching the frontend contract. sd := asJSONMap(row["stats_data"]) delete(row, "stats_data") formatTimes([]map[string]any{row}, "timestamp") for k, v := range sd { row[k] = v } writeJSON(w, http.StatusOK, row) } // GET /combat-stats/{character_name} — lifetime combat blob. Phase 1: DB only, // so session is always null. (main.py:1819) func (s *Server) handleCombatStatsOne(w http.ResponseWriter, r *http.Request) { cn := r.PathValue("character_name") if s.ingestor != nil { if live, ok := s.ingestor.getCombatStats(cn); ok { writeJSON(w, http.StatusOK, map[string]any{ "character_name": cn, "session": live["session"], "lifetime": live["lifetime"], }) return } } ctx, cancel := reqCtx(r) defer cancel() row, err := queryRowAsMap(ctx, s.pool, `SELECT stats_data FROM combat_stats WHERE character_name = $1`, cn) if err != nil { s.dbErr(w, "combat-stats/one", err) return } if row == nil { writeJSON(w, http.StatusOK, map[string]any{"character_name": cn, "session": nil, "lifetime": nil}) return } writeJSON(w, http.StatusOK, map[string]any{ "character_name": cn, "session": nil, "lifetime": decodeJSONValue(row["stats_data"]), }) } // GET /combat-stats — all characters' lifetime combat blobs. Phase 1: DB only. (main.py:1850) func (s *Server) handleCombatStatsAll(w http.ResponseWriter, r *http.Request) { ctx, cancel := reqCtx(r) defer cancel() results := make([]map[string]any, 0) seen := map[string]bool{} if s.ingestor != nil { // live overlay first, like Python for char, live := range s.ingestor.allCombatStats() { seen[char] = true results = append(results, map[string]any{ "character_name": char, "session": live["session"], "lifetime": live["lifetime"], }) } } rows, err := queryRowsAsMaps(ctx, s.pool, `SELECT character_name, stats_data FROM combat_stats`) if err != nil { s.dbErr(w, "combat-stats/all", err) return } for _, row := range rows { if seen[toStr(row["character_name"])] { continue } results = append(results, map[string]any{ "character_name": row["character_name"], "session": nil, "lifetime": decodeJSONValue(row["stats_data"]), }) } sort.Slice(results, func(i, j int) bool { return toStr(results[i]["character_name"]) < toStr(results[j]["character_name"]) }) writeJSON(w, http.StatusOK, map[string]any{"stats": results}) } func toStr(v any) string { if s, ok := v.(string); ok { return s } return "" }