@@ -339,9 +339,12 @@ func (s *Server) Routes() http.Handler {
|
||||
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/clients", s.adminClients)
|
||||
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}/artifact/transfer", s.adminArtifactTransfer)
|
||||
r.Post("/api/admin/artifacts/drop", s.adminArtifactDrop)
|
||||
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)
|
||||
@@ -563,6 +566,7 @@ func taskDTO(t data.Task, next int64, sm settings.Runtime) map[string]any {
|
||||
"display_name": t.DisplayName,
|
||||
"description": t.Description,
|
||||
"parent_task_id": t.ParentTaskID,
|
||||
"retire_after_completion": t.RetireAfterCompletion,
|
||||
"created_at": t.CreatedAt,
|
||||
}
|
||||
}
|
||||
@@ -888,8 +892,10 @@ func (s *Server) guess(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
} else if !errors.Is(succErr, data.ErrTaskRetiring) {
|
||||
log.Printf("successor for %s: %v", id, succErr)
|
||||
} else {
|
||||
dataOut["retired"] = "true"
|
||||
}
|
||||
_ = 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)
|
||||
@@ -1490,6 +1496,83 @@ func (s *Server) adminTasks(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOut(w, 200, ts)
|
||||
}
|
||||
|
||||
func (s *Server) adminClients(w http.ResponseWriter, r *http.Request) {
|
||||
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if limit < 1 || limit > 500 {
|
||||
limit = 200
|
||||
}
|
||||
rows, err := s.store.DB.QueryContext(r.Context(), `SELECT c.id,c.last_seen,
|
||||
(SELECT count(*) FROM tasks t WHERE t.winner_client_id=c.id AND t.status='completed') AS wins,
|
||||
(SELECT count(*) FROM tasks t WHERE COALESCE(t.artifact_owner_client_id,t.winner_client_id)=c.id AND t.status='completed' AND t.artifact_status='ready') AS nft_count
|
||||
FROM clients c WHERE (?='' OR lower(c.id) LIKE '%'||lower(?)||'%') ORDER BY c.last_seen DESC LIMIT ?`, q, q, limit)
|
||||
if err != nil {
|
||||
jsonOut(w, 500, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]map[string]any, 0)
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var lastSeen int64
|
||||
var wins, nfts int
|
||||
if err := rows.Scan(&id, &lastSeen, &wins, &nfts); err != nil {
|
||||
jsonOut(w, 500, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
out = append(out, map[string]any{"client_id": id, "last_seen": lastSeen, "wins": wins, "nft_count": nfts})
|
||||
}
|
||||
jsonOut(w, 200, out)
|
||||
}
|
||||
|
||||
func (s *Server) adminArtifactTransfer(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
TargetClientID string `json:"target_client_id"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if err := decode(r, &in); err != nil || strings.TrimSpace(in.TargetClientID) == "" {
|
||||
jsonOut(w, 400, map[string]string{"error": "target_client_id required"})
|
||||
return
|
||||
}
|
||||
taskID := chi.URLParam(r, "id")
|
||||
from, err := s.store.TransferArtifact(r.Context(), taskID, in.TargetClientID, in.Reason)
|
||||
if err != nil {
|
||||
jsonOut(w, 400, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_ = s.hub.Publish(r.Context(), wsx.Event{Type: "task_changed", TaskID: taskID, Data: map[string]string{"action": "artifact_transfer"}})
|
||||
jsonOut(w, 200, map[string]any{"ok": true, "from_client_id": from, "to_client_id": strings.TrimSpace(in.TargetClientID)})
|
||||
}
|
||||
|
||||
func (s *Server) adminArtifactDrop(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
TargetClientID string `json:"target_client_id"`
|
||||
TemplateTaskID string `json:"template_task_id"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
if err := decode(r, &in); err != nil || strings.TrimSpace(in.TargetClientID) == "" {
|
||||
jsonOut(w, 400, map[string]string{"error": "target_client_id required"})
|
||||
return
|
||||
}
|
||||
if in.Count == 0 {
|
||||
in.Count = 1
|
||||
}
|
||||
if in.Count < 1 || in.Count > 20 {
|
||||
jsonOut(w, 400, map[string]string{"error": "count must be 1..20"})
|
||||
return
|
||||
}
|
||||
created := make([]string, 0, in.Count)
|
||||
for i := 0; i < in.Count; i++ {
|
||||
t, err := s.store.CreateAdminArtifactDrop(r.Context(), in.TargetClientID, in.TemplateTaskID, s.settings.Get().TaskRangeBits)
|
||||
if err != nil {
|
||||
jsonOut(w, 400, map[string]any{"error": err.Error(), "created_task_ids": created})
|
||||
return
|
||||
}
|
||||
created = append(created, t.ID)
|
||||
}
|
||||
jsonOut(w, 201, map[string]any{"ok": true, "created_task_ids": created, "artifact_status": "pending"})
|
||||
}
|
||||
|
||||
func (s *Server) adminTaskConfigPut(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
var in struct {
|
||||
@@ -1497,6 +1580,7 @@ func (s *Server) adminTaskConfigPut(w http.ResponseWriter, r *http.Request) {
|
||||
Description string `json:"description"`
|
||||
NFTPromptInstructions string `json:"nft_prompt_instructions"`
|
||||
NFTNegativePrompt string `json:"nft_negative_prompt"`
|
||||
RetireAfterCompletion bool `json:"retire_after_completion"`
|
||||
}
|
||||
if err := decode(r, &in); err != nil {
|
||||
jsonOut(w, 400, map[string]string{"error": "bad json: " + err.Error()})
|
||||
@@ -1506,6 +1590,10 @@ func (s *Server) adminTaskConfigPut(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOut(w, 400, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := s.store.SetTaskRetireAfterCompletion(r.Context(), id, in.RetireAfterCompletion); 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})
|
||||
}
|
||||
@@ -1703,8 +1791,10 @@ func (s *Server) runTaskAction(ctx context.Context, a data.TaskAction) {
|
||||
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 {
|
||||
} else if !errors.Is(succErr, data.ErrTaskRetiring) {
|
||||
log.Printf("successor for %s: %v", a.TaskID, succErr)
|
||||
} else {
|
||||
dataOut["retired"] = "true"
|
||||
}
|
||||
_ = 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)
|
||||
@@ -1734,8 +1824,10 @@ func (s *Server) adminCloseTask(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
} else if !errors.Is(succErr, data.ErrTaskRetiring) {
|
||||
log.Printf("successor for %s: %v", id, succErr)
|
||||
} else {
|
||||
dataOut["retired"] = "true"
|
||||
}
|
||||
_ = 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)
|
||||
|
||||
Reference in New Issue
Block a user