import { useState, useEffect } from "react"; import { Shield, Server, Activity, Plus, Power, RefreshCw, Trash2, } from "lucide-react"; import { apiFetch } from "./api"; interface VpnNode { id: string; name: string; country_code: string; ip_address: string; port: number; status: string; current_load: number; } export default function AdminDashboard() { const [nodes, setNodes] = useState([]); const [loading, setLoading] = useState(true); const [busyNodeId, setBusyNodeId] = useState(null); const [name, setName] = useState(""); const [ip, setIp] = useState(""); const [code, setCode] = useState("de"); const [sshPassword, setSshPassword] = useState(""); const [sniDomain, setSniDomain] = useState(""); const [publicKey, setPublicKey] = useState(""); const fetchNodes = async () => { try { // /admin/nodes (не публичный /nodes) — админке нужно видеть все ноды, // включая "provisioning"/"offline", а не только "online". const res = await apiFetch("/admin/nodes"); if (!res.ok) throw new Error("API Error"); const data = await res.json(); if (Array.isArray(data)) { setNodes(data); } } catch (e) { console.error(e); } finally { setLoading(false); } }; useEffect(() => { let isMounted = true; const loadData = async () => { if (isMounted) { await fetchNodes(); } }; loadData(); return () => { isMounted = false; }; }, []); const handleAddNode = async (e: React.FormEvent) => { e.preventDefault(); try { const res = await apiFetch("/admin/nodes", { method: "POST", body: JSON.stringify({ name, country_code: code, ip_address: ip, port: 443, ssh_password: sshPassword, public_key: publicKey || null, sni_domain: sniDomain || null, }), }); if (res.ok) { setName(""); setIp(""); setSshPassword(""); setSniDomain(""); setPublicKey(""); fetchNodes(); } else { console.error("Failed to add node:", await res.text()); } } catch (e) { console.error(e); } }; const handleRestartNode = async (node: VpnNode) => { if ( !window.confirm( `Force restart ${node.name}? Активные соединения на этой ноде оборвутся.`, ) ) { return; } setBusyNodeId(node.id); try { const res = await apiFetch(`/admin/nodes/${node.id}/restart`, { method: "POST", }); if (!res.ok) console.error("Failed to restart node:", await res.text()); await fetchNodes(); } catch (e) { console.error(e); } finally { setBusyNodeId(null); } }; const handleDeleteNode = async (node: VpnNode) => { if ( !window.confirm( `Удалить ноду ${node.name} безвозвратно? Это действие нельзя отменить.`, ) ) { return; } setBusyNodeId(node.id); try { const res = await apiFetch(`/admin/nodes/${node.id}`, { method: "DELETE", }); if (res.ok) { setNodes((prev) => prev.filter((n) => n.id !== node.id)); } else { console.error("Failed to delete node:", await res.text()); } } catch (e) { console.error(e); } finally { setBusyNodeId(null); } }; return (

ORCHESTRATOR_NEXUS

Netrunner Global Infrastructure Control

Deploy New Node

setName(e.target.value)} placeholder="e.g. Frankfurt Alpha" className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-sm focus:border-secondary outline-none transition-colors mt-1" />
setIp(e.target.value)} placeholder="147.x.x.x" className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-sm focus:border-secondary outline-none transition-colors mt-1 font-mono" />
setCode(e.target.value)} placeholder="de" className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-sm focus:border-secondary outline-none transition-colors mt-1 uppercase" />
setSshPassword(e.target.value)} placeholder="••••••••" className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-sm focus:border-secondary outline-none transition-colors mt-1 font-mono" />
setSniDomain(e.target.value)} placeholder="cloudflare.com" className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-sm focus:border-secondary outline-none transition-colors mt-1 font-mono" />
setPublicKey(e.target.value)} placeholder="node identity public key" className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-sm focus:border-secondary outline-none transition-colors mt-1 font-mono" />

Active Grid

{nodes.map((node) => (

{node.name}

{node.country_code}

{node.ip_address}:{node.port}

Load

))} {nodes.length === 0 && !loading && (
No active nodes in the grid
)}
); }