+60
-21
@@ -38,22 +38,23 @@ 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, adminWSCount atomic.Int64
|
||||
adminSessions sync.Map // sid -> exp unix seconds
|
||||
store *data.Store
|
||||
auth *auth.Manager
|
||||
settings *settings.Manager
|
||||
hub *wsx.Hub
|
||||
placeHub *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, maxPlaceWS int64
|
||||
userWSCount, leaderboardWSCount, placeWSCount, 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 {
|
||||
@@ -62,6 +63,7 @@ func New(store *data.Store, a *auth.Manager, sm *settings.Manager, hub *wsx.Hub,
|
||||
auth: a,
|
||||
settings: sm,
|
||||
hub: hub,
|
||||
placeHub: wsx.New(),
|
||||
runtime: runtimeState,
|
||||
artifactWorker: artifactWorker,
|
||||
lottery: newGuessLottery(func(d beaconDrawAudit) {
|
||||
@@ -77,6 +79,7 @@ func New(store *data.Store, a *auth.Manager, sm *settings.Manager, hub *wsx.Hub,
|
||||
wsAllowedOrigins: parseOriginAllowlist(os.Getenv("WS_ALLOWED_ORIGINS")),
|
||||
maxUserWS: int64(envIntServer("WS_MAX_USER_CONNECTIONS", 5000)),
|
||||
maxLeaderboardWS: int64(envIntServer("WS_MAX_LEADERBOARD_CONNECTIONS", 500)),
|
||||
maxPlaceWS: int64(envIntServer("WS_MAX_PLACE_CONNECTIONS", 1500)),
|
||||
}
|
||||
s.upgrader = websocket.Upgrader{CheckOrigin: s.checkWSOrigin, Subprotocols: []string{"neuralhunt.v1"}}
|
||||
return s
|
||||
@@ -293,7 +296,7 @@ func (s *Server) Routes() http.Handler {
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
|
||||
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
|
||||
path := strings.ToLower(req.URL.Path)
|
||||
if path == "/" || path == "/admin" || path == "/leaderboard" || strings.HasPrefix(path, "/api/admin") || strings.HasPrefix(path, "/api/auth") || strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".css") {
|
||||
if path == "/" || path == "/admin" || path == "/leaderboard" || path == "/place" || strings.HasPrefix(path, "/api/admin") || strings.HasPrefix(path, "/api/auth") || strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".css") {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
}
|
||||
next.ServeHTTP(w, req)
|
||||
@@ -304,9 +307,12 @@ func (s *Server) Routes() http.Handler {
|
||||
r.Get("/api/public/artifacts", s.publicArtifacts)
|
||||
r.Get("/api/public/beacon/{id}/latest", s.latestBeaconDraw)
|
||||
r.Get("/api/public/tasks", s.publicTaskCatalog)
|
||||
r.Get("/api/public/place", s.publicPlace)
|
||||
r.Get("/api/public/place/changes", s.publicPlaceChanges)
|
||||
r.Get("/api/public/artifacts/{id}/preview", s.publicArtifactPreview)
|
||||
r.Get("/api/public/tasks/{id}/style-reference", s.publicTaskStyleReference)
|
||||
r.Get("/api/leaderboard/ws", s.leaderboardWS)
|
||||
r.Get("/api/place/ws", s.placeWS)
|
||||
r.Post("/api/auth/challenge", s.challenge)
|
||||
r.Post("/api/auth/login", s.login)
|
||||
r.Post("/api/admin/login", s.adminLogin)
|
||||
@@ -323,6 +329,8 @@ func (s *Server) Routes() http.Handler {
|
||||
r.Post("/api/tasks/{id}/guess", s.guess)
|
||||
r.Get("/api/tasks/{id}/points", s.points)
|
||||
r.Get("/api/me", s.me)
|
||||
r.Get("/api/place/me", s.placeMe)
|
||||
r.Post("/api/place/pixel", s.placePixel)
|
||||
r.Get("/api/me/artifacts", s.myArtifacts)
|
||||
r.Get("/api/me/artifacts/{id}/download", s.myArtifactDownload)
|
||||
r.Post("/api/me/customer-link", s.customerLinkCode)
|
||||
@@ -847,7 +855,11 @@ func (s *Server) guess(w http.ResponseWriter, r *http.Request) {
|
||||
if correct && draw.BeaconEnabled {
|
||||
beaconPath, beaconBoost, beaconRound = draw.ChosenPath, draw.BoostedPath, draw.BeaconRound
|
||||
}
|
||||
p, err := s.store.PersistImprovement(r.Context(), t, c.ClientID, rewardOwner, accepted.State.NextSeq, accepted.State.GuessCount, accepted.State.LastGuess, accepted.State.BestScore, in.Guess, in.Signature, correct, beaconPath, beaconBoost, beaconRound)
|
||||
placePointsPerScore := 0
|
||||
if placeCfg := s.settings.Get(); placeCfg.PlaceEnabled == 1 {
|
||||
placePointsPerScore = placeCfg.PlacePointsPerScore
|
||||
}
|
||||
p, err := s.store.PersistImprovement(r.Context(), t, c.ClientID, rewardOwner, accepted.State.NextSeq, accepted.State.GuessCount, accepted.State.LastGuess, accepted.State.BestScore, in.Guess, in.Signature, correct, beaconPath, beaconBoost, beaconRound, placePointsPerScore)
|
||||
if err != nil {
|
||||
s.runtime.Restore(t.Task, c.ClientID, accepted.State.NextSeq, accepted.Previous)
|
||||
switch {
|
||||
@@ -1346,6 +1358,7 @@ type profileCleanupPreview struct {
|
||||
InactiveForSeconds int64 `json:"inactive_for_seconds"`
|
||||
Eligible int `json:"eligible"`
|
||||
ProtectedWinners int64 `json:"protected_winners"`
|
||||
ProtectedPlace int64 `json:"protected_place"`
|
||||
ProtectedConnected int `json:"protected_connected"`
|
||||
OldestEligibleMS int64 `json:"oldest_eligible_ms,omitempty"`
|
||||
NewestEligibleMS int64 `json:"newest_eligible_ms,omitempty"`
|
||||
@@ -1374,8 +1387,12 @@ func (s *Server) profileCleanupPreview(ctx context.Context, inactiveForSeconds i
|
||||
if err != nil {
|
||||
return profileCleanupPreview{}, nil, err
|
||||
}
|
||||
protectedPlace, err := s.store.OldPlaceParticipantCount(ctx, cutoff)
|
||||
if err != nil {
|
||||
return profileCleanupPreview{}, nil, err
|
||||
}
|
||||
ids := make([]string, 0, len(candidates))
|
||||
out := profileCleanupPreview{CutoffMS: cutoff, InactiveForSeconds: inactiveForSeconds, ProtectedWinners: protectedWinners}
|
||||
out := profileCleanupPreview{CutoffMS: cutoff, InactiveForSeconds: inactiveForSeconds, ProtectedWinners: protectedWinners, ProtectedPlace: protectedPlace}
|
||||
for _, c := range candidates {
|
||||
if s.runtime.IsConnected(c.ClientID) {
|
||||
out.ProtectedConnected++
|
||||
@@ -1437,6 +1454,7 @@ func (s *Server) adminProfileCleanup(w http.ResponseWriter, r *http.Request) {
|
||||
"deleted": len(deleted),
|
||||
"eligible_before": preview.Eligible,
|
||||
"protected_winners": preview.ProtectedWinners,
|
||||
"protected_place": preview.ProtectedPlace,
|
||||
"protected_connected": preview.ProtectedConnected,
|
||||
"cutoff_ms": preview.CutoffMS,
|
||||
"inactive_for_seconds": seconds,
|
||||
@@ -1457,11 +1475,16 @@ func (s *Server) adminOverview(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) adminPerformance(w http.ResponseWriter, r *http.Request) {
|
||||
rm := s.runtime.Metrics()
|
||||
wm := s.hub.Metrics()
|
||||
pm := wsx.Metrics{}
|
||||
if s.placeHub != nil {
|
||||
pm = s.placeHub.Metrics()
|
||||
}
|
||||
var ms gort.MemStats
|
||||
gort.ReadMemStats(&ms)
|
||||
jsonOut(w, 200, map[string]any{
|
||||
"runtime": rm,
|
||||
"websocket": wm,
|
||||
"runtime": rm,
|
||||
"websocket": wm,
|
||||
"place_websocket": pm,
|
||||
"process": map[string]any{
|
||||
"goroutines": gort.NumGoroutine(),
|
||||
"heap_bytes": ms.HeapAlloc,
|
||||
@@ -1480,6 +1503,22 @@ func (s *Server) adminSettingsPut(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOut(w, 400, map[string]string{"error": "bad json"})
|
||||
return
|
||||
}
|
||||
if err := settings.Validate(v); err != nil {
|
||||
jsonOut(w, 400, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
current := s.settings.Get()
|
||||
if v.PlaceWidth < current.PlaceWidth || v.PlaceHeight < current.PlaceHeight {
|
||||
outside, err := s.store.PlacePixelsOutside(r.Context(), v.PlaceWidth, v.PlaceHeight)
|
||||
if err != nil {
|
||||
jsonOut(w, 500, map[string]string{"error": "place canvas bounds check failed"})
|
||||
return
|
||||
}
|
||||
if outside > 0 {
|
||||
jsonOut(w, 400, map[string]string{"error": fmt.Sprintf("place canvas cannot shrink: %d existing pixels would fall outside the new dimensions", outside)})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := s.settings.Update(r.Context(), v); err != nil {
|
||||
jsonOut(w, 400, map[string]string{"error": err.Error()})
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user