Adds the rest of the read-side endpoints to the Go tracker, all parity-checked
against the live Python service:
- DB reads: /stats/{c}, /portals, /spawns/heatmap, /server-health,
/character-stats/{c} (stats_data JSONB merged to top level),
/combat-stats[/{c}], /inventories, /inventory/{c}/search.
- 5-minute totals cache + /total-rares, /total-kills.
- Ingest-only state returned as Python's empty/default shapes (/quest-status,
/vital-sharing/peers, /equipment-cantrip-state/{c}); /issues (flat file),
/me (401 until cookie verification lands).
- Streaming reverse proxy to inventory-service (/inventory/{c},
/inventory-characters, /search/*, /sets/list, /inv/{path...} incl. the SSE
suitbuilder stream).
- compare/compare_endpoints.py: structural parity for all read endpoints +
exact-match check for /character-stats and /combat-stats on OFFLINE chars
(online chars legitimately differ — Python serves a richer live overlay that
Phase-1 Go lacks until ingest).
Verified live: 14/14 endpoints structural-match, 8/8 rich offline chars
exact-match on /character-stats.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
80 lines
2.3 KiB
Go
80 lines
2.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const totalsInterval = 300 * time.Second // _refresh_total_rares_cache cadence
|
|
|
|
// totalsCache holds the pre-marshaled bodies for /total-rares and /total-kills,
|
|
// refreshed every totalsInterval, mirroring main.py:924.
|
|
type totalsCache struct {
|
|
mu sync.RWMutex
|
|
raresJSON []byte
|
|
killsJSON []byte
|
|
}
|
|
|
|
func newTotalsCache() *totalsCache {
|
|
return &totalsCache{
|
|
raresJSON: []byte(`{"all_time":0,"today":0,"last_updated":null}`),
|
|
killsJSON: []byte(`{"total":0,"last_updated":null}`),
|
|
}
|
|
}
|
|
|
|
func (c *totalsCache) getRares() []byte { c.mu.RLock(); defer c.mu.RUnlock(); return c.raresJSON }
|
|
func (c *totalsCache) getKills() []byte { c.mu.RLock(); defer c.mu.RUnlock(); return c.killsJSON }
|
|
|
|
func (c *totalsCache) set(rares, kills []byte) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.raresJSON = rares
|
|
c.killsJSON = kills
|
|
}
|
|
|
|
func (s *Server) refreshTotals(ctx context.Context) error {
|
|
qctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
|
defer cancel()
|
|
|
|
var allTime, today, totalKills int64
|
|
// Each query degrades to 0 on error, mirroring the Python try/except blocks.
|
|
_ = s.pool.QueryRow(qctx, "SELECT COALESCE(SUM(total_rares), 0) FROM rare_stats").Scan(&allTime)
|
|
_ = s.pool.QueryRow(qctx, "SELECT COUNT(*) FROM rare_events WHERE timestamp >= CURRENT_DATE").Scan(&today)
|
|
_ = s.pool.QueryRow(qctx, "SELECT COALESCE(SUM(total_kills), 0) FROM char_stats").Scan(&totalKills)
|
|
|
|
lastUpdated := pyISO(time.Now().UTC())
|
|
raresJSON, err := json.Marshal(map[string]any{"all_time": allTime, "today": today, "last_updated": lastUpdated})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
killsJSON, err := json.Marshal(map[string]any{"total": totalKills, "last_updated": lastUpdated})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.totals.set(raresJSON, killsJSON)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) runTotalsLoop(ctx context.Context) {
|
|
for {
|
|
if err := s.refreshTotals(ctx); err != nil {
|
|
s.log.Error("totals cache refresh failed", "err", err)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(totalsInterval):
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleTotalRares(w http.ResponseWriter, r *http.Request) {
|
|
writeRawJSON(w, s.totals.getRares())
|
|
}
|
|
|
|
func (s *Server) handleTotalKills(w http.ResponseWriter, r *http.Request) {
|
|
writeRawJSON(w, s.totals.getKills())
|
|
}
|