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>
74 lines
2.9 KiB
Go
74 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
)
|
|
|
|
// initInvProxy builds a streaming reverse proxy to the inventory-service.
|
|
// FlushInterval=-1 flushes writes immediately so SSE endpoints (the suitbuilder
|
|
// search stream) work. Connection errors map to 503, mirroring the Python
|
|
// service's "Inventory service unavailable".
|
|
func (s *Server) initInvProxy(target string) error {
|
|
u, err := url.Parse(target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rp := httputil.NewSingleHostReverseProxy(u)
|
|
rp.FlushInterval = -1
|
|
rp.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
|
s.log.Error("inventory proxy error", "err", err, "path", r.URL.Path)
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]any{"detail": "Inventory service unavailable"})
|
|
}
|
|
s.invProxy = rp
|
|
return nil
|
|
}
|
|
|
|
// proxyInv returns a handler that rewrites the request path (via rewrite) and
|
|
// forwards it to the inventory-service, preserving method, query, headers, and
|
|
// body. The original /inv/* prefix etc. is mapped to the upstream path.
|
|
func (s *Server) proxyInv(rewrite func(r *http.Request) string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if s.invProxy == nil {
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]any{"detail": "Inventory service unavailable"})
|
|
return
|
|
}
|
|
r.URL.Path = rewrite(r)
|
|
r.URL.RawPath = "" // force re-encode from the (decoded) Path
|
|
s.invProxy.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
func (s *Server) registerProxyRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /inventory/{character_name}", s.proxyInv(func(r *http.Request) string {
|
|
return "/inventory/" + r.PathValue("character_name")
|
|
}))
|
|
mux.HandleFunc("GET /inventory-characters", s.proxyInv(func(r *http.Request) string {
|
|
return "/characters/list"
|
|
}))
|
|
mux.HandleFunc("GET /search/items", s.proxyInv(func(r *http.Request) string {
|
|
return "/search/items"
|
|
}))
|
|
mux.HandleFunc("GET /search/equipped/{character_name}", s.proxyInv(func(r *http.Request) string {
|
|
return "/search/equipped/" + r.PathValue("character_name")
|
|
}))
|
|
mux.HandleFunc("GET /search/upgrades/{character_name}/{slot}", s.proxyInv(func(r *http.Request) string {
|
|
return "/search/upgrades/" + r.PathValue("character_name") + "/" + r.PathValue("slot")
|
|
}))
|
|
mux.HandleFunc("GET /sets/list", s.proxyInv(func(r *http.Request) string {
|
|
return "/sets/list"
|
|
}))
|
|
|
|
// /inv/test is a static liveness probe in the Python service.
|
|
mux.HandleFunc("GET /inv/test", func(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]any{"message": "Inventory proxy route is working"})
|
|
})
|
|
// Generic catch-all proxy: /inv/{path...} -> {SVC}/{path}. Covers GET and
|
|
// POST (incl. the SSE suitbuilder search). Registered for both methods.
|
|
invAll := s.proxyInv(func(r *http.Request) string {
|
|
return "/" + r.PathValue("path")
|
|
})
|
|
mux.HandleFunc("GET /inv/{path...}", invAll)
|
|
mux.HandleFunc("POST /inv/{path...}", invAll)
|
|
}
|