package server import ( "context" "encoding/json" "errors" "fmt" "io" iofs "io/fs" "log" "math" "net/http" "os" "path/filepath" gort "runtime" "strconv" "strings" "time" "neuralhunt/internal/artifact" "neuralhunt/internal/auth" "neuralhunt/internal/core" "neuralhunt/internal/data" rtx "neuralhunt/internal/runtime" "neuralhunt/internal/settings" "neuralhunt/internal/webui" wsx "neuralhunt/internal/ws" "github.com/go-chi/chi/v5" "github.com/gorilla/websocket" ) type Server struct { store *data.Store auth *auth.Manager settings *settings.Manager hub *wsx.Hub runtime *rtx.State artifactWorker *artifact.Worker adminUser, adminPass, staticDir, artifactDir string upgrader websocket.Upgrader } func New(store *data.Store, a *auth.Manager, sm *settings.Manager, hub *wsx.Hub, runtimeState *rtx.State, artifactDir string, artifactWorker *artifact.Worker) *Server { return &Server{ store: store, auth: a, settings: sm, hub: hub, runtime: runtimeState, artifactWorker: artifactWorker, adminUser: env("ADMIN_USER", "admin"), adminPass: env("ADMIN_PASSWORD", "change-me"), staticDir: env("STATIC_DIR", ""), artifactDir: artifactDir, upgrader: websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}, } } func env(k, d string) string { if v := os.Getenv(k); v != "" { return v } return d } func jsonOut(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func jsonAPIError(w http.ResponseWriter, status int, code, message string, extra map[string]any) { body := map[string]any{"error": message, "code": code} for k, v := range extra { body[k] = v } jsonOut(w, status, body) } func decode(r *http.Request, v any) error { d := json.NewDecoder(io.LimitReader(r.Body, 1<<20)) d.DisallowUnknownFields() if err := d.Decode(v); err != nil { return err } // Reject a second JSON value while still allowing insignificant trailing // whitespace. This keeps the request format unambiguous. var extra any if err := d.Decode(&extra); err != io.EOF { if err == nil { return errors.New("multiple JSON values") } return err } return nil } // Authentication receives a standards-compliant JWK exported by the browser. // Different WebCrypto implementations may add optional JWK members such as // alg/use/kid (and future implementations may add more). Those members are not // security relevant here: ClientID/PublicKey only consume kty, crv, x and y. // Therefore auth payloads intentionally accept unknown nested JWK properties. func decodeAuth(r *http.Request, v any) error { d := json.NewDecoder(io.LimitReader(r.Body, 1<<20)) if err := d.Decode(v); err != nil { return err } var extra any if err := d.Decode(&extra); err != io.EOF { if err == nil { return errors.New("multiple JSON values") } return err } return nil } type ctxKey string const claimsKey ctxKey = "claims" func (s *Server) bearer(r *http.Request) (auth.Claims, error) { h := r.Header.Get("Authorization") if !strings.HasPrefix(h, "Bearer ") { return auth.Claims{}, errors.New("missing bearer") } return s.auth.Parse(strings.TrimPrefix(h, "Bearer ")) } func (s *Server) require(role string, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := s.bearer(r) if err != nil || (role != "" && c.Role != role) { jsonOut(w, 401, map[string]string{"error": "unauthorized"}) return } next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), claimsKey, c))) }) } func claims(r *http.Request) auth.Claims { return r.Context().Value(claimsKey).(auth.Claims) } func (s *Server) Routes() http.Handler { r := chi.NewRouter() r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()") path := strings.ToLower(req.URL.Path) if path == "/" || path == "/admin" || path == "/leaderboard" || strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".css") { w.Header().Set("Cache-Control", "no-store") } next.ServeHTTP(w, req) }) }) r.Get("/api/healthz", func(w http.ResponseWriter, r *http.Request) { jsonOut(w, 200, map[string]bool{"ok": true}) }) r.Get("/api/public/leaderboard", s.publicLeaderboard) r.Get("/api/public/artifacts", s.publicArtifacts) 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.Post("/api/auth/challenge", s.challenge) r.Post("/api/auth/login", s.login) r.Post("/api/admin/login", s.adminLogin) r.Group(func(r chi.Router) { r.Use(func(n http.Handler) http.Handler { return s.require("user", n) }) r.Get("/api/tasks", s.clientTasks) r.Get("/api/tasks/current", s.currentTask) r.Post("/api/tasks/select", s.selectTask) 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/leaderboard", s.leaderboard) }) r.Group(func(r chi.Router) { r.Use(func(n http.Handler) http.Handler { return s.require("admin", n) }) r.Get("/api/admin/overview", s.adminOverview) r.Get("/api/admin/performance", s.adminPerformance) r.Get("/api/admin/profiles/cleanup-preview", s.adminProfileCleanupPreview) r.Post("/api/admin/profiles/cleanup", s.adminProfileCleanup) r.Get("/api/admin/settings", s.adminSettingsGet) r.Put("/api/admin/settings", s.adminSettingsPut) r.Get("/api/admin/tasks", s.adminTasks) r.Get("/api/admin/tasks/{id}/points", s.adminPoints) r.Get("/api/admin/tasks/{id}/actions", s.adminTaskActions) r.Put("/api/admin/tasks/{id}/config", s.adminTaskConfigPut) r.Post("/api/admin/tasks/{id}/actions", s.adminScheduleAction) r.Post("/api/admin/actions/{id}/cancel", s.adminCancelAction) r.Get("/api/admin/artifact/providers", s.adminArtifactProviders) r.Get("/api/admin/artifact/usage", s.adminArtifactUsage) r.Post("/api/admin/artifact/character-anchor", s.adminCreateCharacterAnchor) r.Get("/api/admin/artifact/character-anchor", s.adminCharacterAnchorFile) r.Put("/api/admin/tasks/{id}/style-reference", s.adminTaskStyleReferencePut) r.Delete("/api/admin/tasks/{id}/style-reference", s.adminTaskStyleReferenceDelete) r.Get("/api/admin/tasks/{id}/style-reference", s.adminTaskStyleReferenceFile) r.Post("/api/admin/tasks/{id}/pipeline-test", s.adminCreatePipelineTestCard) r.Get("/api/admin/tasks/{id}/pipeline-test/card", s.adminPipelineTestCardFile) r.Get("/api/admin/tasks/{id}/artifact", s.adminArtifactFile) r.Get("/api/admin/tasks/{id}/manifest", s.adminArtifactManifest) r.Post("/api/admin/tasks/{id}/close", s.adminCloseTask) r.Post("/api/admin/tasks/ensure", s.adminEnsure) }) r.Get("/api/ws", s.ws) // Original winner artifacts are intentionally not publicly file-served. // Public viewers only receive /api/public/artifacts/{id}/preview, which is // watermarked. Keep the old namespace as an explicit 404 instead of letting // the SPA fallback accidentally return index.html for an artifact URL. r.Handle("/artifacts/*", http.NotFoundHandler()) // The production UI is embedded in the Go binary. STATIC_DIR remains an // optional development override, but Node.js/npm are never required to run // the application. var staticFS iofs.FS if s.staticDir != "" { if st, err := os.Stat(s.staticDir); err == nil && st.IsDir() { staticFS = os.DirFS(s.staticDir) log.Printf("serving frontend override from %q", s.staticDir) } } if staticFS == nil { embedded, err := iofs.Sub(webui.Dist, "dist") if err != nil { panic(fmt.Errorf("embedded frontend: %w", err)) } staticFS = embedded } fileServer := http.FileServer(http.FS(staticFS)) spa := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.Path, "/") if name == "" { name = "index.html" } if info, err := iofs.Stat(staticFS, name); err == nil && !info.IsDir() { fileServer.ServeHTTP(w, r) return } // Browser-side routes such as /admin receive the SPA entry point. b, err := iofs.ReadFile(staticFS, "index.html") if err != nil { http.Error(w, "embedded frontend unavailable", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write(b) }) r.Handle("/", spa) r.Handle("/*", spa) return r } func (s *Server) challenge(w http.ResponseWriter, r *http.Request) { var in struct { PublicJWK auth.PublicJWK `json:"public_jwk"` } if err := decodeAuth(r, &in); err != nil { log.Printf("auth challenge decode: %v", err) jsonOut(w, 400, map[string]string{"error": "bad json: " + err.Error()}) return } cid, err := auth.ClientID(in.PublicJWK) if err != nil { jsonOut(w, 400, map[string]string{"error": err.Error()}) return } c, err := s.auth.NewChallenge(r.Context(), cid) if err != nil { log.Printf("auth challenge for %s: %v", cid, err) jsonOut(w, 500, map[string]string{"error": "challenge failed"}) return } jsonOut(w, 200, map[string]string{"client_id": cid, "challenge": c}) } func (s *Server) login(w http.ResponseWriter, r *http.Request) { var in struct { PublicJWK auth.PublicJWK `json:"public_jwk"` Challenge string `json:"challenge"` Signature string `json:"signature"` } if err := decodeAuth(r, &in); err != nil { log.Printf("auth login decode: %v", err) jsonOut(w, 400, map[string]string{"error": "bad json: " + err.Error()}) return } cid, err := auth.ClientID(in.PublicJWK) if err != nil { jsonOut(w, 400, map[string]string{"error": err.Error()}) return } pub, err := auth.PublicKey(in.PublicJWK) if err != nil || !auth.VerifyRaw(pub, "login|"+in.Challenge+"|"+cid, in.Signature) { jsonOut(w, 401, map[string]string{"error": "invalid signature"}) return } if err = s.auth.ConsumeChallenge(r.Context(), cid, in.Challenge); err != nil { log.Printf("auth login challenge for %s: %v", cid, err) jsonOut(w, 401, map[string]string{"error": err.Error()}) return } if err = s.store.UpsertClient(r.Context(), cid, in.PublicJWK); err != nil { log.Printf("auth store client %s: %v", cid, err) jsonOut(w, 500, map[string]string{"error": "client store failed"}) return } tok, _, err := s.auth.Issue(cid, "user", 24*time.Hour) if err != nil { log.Printf("auth issue token for %s: %v", cid, err) jsonOut(w, 500, map[string]string{"error": "token creation failed"}) return } jsonOut(w, 200, map[string]string{"token": tok, "client_id": cid}) } func (s *Server) adminLogin(w http.ResponseWriter, r *http.Request) { var in struct { User string `json:"user"` Password string `json:"password"` } if decode(r, &in) != nil || in.User != s.adminUser || in.Password != s.adminPass { jsonOut(w, 401, map[string]string{"error": "invalid credentials"}) return } tok, _, err := s.auth.Issue("admin", "admin", 8*time.Hour) if err != nil { log.Printf("admin auth issue token: %v", err) jsonOut(w, 500, map[string]string{"error": "token creation failed"}) return } jsonOut(w, 200, map[string]string{"token": tok}) } func taskIntervals(t data.Task, sm settings.Runtime) (int, int) { serverMin := sm.GuessMinIntervalSec clientSubmit := sm.ClientSubmitIntervalSec if t.GuessMinIntervalSec != nil { serverMin = *t.GuessMinIntervalSec } if t.ClientSubmitIntervalSec != nil { clientSubmit = *t.ClientSubmitIntervalSec } return serverMin, clientSubmit } func taskDTO(t data.Task, next int64, sm settings.Runtime) map[string]any { serverMin, clientSubmit := taskIntervals(t, sm) return map[string]any{ "id": t.ID, "public_seed": t.PublicSeed, "range_bits": t.RangeBits, "next_seq": next, "server_min_interval_sec": serverMin, "client_submit_interval_sec": clientSubmit, "default_max_nodes": sm.DefaultMaxNodes, "paused": t.Paused, "revision": t.Revision, "display_name": t.DisplayName, "description": t.Description, "parent_task_id": t.ParentTaskID, "created_at": t.CreatedAt, } } func (s *Server) clientTasks(w http.ResponseWriter, r *http.Request) { c := claims(r) if !s.store.ClientExists(r.Context(), c.ClientID) { jsonOut(w, http.StatusUnauthorized, map[string]string{"error": "identity not registered in this database"}) return } items, err := s.store.ActiveTasksForClient(r.Context(), c.ClientID) if err != nil { jsonOut(w, 500, map[string]string{"error": "tasks failed"}) return } if len(items) == 0 { _ = s.store.EnsureActiveTasks(r.Context(), s.settings.Get().ActiveTaskCount, s.settings.Get().TaskRangeBits) items, err = s.store.ActiveTasksForClient(r.Context(), c.ClientID) if err != nil { jsonOut(w, 500, map[string]string{"error": "tasks failed"}) return } } // Keep score disclosure consistent with the regular client endpoints. prec := s.settings.Get().PublicScorePrecision for i := range items { items[i].OwnScore = round(items[i].OwnScore, prec) } jsonOut(w, 200, items) } func (s *Server) selectTask(w http.ResponseWriter, r *http.Request) { c := claims(r) var in struct { TaskID string `json:"task_id"` } if err := decode(r, &in); err != nil { jsonOut(w, 400, map[string]string{"error": "bad json: " + err.Error()}) return } in.TaskID = strings.TrimSpace(in.TaskID) if in.TaskID == "" { jsonOut(w, 400, map[string]string{"error": "task_id required"}) return } if err := s.store.SetClientTaskSelection(r.Context(), c.ClientID, in.TaskID); err != nil { jsonOut(w, 400, map[string]string{"error": err.Error()}) return } s.runtime.SetTaskSelection(c.ClientID, in.TaskID) t, err := s.store.TaskForClient(r.Context(), c.ClientID) if err != nil { jsonOut(w, 500, map[string]string{"error": "task selection failed"}) return } s.runtime.SetTaskSelection(c.ClientID, t.ID) snap, _ := s.store.LoadGuessState(r.Context(), t.ID, c.ClientID) g := s.runtime.InitGuess(t, c.ClientID, rtx.GuessState{NextSeq: snap.NextSeq, LastGuess: snap.LastGuess, BestScore: snap.BestScore, GuessCount: snap.GuessCount}) jsonOut(w, 200, taskDTO(t, g.NextSeq, s.settings.Get())) } func (s *Server) currentTask(w http.ResponseWriter, r *http.Request) { c := claims(r) if !s.store.ClientExists(r.Context(), c.ClientID) { jsonOut(w, http.StatusUnauthorized, map[string]string{"error": "identity not registered in this database"}) return } t, err := s.store.TaskForClient(r.Context(), c.ClientID) if data.IsNoRows(err) { _ = s.store.EnsureActiveTasks(r.Context(), s.settings.Get().ActiveTaskCount, s.settings.Get().TaskRangeBits) t, err = s.store.TaskForClient(r.Context(), c.ClientID) } if err != nil { jsonOut(w, 503, map[string]string{"error": "no active task"}) return } s.runtime.SetTaskSelection(c.ClientID, t.ID) snap, _ := s.store.LoadGuessState(r.Context(), t.ID, c.ClientID) g := s.runtime.InitGuess(t, c.ClientID, rtx.GuessState{NextSeq: snap.NextSeq, LastGuess: snap.LastGuess, BestScore: snap.BestScore, GuessCount: snap.GuessCount}) jsonOut(w, 200, taskDTO(t, g.NextSeq, s.settings.Get())) } func guessMsg(taskID string, seq int64, guess string) string { return fmt.Sprintf("guess|%s|%d|%s", taskID, seq, guess) } func round(v float64, p int) float64 { m := math.Pow10(p) return math.Round(v*m) / m } func (s *Server) guess(w http.ResponseWriter, r *http.Request) { c := claims(r) id := chi.URLParam(r, "id") if selected := s.runtime.TaskSelection(c.ClientID); selected != id { durable, err := s.store.SelectedTaskID(r.Context(), c.ClientID) if err != nil || durable != id { jsonAPIError(w, http.StatusConflict, "selection_conflict", "task selection changed", nil) return } s.runtime.SetTaskSelection(c.ClientID, id) } if !s.runtime.HasPresence(c.ClientID, c.SessionID) { jsonAPIError(w, http.StatusConflict, "presence_required", "live connection lost; reconnect websocket", nil) return } var in struct { Seq int64 `json:"seq"` Guess string `json:"guess"` Signature string `json:"signature"` } if decode(r, &in) != nil { jsonOut(w, 400, false) return } t, err := s.store.SecretTask(r.Context(), id) if err != nil || t.Status != "active" { jsonAPIError(w, http.StatusConflict, "task_inactive", "task is no longer active", nil) return } if t.Paused { jsonOut(w, 423, false) return } if core.ExpectedGuess(id, t.PublicSeed, c.ClientID, in.Seq, t.RangeBits) != in.Guess { // This most commonly means an admin rerolled/changed the task between the // client's config read and its submit. Treat it as recoverable config drift. jsonAPIError(w, http.StatusConflict, "task_config_changed", "task configuration changed; resync required", map[string]any{"revision": t.Revision}) return } jwk, err := s.store.ClientPublicJWK(r.Context(), c.ClientID) if err != nil { jsonOut(w, 401, false) return } pub, _ := auth.PublicKey(jwk) if pub == nil || !auth.VerifyRaw(pub, guessMsg(id, in.Seq, in.Guess), in.Signature) { jsonOut(w, 401, false) return } d, err := core.Distance(in.Guess, t.Secret) if err != nil { jsonOut(w, 400, false) return } correct := d.Sign() == 0 score := core.Score(d, t.RangeBits) if _, ok := s.runtime.Current(t.Task, c.ClientID); !ok { snap, _ := s.store.LoadGuessState(r.Context(), t.ID, c.ClientID) s.runtime.InitGuess(t.Task, c.ClientID, rtx.GuessState{NextSeq: snap.NextSeq, LastGuess: snap.LastGuess, BestScore: snap.BestScore, GuessCount: snap.GuessCount}) } serverMin, _ := taskIntervals(t.Task, s.settings.Get()) accepted, err := s.runtime.Accept(t.Task, c.ClientID, in.Seq, score, time.Duration(serverMin)*time.Second) if err != nil { switch { case errors.Is(err, rtx.ErrRateLimited): jsonOut(w, 429, false) case errors.Is(err, rtx.ErrBadSequence): expected := int64(0) if cur, ok := s.runtime.Current(t.Task, c.ClientID); ok { expected = cur.NextSeq } jsonAPIError(w, http.StatusConflict, "sequence_mismatch", "guess sequence is stale", map[string]any{"next_seq": expected}) default: jsonOut(w, 500, false) } return } // Losing tips are intentionally ephemeral: no SQLite write and no websocket event. if accepted.Improved || correct { p, err := s.store.PersistImprovement(r.Context(), t, c.ClientID, accepted.State.NextSeq, accepted.State.GuessCount, accepted.State.LastGuess, accepted.State.BestScore, in.Guess, in.Signature, correct) if err != nil { s.runtime.Restore(t.Task, c.ClientID, accepted.State.NextSeq, accepted.Previous) switch { case errors.Is(err, data.ErrTaskCompleted): jsonAPIError(w, http.StatusConflict, "task_inactive", "task completed while submitting", nil) case errors.Is(err, data.ErrTaskPaused): jsonOut(w, 423, false) default: jsonOut(w, 500, false) } return } s.runtime.MarkSQLiteWrite() p.Score = round(p.Score, s.settings.Get().PublicScorePrecision) s.hub.PublishPoint(id, c.ClientID, p) } if correct { dataOut := map[string]string{"winner_client_id": c.ClientID} if successor, succErr := s.store.EnsureSuccessorTask(r.Context(), id, s.settings.Get().TaskRangeBits); succErr == nil { dataOut["successor_task_id"] = successor.ID s.runtime.ReplaceTaskSelection(id, successor.ID) } else { log.Printf("successor for %s: %v", id, succErr) } _ = s.hub.Publish(r.Context(), wsx.Event{Type: "task_completed", TaskID: id, Data: dataOut}) _ = s.store.EnsureActiveTasks(r.Context(), s.settings.Get().ActiveTaskCount, s.settings.Get().TaskRangeBits) } jsonOut(w, 200, correct) } func (s *Server) points(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit <= 0 { limit = s.settings.Get().DefaultMaxNodes * 3 } if limit > 10000 { limit = 10000 } ps, err := s.store.PointsForClient(r.Context(), chi.URLParam(r, "id"), claims(r).ClientID, limit) if err != nil { jsonOut(w, 500, map[string]string{"error": "points failed"}) return } for i := range ps { ps[i].Score = round(ps[i].Score, s.settings.Get().PublicScorePrecision) } jsonOut(w, 200, ps) } func (s *Server) me(w http.ResponseWriter, r *http.Request) { c := claims(r) // A JWT can outlive a replaced/empty SQLite database. Do not treat such a // token as a valid registered browser identity; force challenge/login again // so the public key is upserted into this database. if !s.store.ClientExists(r.Context(), c.ClientID) { jsonOut(w, http.StatusUnauthorized, map[string]string{"error": "identity not registered in this database"}) return } t, err := s.store.TaskForClient(r.Context(), c.ClientID) if err != nil { jsonOut(w, 200, data.Me{ClientID: c.ClientID, Unlocks: []string{}}) return } m, _ := s.store.Me(r.Context(), c.ClientID, t.ID) m.Score = round(m.Score, s.settings.Get().PublicScorePrecision) jsonOut(w, 200, m) } func (s *Server) leaderboard(w http.ResponseWriter, r *http.Request) { l, err := s.store.Leaderboard(r.Context(), 100) if err != nil { jsonOut(w, 500, map[string]string{"error": "leaderboard failed"}) return } for i := range l { l[i].Connected = s.runtime.IsConnected(l[i].ClientID) } jsonOut(w, 200, l) } func (s *Server) publicLeaderboard(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) mode := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("mode"))) var ( l []data.Leader err error ) if mode == "live" { l, err = s.store.LiveLeaderboard(r.Context(), limit) } else { l, err = s.store.Leaderboard(r.Context(), limit) } if err != nil { jsonOut(w, 500, map[string]string{"error": "leaderboard failed"}) return } prec := s.settings.Get().PublicScorePrecision for i := range l { l[i].BestScore = round(l[i].BestScore, prec) l[i].LiveScore = round(l[i].LiveScore, prec) l[i].Connected = s.runtime.IsConnected(l[i].ClientID) } jsonOut(w, 200, l) } func (s *Server) publicArtifacts(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) winner := strings.TrimSpace(r.URL.Query().Get("winner")) items, err := s.store.PublicArtifacts(r.Context(), limit, winner) if err != nil { jsonOut(w, 500, map[string]string{"error": "artifact gallery failed"}) return } jsonOut(w, 200, items) } func (s *Server) publicArtifactPreview(w http.ResponseWriter, r *http.Request) { id := strings.TrimSpace(chi.URLParam(r, "id")) if id == "" { jsonOut(w, 400, map[string]string{"error": "task id required"}) return } artifactURI, _, ok, err := s.store.PublicArtifactSource(r.Context(), id) if err != nil { jsonOut(w, 500, map[string]string{"error": "artifact lookup failed"}) return } if !ok { jsonOut(w, 404, map[string]string{"error": "artifact not found"}) return } path, err := artifactLocalPath(s.artifactDir, artifactURI) if err != nil { log.Printf("artifact preview path %s: %v", id, err) jsonOut(w, 404, map[string]string{"error": "artifact file unavailable"}) return } preview, contentType, err := watermarkPreviewFile(path, "NEURAL HUNT PREVIEW") if err != nil { log.Printf("artifact preview render %s: %v", id, err) jsonOut(w, 500, map[string]string{"error": "preview generation failed"}) return } w.Header().Set("Content-Type", contentType) w.Header().Set("Cache-Control", "public, max-age=300") w.Header().Set("X-Neural-Hunt-Watermark", "leaderboard-preview") w.Header().Set("Content-Disposition", "inline") w.WriteHeader(http.StatusOK) _, _ = w.Write(preview) } func (s *Server) leaderboardWS(w http.ResponseWriter, r *http.Request) { conn, err := s.upgrader.Upgrade(w, r, nil) if err != nil { return } cl := wsx.NewClient(conn, "", "leaderboard", true) s.hub.Add(cl) defer s.hub.Remove(cl) initial, _ := s.store.LiveLeaderboard(r.Context(), 200) cl.Enqueue(wsx.Event{Type: "leaderboard", Data: initial}) for { if _, _, err := conn.ReadMessage(); err != nil { return } } } func (s *Server) adminArtifactFile(w http.ResponseWriter, r *http.Request) { s.serveAdminArtifactPart(w, r, false) } func (s *Server) adminArtifactManifest(w http.ResponseWriter, r *http.Request) { s.serveAdminArtifactPart(w, r, true) } func (s *Server) serveAdminArtifactPart(w http.ResponseWriter, r *http.Request, manifest bool) { id := strings.TrimSpace(chi.URLParam(r, "id")) imageURI, manifestURI, ok, err := s.store.TaskArtifactURIs(r.Context(), id) if err != nil { jsonOut(w, 500, map[string]string{"error": "artifact lookup failed"}) return } if !ok { jsonOut(w, 404, map[string]string{"error": "artifact not found"}) return } uri := imageURI if manifest { uri = manifestURI if uri == "" { jsonOut(w, 404, map[string]string{"error": "manifest not found"}) return } } path, err := artifactLocalPath(s.artifactDir, uri) if err != nil { jsonOut(w, 404, map[string]string{"error": "artifact file unavailable"}) return } if manifest { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Disposition", `inline; filename="manifest.json"`) } else { w.Header().Set("Content-Disposition", `inline`) } w.Header().Set("Cache-Control", "private, no-store") http.ServeFile(w, r, path) } type profileCleanupPreview struct { CutoffMS int64 `json:"cutoff_ms"` InactiveForSeconds int64 `json:"inactive_for_seconds"` Eligible int `json:"eligible"` ProtectedWinners int64 `json:"protected_winners"` ProtectedConnected int `json:"protected_connected"` OldestEligibleMS int64 `json:"oldest_eligible_ms,omitempty"` NewestEligibleMS int64 `json:"newest_eligible_ms,omitempty"` } func profileCleanupDurationSeconds(raw string) (int64, error) { seconds, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64) if err != nil { return 0, errors.New("inactive_for_seconds must be an integer") } // A one-hour minimum prevents an accidental near-live purge while still // allowing short-lived development/test deployments to clean up quickly. if seconds < 3600 || seconds > 10*365*24*60*60 { return 0, errors.New("inactive_for_seconds must be between 3600 seconds and 10 years") } return seconds, nil } func (s *Server) profileCleanupPreview(ctx context.Context, inactiveForSeconds int64) (profileCleanupPreview, []string, error) { cutoff := time.Now().UTC().Add(-time.Duration(inactiveForSeconds) * time.Second).UnixMilli() candidates, err := s.store.InactiveNonWinnerClients(ctx, cutoff) if err != nil { return profileCleanupPreview{}, nil, err } protectedWinners, err := s.store.OldWinnerCount(ctx, cutoff) if err != nil { return profileCleanupPreview{}, nil, err } ids := make([]string, 0, len(candidates)) out := profileCleanupPreview{CutoffMS: cutoff, InactiveForSeconds: inactiveForSeconds, ProtectedWinners: protectedWinners} for _, c := range candidates { if s.runtime.IsConnected(c.ClientID) { out.ProtectedConnected++ continue } ids = append(ids, c.ClientID) if out.OldestEligibleMS == 0 || c.LastSeen < out.OldestEligibleMS { out.OldestEligibleMS = c.LastSeen } if c.LastSeen > out.NewestEligibleMS { out.NewestEligibleMS = c.LastSeen } } out.Eligible = len(ids) return out, ids, nil } func (s *Server) adminProfileCleanupPreview(w http.ResponseWriter, r *http.Request) { seconds, err := profileCleanupDurationSeconds(r.URL.Query().Get("inactive_for_seconds")) if err != nil { jsonOut(w, 400, map[string]string{"error": err.Error()}) return } preview, _, err := s.profileCleanupPreview(r.Context(), seconds) if err != nil { jsonOut(w, 500, map[string]string{"error": "profile cleanup preview failed: " + err.Error()}) return } jsonOut(w, 200, preview) } func (s *Server) adminProfileCleanup(w http.ResponseWriter, r *http.Request) { var in struct { InactiveForSeconds int64 `json:"inactive_for_seconds"` } if err := decode(r, &in); err != nil { jsonOut(w, 400, map[string]string{"error": "bad json: " + err.Error()}) return } seconds, err := profileCleanupDurationSeconds(strconv.FormatInt(in.InactiveForSeconds, 10)) if err != nil { jsonOut(w, 400, map[string]string{"error": err.Error()}) return } preview, ids, err := s.profileCleanupPreview(r.Context(), seconds) if err != nil { jsonOut(w, 500, map[string]string{"error": "profile cleanup check failed: " + err.Error()}) return } deleted, err := s.store.DeleteInactiveNonWinnerClients(r.Context(), preview.CutoffMS, ids) if err != nil { jsonOut(w, 500, map[string]string{"error": "profile cleanup failed: " + err.Error()}) return } for _, id := range deleted { s.runtime.ForgetClient(id) } jsonOut(w, 200, map[string]any{ "deleted": len(deleted), "eligible_before": preview.Eligible, "protected_winners": preview.ProtectedWinners, "protected_connected": preview.ProtectedConnected, "cutoff_ms": preview.CutoffMS, "inactive_for_seconds": seconds, }) } func (s *Server) adminOverview(w http.ResponseWriter, r *http.Request) { var clients, activeTasks, completedTasks, guesses, artifacts int64 _ = s.store.DB.QueryRowContext(r.Context(), `SELECT count(*) FROM clients`).Scan(&clients) _ = s.store.DB.QueryRowContext(r.Context(), `SELECT count(*) FROM tasks WHERE status='active'`).Scan(&activeTasks) _ = s.store.DB.QueryRowContext(r.Context(), `SELECT count(*) FROM tasks WHERE status='completed'`).Scan(&completedTasks) _ = s.store.DB.QueryRowContext(r.Context(), `SELECT COALESCE(sum(guess_count),0) FROM task_points`).Scan(&guesses) _ = s.store.DB.QueryRowContext(r.Context(), `SELECT count(*) FROM tasks WHERE artifact_status='ready'`).Scan(&artifacts) connected := s.runtime.ConnectedCount() jsonOut(w, 200, map[string]int64{"clients": clients, "connected": connected, "active_tasks": activeTasks, "completed_tasks": completedTasks, "guesses": guesses, "artifacts_ready": artifacts}) } func (s *Server) adminPerformance(w http.ResponseWriter, r *http.Request) { rm := s.runtime.Metrics() wm := s.hub.Metrics() var ms gort.MemStats gort.ReadMemStats(&ms) jsonOut(w, 200, map[string]any{ "runtime": rm, "websocket": wm, "process": map[string]any{ "goroutines": gort.NumGoroutine(), "heap_bytes": ms.HeapAlloc, "heap_objects": ms.HeapObjects, "gc_cycles": ms.NumGC, }, }) } func (s *Server) adminSettingsGet(w http.ResponseWriter, r *http.Request) { jsonOut(w, 200, s.settings.Get()) } func (s *Server) adminSettingsPut(w http.ResponseWriter, r *http.Request) { var v settings.Runtime if decode(r, &v) != nil { jsonOut(w, 400, map[string]string{"error": "bad json"}) return } if err := s.settings.Update(r.Context(), v); err != nil { jsonOut(w, 400, map[string]string{"error": err.Error()}) return } jsonOut(w, 200, v) } func (s *Server) adminTasks(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) ts, err := s.store.AdminTasks(r.Context(), r.URL.Query().Get("status"), r.URL.Query().Get("q"), limit) if err != nil { jsonOut(w, 500, map[string]string{"error": err.Error()}) return } jsonOut(w, 200, ts) } func (s *Server) adminTaskConfigPut(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var in struct { DisplayName string `json:"display_name"` Description string `json:"description"` NFTPromptInstructions string `json:"nft_prompt_instructions"` NFTNegativePrompt string `json:"nft_negative_prompt"` } if err := decode(r, &in); err != nil { jsonOut(w, 400, map[string]string{"error": "bad json: " + err.Error()}) return } if err := s.store.UpdateTaskConfig(r.Context(), id, in.DisplayName, in.Description, in.NFTPromptInstructions, in.NFTNegativePrompt); err != nil { jsonOut(w, 400, map[string]string{"error": err.Error()}) return } _ = s.hub.Publish(r.Context(), wsx.Event{Type: "task_changed", TaskID: id, Data: map[string]string{"action": "config"}}) jsonOut(w, 200, map[string]bool{"ok": true}) } func (s *Server) adminPoints(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) ps, err := s.store.Points(r.Context(), chi.URLParam(r, "id"), limit) if err != nil { jsonOut(w, 500, map[string]string{"error": err.Error()}) return } jsonOut(w, 200, ps) } func (s *Server) adminTaskActions(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) actions, err := s.store.TaskActions(r.Context(), chi.URLParam(r, "id"), limit) if err != nil { jsonOut(w, 500, map[string]string{"error": err.Error()}) return } jsonOut(w, 200, actions) } type scheduleActionRequest struct { ActionType string `json:"action_type"` Payload json.RawMessage `json:"payload"` ExecuteAt *time.Time `json:"execute_at"` } func validateAction(actionType string, raw json.RawMessage) error { actionType = strings.ToLower(strings.TrimSpace(actionType)) switch actionType { case "set_range_bits": var p struct { Bits int `json:"bits"` Mode string `json:"mode"` } if len(raw) == 0 || json.Unmarshal(raw, &p) != nil { return errors.New("set_range_bits requires payload {bits,mode}") } if p.Bits < 8 || p.Bits > 128 { return errors.New("bits must be 8..128") } if p.Mode != "preserve" && p.Mode != "reroll" { return errors.New("mode must be preserve or reroll") } case "set_intervals": var p struct { Server int `json:"server_min_interval_sec"` Client int `json:"client_submit_interval_sec"` } if len(raw) == 0 || json.Unmarshal(raw, &p) != nil { return errors.New("set_intervals requires interval payload") } if p.Server < 1 || p.Server > 3600 || p.Client <= p.Server || p.Client > 7200 { return errors.New("intervals require server 1..3600 and client > server <=7200") } case "pause", "resume", "reroll", "clear_intervals", "close", "regenerate_artifact": // no payload required default: return fmt.Errorf("unsupported action_type %q", actionType) } return nil } func (s *Server) adminScheduleAction(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var in scheduleActionRequest if err := decode(r, &in); err != nil { jsonOut(w, 400, map[string]string{"error": "bad json: " + err.Error()}) return } in.ActionType = strings.ToLower(strings.TrimSpace(in.ActionType)) if err := validateAction(in.ActionType, in.Payload); err != nil { jsonOut(w, 400, map[string]string{"error": err.Error()}) return } executeAt := time.Now().UTC() if in.ExecuteAt != nil { executeAt = in.ExecuteAt.UTC() } if executeAt.Before(time.Now().Add(-2 * time.Minute)) { jsonOut(w, 400, map[string]string{"error": "execute_at is in the past"}) return } if executeAt.After(time.Now().Add(366 * 24 * time.Hour)) { jsonOut(w, 400, map[string]string{"error": "execute_at is more than one year away"}) return } payload := any(map[string]any{}) if len(in.Payload) > 0 { var x any if err := json.Unmarshal(in.Payload, &x); err != nil { jsonOut(w, 400, map[string]string{"error": "invalid payload"}) return } payload = x } a, err := s.store.ScheduleTaskAction(r.Context(), id, in.ActionType, payload, executeAt) if err != nil { jsonOut(w, 400, map[string]string{"error": err.Error()}) return } // "Run now" feels immediate in the admin UI while still going through the // same persisted action/audit path as scheduled changes. if !executeAt.After(time.Now().Add(1500 * time.Millisecond)) { s.runTaskAction(r.Context(), a) actions, _ := s.store.TaskActions(r.Context(), id, 1) if len(actions) > 0 { a = actions[0] } } jsonOut(w, 200, a) } func (s *Server) adminCancelAction(w http.ResponseWriter, r *http.Request) { if err := s.store.CancelTaskAction(r.Context(), chi.URLParam(r, "id")); err != nil { jsonOut(w, 400, map[string]string{"error": err.Error()}) return } jsonOut(w, 200, map[string]bool{"ok": true}) } func (s *Server) adminArtifactProviders(w http.ResponseWriter, r *http.Request) { cfg := s.settings.Get() anchorPath := filepath.Join(s.artifactDir, "_collection", "character_anchor.png") anchorReady := false if st, err := os.Stat(anchorPath); err == nil && st.Size() > 1024 { anchorReady = true } jsonOut(w, 200, map[string]any{ "current": cfg.ArtifactProvider, "preset": cfg.ArtifactPreset, "model": cfg.ArtifactModel, "character_anchor": anchorReady, "providers": map[string]bool{ "local": true, "openai": strings.TrimSpace(os.Getenv("OPENAI_API_KEY")) != "", "comfyui": strings.TrimSpace(os.Getenv("COMFYUI_URL")) != "" && strings.TrimSpace(os.Getenv("COMFYUI_WORKFLOW_PATH")) != "", "a1111": strings.TrimSpace(os.Getenv("A1111_URL")) != "", }, }) } func (s *Server) executeTaskAction(ctx context.Context, a data.TaskAction) error { switch a.ActionType { case "set_range_bits": var p struct { Bits int `json:"bits"` Mode string `json:"mode"` } if err := json.Unmarshal(a.Payload, &p); err != nil { return err } return s.store.SetTaskRangeBits(ctx, a.TaskID, p.Bits, p.Mode) case "set_intervals": var p struct { Server int `json:"server_min_interval_sec"` Client int `json:"client_submit_interval_sec"` } if err := json.Unmarshal(a.Payload, &p); err != nil { return err } return s.store.SetTaskIntervals(ctx, a.TaskID, p.Server, p.Client) case "clear_intervals": return s.store.ClearTaskIntervals(ctx, a.TaskID) case "pause": return s.store.SetTaskPaused(ctx, a.TaskID, true) case "resume": return s.store.SetTaskPaused(ctx, a.TaskID, false) case "reroll": return s.store.RerollTask(ctx, a.TaskID) case "close": return s.store.CloseTask(ctx, a.TaskID) case "regenerate_artifact": return s.store.QueueArtifact(ctx, a.TaskID) default: return fmt.Errorf("unsupported action %q", a.ActionType) } } func (s *Server) runTaskAction(ctx context.Context, a data.TaskAction) { if !s.store.StartTaskAction(ctx, a.ID) { return } err := s.executeTaskAction(ctx, a) s.store.FinishTaskAction(ctx, a.ID, err) if err != nil { log.Printf("task action %s (%s): %v", a.ID, a.ActionType, err) return } if a.ActionType == "close" { dataOut := map[string]string{"reason": "scheduled_close"} if successor, succErr := s.store.EnsureSuccessorTask(ctx, a.TaskID, s.settings.Get().TaskRangeBits); succErr == nil { dataOut["successor_task_id"] = successor.ID s.runtime.ReplaceTaskSelection(a.TaskID, successor.ID) } else { log.Printf("successor for %s: %v", a.TaskID, succErr) } _ = s.hub.Publish(ctx, wsx.Event{Type: "task_completed", TaskID: a.TaskID, Data: dataOut}) _ = s.store.EnsureActiveTasks(ctx, s.settings.Get().ActiveTaskCount, s.settings.Get().TaskRangeBits) return } _ = s.hub.Publish(ctx, wsx.Event{Type: "task_changed", TaskID: a.TaskID, Data: map[string]string{"action": a.ActionType}}) } func (s *Server) runDueActions(ctx context.Context) { actions, err := s.store.DueTaskActions(ctx, 20) if err != nil { log.Printf("task actions: %v", err) return } for _, a := range actions { s.runTaskAction(ctx, a) } } func (s *Server) adminCloseTask(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if err := s.store.CloseTask(r.Context(), id); err != nil { jsonOut(w, 400, map[string]string{"error": err.Error()}) return } dataOut := map[string]string{"reason": "closed_by_admin"} if successor, succErr := s.store.EnsureSuccessorTask(r.Context(), id, s.settings.Get().TaskRangeBits); succErr == nil { dataOut["successor_task_id"] = successor.ID s.runtime.ReplaceTaskSelection(id, successor.ID) } else { log.Printf("successor for %s: %v", id, succErr) } _ = s.hub.Publish(r.Context(), wsx.Event{Type: "task_completed", TaskID: id, Data: dataOut}) _ = s.store.EnsureActiveTasks(r.Context(), s.settings.Get().ActiveTaskCount, s.settings.Get().TaskRangeBits) jsonOut(w, 200, map[string]bool{"ok": true}) } func (s *Server) adminEnsure(w http.ResponseWriter, r *http.Request) { err := s.store.EnsureActiveTasks(r.Context(), s.settings.Get().ActiveTaskCount, s.settings.Get().TaskRangeBits) if err != nil { jsonOut(w, 500, map[string]string{"error": err.Error()}) return } jsonOut(w, 200, map[string]bool{"ok": true}) } func (s *Server) ws(w http.ResponseWriter, r *http.Request) { tok := r.URL.Query().Get("token") c, err := s.auth.Parse(tok) if err != nil || c.Role != "user" { http.Error(w, "unauthorized", 401) return } if !s.store.ClientExists(r.Context(), c.ClientID) { http.Error(w, "identity not registered", http.StatusUnauthorized) return } // WebSocket use counts as recent profile activity. This also makes a race // with the admin cleanup safe: a newly connecting identity no longer // matches an old last_seen cutoff. s.store.TouchClient(r.Context(), c.ClientID) t, err := s.store.TaskForClient(r.Context(), c.ClientID) if err != nil { http.Error(w, "no task", 503) return } s.runtime.SetTaskSelection(c.ClientID, t.ID) leaseID, err := s.runtime.AcquirePresence(c.ClientID, c.SessionID) if err != nil { http.Error(w, "identity already connected", 409) return } conn, err := s.upgrader.Upgrade(w, r, nil) if err != nil { s.runtime.ReleasePresence(c.ClientID, c.SessionID, leaseID) return } cl := wsx.NewClient(conn, t.ID, c.ClientID, false) s.hub.Add(cl) defer func() { s.hub.Remove(cl) s.runtime.ReleasePresence(c.ClientID, c.SessionID, leaseID) // Mark the disconnect time as last activity. A client that stayed online // for days therefore starts its inactivity window only after disconnect. s.store.TouchClient(context.Background(), c.ClientID) }() // A map point is durable only once per client/task. Subsequent losing guesses // stay in memory; improvements are checkpointed by the guess handler. if p, err := s.store.EnsurePoint(r.Context(), t.ID, c.ClientID); err == nil { p.Score = round(p.Score, s.settings.Get().PublicScorePrecision) s.hub.PublishPoint(t.ID, c.ClientID, p) } snap, _ := s.store.LoadGuessState(r.Context(), t.ID, c.ClientID) s.runtime.InitGuess(t, c.ClientID, rtx.GuessState{NextSeq: snap.NextSeq, LastGuess: snap.LastGuess, BestScore: snap.BestScore, GuessCount: snap.GuessCount}) maxNodes := s.settings.Get().DefaultMaxNodes if q, _ := strconv.Atoi(r.URL.Query().Get("max_nodes")); q > 0 { maxNodes = q } // Give the browser a bounded overscan set for local LOD, not the entire task. limit := maxNodes * 3 if limit < 300 { limit = 300 } if limit > 10000 { limit = 10000 } ps, _ := s.store.PointsForClient(r.Context(), t.ID, c.ClientID, limit) for i := range ps { ps[i].Score = round(ps[i].Score, s.settings.Get().PublicScorePrecision) } cl.Enqueue(wsx.Event{Type: "snapshot", TaskID: t.ID, Data: ps}) conn.SetReadLimit(4 << 10) _ = 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: // Real WebSocket control ping. The browser answers with pong // automatically and the PongHandler extends the 90s read deadline. if err := cl.Ping(); err != nil { return } } } } func (s *Server) Scheduler(ctx context.Context) { t := time.NewTicker(time.Second) defer t.Stop() maintenance := 0 for { select { case <-ctx.Done(): return case <-t.C: s.runDueActions(ctx) maintenance++ if maintenance%5 == 0 { if err := s.store.EnsureActiveTasks(ctx, s.settings.Get().ActiveTaskCount, s.settings.Get().TaskRangeBits); err != nil { log.Printf("scheduler: %v", err) } } } } }