Add fleet groups, agent screenshots, deploy guards, and Crucible polish.

This commit is contained in:
AetherForge
2026-06-02 20:51:52 -07:00
parent 01d76b3730
commit 41b5ec7a88
66 changed files with 1773 additions and 388 deletions

View File

@@ -310,8 +310,22 @@ func (f *FleetHandler) PostAgentCommand(w http.ResponseWriter, r *http.Request)
}
f.ws.BroadcastAgentCommand(req.Action, args)
} else {
if !f.ws.isAgentConnected(id) {
writeJSON(w, map[string]interface{}{
"success": false,
"error": "agent not connected",
"agent_id": id,
"action": req.Action,
})
return
}
if err := f.ws.SendAgentCommand(id, req.Action, args); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
writeJSON(w, map[string]interface{}{
"success": false,
"error": err.Error(),
"agent_id": id,
"action": req.Action,
})
return
}
}

View File

@@ -588,8 +588,15 @@ func TestFleetPostAgentCommandErrors(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/agents/offline-agent/command",
strings.NewReader(`{"action":"pause"}`))
fleetChiRoute(http.MethodPost, "/agents/{id}/command", fh.PostAgentCommand).ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d body %s", rec.Code, rec.Body.String())
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body %s", rec.Code, rec.Body.String())
}
var body map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["success"] != false || body["error"] != "agent not connected" {
t.Fatalf("unexpected body: %v", body)
}
})

View File

@@ -337,8 +337,17 @@ func TestIntegrationAgentCommandOffline(t *testing.T) {
router, _, _, _ := newTestRouter(t)
rec := serveAuthed(t, router, http.MethodPost, "/api/v1/agents/offline-agent/command",
[]byte(`{"action":"pause"}`))
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400 for offline agent, got %d body=%s", rec.Code, rec.Body.String())
// Command for a non-connected agent returns 200 with success:false (not a 4xx),
// so the caller can inspect the error without tripping HTTP error handling.
if rec.Code != http.StatusOK {
t.Fatalf("expected 200 for offline agent, got %d body=%s", rec.Code, rec.Body.String())
}
var body map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["success"] != false {
t.Fatalf("expected success=false, got %v", body)
}
}

View File

@@ -47,8 +47,8 @@ func checkDashboardWSToken(r *http.Request) bool {
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
ReadBufferSize: 512 * 1024,
WriteBufferSize: 512 * 1024,
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins for local use
},
@@ -246,6 +246,17 @@ func (h *WSHub) isAgentConnected(agentID string) bool {
return ok
}
// writeAgentJSON sends a message to a connected agent using the per-connection
// write mutex. All post-auth outbound JSON must use this — never conn.WriteJSON
// from the read loop, or commands and new_job messages can corrupt each other.
func (h *WSHub) writeAgentJSON(agentID string, msg Message) error {
ac := h.getAgentConn(agentID)
if ac == nil {
return fmt.Errorf("agent %s not connected", agentID)
}
return ac.SendJSON(msg)
}
func (h *WSHub) SetPoolManager(manager *pool.Manager, defaultCfg pool.Config) {
h.mu.Lock()
h.poolManager = manager
@@ -826,12 +837,12 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
if proxy != nil {
job := proxy.GetCurrentJob()
if job != nil {
conn.WriteJSON(Message{Type: "new_job", Payload: mustMarshal(job)})
_ = h.writeAgentJSON(agentID, Message{Type: "new_job", Payload: mustMarshal(job)})
} else {
conn.WriteJSON(Message{Type: "new_job", Payload: mustMarshal(map[string]string{"error": "no job available — pool connecting"})})
_ = h.writeAgentJSON(agentID, Message{Type: "new_job", Payload: mustMarshal(map[string]string{"error": "no job available — pool connecting"})})
}
} else {
conn.WriteJSON(Message{Type: "new_job", Payload: mustMarshal(map[string]string{"error": "pool connecting — retry shortly"})})
_ = h.writeAgentJSON(agentID, Message{Type: "new_job", Payload: mustMarshal(map[string]string{"error": "pool connecting — retry shortly"})})
}
case "log_tail":
@@ -899,6 +910,11 @@ func (h *WSHub) HandleDashboardWS(w http.ResponseWriter, r *http.Request) {
// Send initial data
agents, _ := h.db.ListAgents()
h.enrichAgentsCapabilities(agents)
for _, a := range agents {
if a != nil && h.isAgentConnected(a.ID) {
a.Status = "online"
}
}
stats, _ := h.db.GetFleetStats()
_ = dc.WriteJSON(Message{Type: "init", Payload: mustMarshal(map[string]interface{}{

View File

@@ -46,8 +46,9 @@ func TestShortAgentID(t *testing.T) {
}
func TestAgentDisplayNameFallback(t *testing.T) {
// No hostname, no worker name — falls back to "agent-<shortID>"
name := agentDisplayName("", "", "", "12345678-abcd")
if name != "12345678" {
t.Fatalf("expected id prefix, got %q", name)
if name != "agent-12345678" {
t.Fatalf("expected agent-12345678, got %q", name)
}
}