Ports main.py's _combat_session_delta / _combat_merge_into_lifetime (incl. the documented "offense/defense use latest, additively" quirk) and the combat_stats handler (session delta -> DB-backed lifetime merge -> delete-then-insert of combat_stats + combat_stats_sessions). Read handlers gain the live combat overlay (union live + DB), like Python. Validation: - combat.go `combat-merge` CLI folds snapshots through the accumulator; diffed against the Python functions on identical input -> byte-IDENTICAL. - combat_test.go golden test runs in the build (go test now part of the tracker Dockerfile). - Live: 40 combat lifetime rows + 40 session snapshots + rare_events flowing in dereth_go via the shadow consumer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
114 lines
3.4 KiB
Go
114 lines
3.4 KiB
Go
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 ""
|
|
}
|