RC-11
release-tag / release-image (push) Successful in 4m46s

This commit is contained in:
2026-08-12 22:26:00 +02:00
parent d76f1c3d25
commit f2da96ec24
16 changed files with 672 additions and 117 deletions
+95 -3
View File
@@ -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)