+74
-16
@@ -38,22 +38,22 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
store *data.Store
|
||||
auth *auth.Manager
|
||||
settings *settings.Manager
|
||||
hub *wsx.Hub
|
||||
runtime *rtx.State
|
||||
artifactWorker *artifact.Worker
|
||||
lottery *guessLottery
|
||||
adminUser, adminPass, staticDir, artifactDir string
|
||||
internalServiceSecret string
|
||||
customerServiceInternalURL string
|
||||
customerServiceHTTP *http.Client
|
||||
upgrader websocket.Upgrader
|
||||
wsAllowedOrigins map[string]struct{}
|
||||
maxUserWS, maxLeaderboardWS int64
|
||||
userWSCount, leaderboardWSCount atomic.Int64
|
||||
adminSessions sync.Map // sid -> exp unix seconds
|
||||
store *data.Store
|
||||
auth *auth.Manager
|
||||
settings *settings.Manager
|
||||
hub *wsx.Hub
|
||||
runtime *rtx.State
|
||||
artifactWorker *artifact.Worker
|
||||
lottery *guessLottery
|
||||
adminUser, adminPass, staticDir, artifactDir string
|
||||
internalServiceSecret string
|
||||
customerServiceInternalURL string
|
||||
customerServiceHTTP *http.Client
|
||||
upgrader websocket.Upgrader
|
||||
wsAllowedOrigins map[string]struct{}
|
||||
maxUserWS, maxLeaderboardWS int64
|
||||
userWSCount, leaderboardWSCount, adminWSCount atomic.Int64
|
||||
adminSessions sync.Map // sid -> exp unix seconds
|
||||
}
|
||||
|
||||
func New(store *data.Store, a *auth.Manager, sm *settings.Manager, hub *wsx.Hub, runtimeState *rtx.State, artifactDir string, artifactWorker *artifact.Worker) *Server {
|
||||
@@ -331,6 +331,7 @@ func (s *Server) Routes() http.Handler {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(func(n http.Handler) http.Handler { return s.require("admin", n) })
|
||||
r.Get("/api/admin/session", s.adminSession)
|
||||
r.Get("/api/admin/ws", s.adminWS)
|
||||
r.Get("/api/admin/overview", s.adminOverview)
|
||||
r.Get("/api/admin/performance", s.adminPerformance)
|
||||
r.Get("/api/admin/profiles/cleanup-preview", s.adminProfileCleanupPreview)
|
||||
@@ -869,6 +870,18 @@ func (s *Server) guess(w http.ResponseWriter, r *http.Request) {
|
||||
p.Score = round(p.Score, s.settings.Get().PublicScorePrecision)
|
||||
s.hub.PublishPoint(id, c.ClientID, p)
|
||||
}
|
||||
// Admin-only live telemetry: expose the score of every actually evaluated
|
||||
// guess, even when it is below the client's personal best. This event is
|
||||
// ephemeral and never reaches public/user websocket clients.
|
||||
_ = s.hub.PublishAdmin(r.Context(), wsx.Event{Type: "guess_signal", TaskID: id, Data: map[string]any{
|
||||
"client_id": c.ClientID,
|
||||
"seq": in.Seq,
|
||||
"score": round(score, 2),
|
||||
"best_score": round(accepted.State.BestScore, 2),
|
||||
"improved": accepted.Improved,
|
||||
"correct": correct,
|
||||
}})
|
||||
|
||||
if correct {
|
||||
rewardOwner := s.store.RewardOwnerForWorker(r.Context(), c.ClientID)
|
||||
dataOut := map[string]string{"winner_client_id": rewardOwner, "winner_worker_client_id": c.ClientID}
|
||||
@@ -1190,6 +1203,51 @@ func (s *Server) publicArtifactPreview(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(preview)
|
||||
}
|
||||
|
||||
func (s *Server) adminWS(w http.ResponseWriter, r *http.Request) {
|
||||
// The route itself is behind require("admin"), so the HttpOnly admin session
|
||||
// cookie is validated before the websocket upgrade. Keep a small independent
|
||||
// cap because this stream can contain one event for every evaluated guess.
|
||||
if !acquireWSCap(&s.adminWSCount, 32) {
|
||||
http.Error(w, "admin websocket capacity reached", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
defer s.adminWSCount.Add(-1)
|
||||
conn, err := s.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cl := wsx.NewAdminClient(conn)
|
||||
s.hub.Add(cl)
|
||||
defer s.hub.Remove(cl)
|
||||
|
||||
conn.SetReadLimit(1024)
|
||||
_ = conn.SetReadDeadline(time.Now().Add(90 * time.Second))
|
||||
conn.SetPongHandler(func(string) error { return conn.SetReadDeadline(time.Now().Add(90 * time.Second)) })
|
||||
ping := time.NewTicker(30 * time.Second)
|
||||
defer ping.Stop()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for {
|
||||
if _, _, err := conn.ReadMessage(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-ping.C:
|
||||
if err := cl.Ping(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) leaderboardWS(w http.ResponseWriter, r *http.Request) {
|
||||
if !acquireWSCap(&s.leaderboardWSCount, s.maxLeaderboardWS) {
|
||||
http.Error(w, "leaderboard websocket capacity reached", http.StatusServiceUnavailable)
|
||||
|
||||
Reference in New Issue
Block a user