import { useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { Copy, Cpu, HardDrive, MemoryStick, Plus, Server, Settings, Signal, Users, } from "lucide-react"; import { useDeleteNode, useNodeDebugBundle, useNodes } from "@/api/hooks"; import { useRestartCore, useStopCore, useUpdateGeo } from "@/api/policy-hooks"; import type { Node } from "@/api/types"; import { Badge, Button } from "@/components/ui"; import { CreateNodeModal, diagColor, diagLabel, phaseLabel } from "@/components/CreateNodeModal"; import { EditNodeModal } from "@/components/EditNodeModal"; import { NodeInboundsModal } from "@/components/NodeInboundsModal"; import { NodeLogsModal } from "@/components/NodeLogsModal"; import { CoreBadge, GlassCard, StatusBadge } from "@/components/vortexui"; import { useConfirm } from "@/components/confirm"; import { useToast } from "@/components/toast"; import { useI18n } from "@/i18n/i18n"; import { useTitle } from "@/lib/useTitle"; import { useAuth } from "@/auth/auth"; import { EmptyState } from "@/components/EmptyState"; import { cn } from "@/lib/utils"; type FleetFilter = "" | "online" | "warning" | "offline"; const FLEET_FILTERS: { value: FleetFilter; labelKey: "users.filterAll" | "nodes.filterOnline" | "nodes.filterWarning" | "nodes.filterOffline" }[] = [ { value: "", labelKey: "users.filterAll" }, { value: "online", labelKey: "nodes.filterOnline" }, { value: "warning", labelKey: "nodes.filterWarning" }, { value: "offline", labelKey: "nodes.filterOffline" }, ]; function timeAgoShort(iso: string | null): string { if (!iso) return "—"; const diff = Date.now() - new Date(iso).getTime(); const sec = Math.floor(diff / 1000); if (sec < 0) return "now"; if (sec < 60) return `${sec}s`; const min = Math.floor(sec / 60); if (min < 60) return `${min}m`; const hrs = Math.floor(min / 60); if (hrs < 24) return `${hrs}h`; return `${Math.floor(hrs / 24)}d`; } function isNodeOnline(n: Node): boolean { const lastSeenFresh = n.last_seen != null && Date.now() - new Date(n.last_seen).getTime() < 90_000; return lastSeenFresh && n.health.core_running; } function nodeDisplayStatus(n: Node): "active" | "warning" | "inactive" { if (!isNodeOnline(n)) return "inactive"; const load = Math.max(n.health.cpu_percent ?? 0, n.health.mem_percent ?? 0); if (load > 75) return "warning"; return "active"; } function statusLabel(status: "active" | "warning" | "inactive", online: boolean): string { if (!online) return "OFFLINE"; if (status === "warning") return "WARNING"; return "ONLINE"; } function matchesFleetFilter(n: Node, filter: FleetFilter): boolean { const st = nodeDisplayStatus(n); switch (filter) { case "online": return st === "active"; case "warning": return st === "warning"; case "offline": return st === "inactive"; default: return true; } } /** Renders a 2-letter ISO country code as a flag emoji via regional indicator * symbols; falls back to a globe icon when no code is set. */ function flagEmoji(code?: string): string { if (!code || code.length !== 2) return "🌐"; const cc = code.toUpperCase(); const points = [...cc].map((c) => 127397 + c.charCodeAt(0)); return String.fromCodePoint(...points); } function metricTone(v: number): { bar: string; text: string } { if (v > 85) return { bar: "bg-danger", text: "text-danger" }; if (v > 65) return { bar: "bg-warning", text: "text-warning" }; return { bar: "bg-success", text: "text-success" }; } function MetricBar({ icon, label, value }: { icon: React.ReactNode; label: string; value: number }) { const v = Math.min(100, Math.max(0, value || 0)); const tone = metricTone(v); return (
{icon} {label}
{v.toFixed(0)}%
); } export function Nodes() { useTitle("Nodes"); const { can } = useAuth(); const canManage = can("node:write"); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [fleetFilter, setFleetFilter] = useState(""); const [menuNodeId, setMenuNodeId] = useState(null); const menuRef = useRef(null); useEffect(() => { if (searchParams.get("tab") === "inbounds") { navigate("/inbounds", { replace: true }); } }, [searchParams, navigate]); const { data, isLoading } = useNodes(); const del = useDeleteNode(); const restart = useRestartCore(); const stop = useStopCore(); const updateGeo = useUpdateGeo(); const debug = useNodeDebugBundle(); const confirm = useConfirm(); const toast = useToast(); const { t } = useI18n(); const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [managing, setManaging] = useState(null); const [logging, setLogging] = useState(null); const nodes = data?.nodes ?? []; const filteredNodes = useMemo( () => nodes.filter((n) => matchesFleetFilter(n, fleetFilter)), [nodes, fleetFilter], ); const summary = useMemo(() => { let active = 0; let warning = 0; let offline = 0; let users = 0; for (const n of nodes) { const st = nodeDisplayStatus(n); if (st === "active") active++; else if (st === "warning") warning++; else offline++; users += n.users_count ?? 0; } return { active, warning, offline, users }; }, [nodes]); useEffect(() => { function onDocClick(e: MouseEvent) { if (menuRef.current && !menuRef.current.contains(e.target as globalThis.Node)) { setMenuNodeId(null); } } document.addEventListener("mousedown", onDocClick); return () => document.removeEventListener("mousedown", onDocClick); }, []); async function remove(n: Node) { setMenuNodeId(null); if ( await confirm({ title: `Delete node ${n.name}?`, message: "Its inbounds are removed and the agent is deregistered.", confirmLabel: "Delete", destructive: true, }) ) { del .mutateAsync(n.id) .then(() => toast.success(`Deleted ${n.name}`)) .catch(() => toast.error("Delete failed")); } } async function doStop(n: Node) { setMenuNodeId(null); if ( await confirm({ title: `Stop core on ${n.name}?`, message: "The proxy engine will shut down. Users on this node will disconnect.", confirmLabel: "Stop", destructive: true, }) ) { stop .mutateAsync(n.id) .then(() => toast.success("Core stopped")) .catch(() => toast.error("Stop failed")); } } async function doUpdateGeo(n: Node) { setMenuNodeId(null); if ( await confirm({ title: `Update geo data on ${n.name}?`, message: "Downloads the latest Iran geoip/geosite databases and restarts the core (brief reconnect).", confirmLabel: "Update", }) ) { toast.info("Updating geo data…"); updateGeo .mutateAsync(n.id) .then((r) => toast.success(`Geo updated (${Math.round((r.geoip_bytes + r.geosite_bytes) / 1024)} KB)`), ) .catch(() => toast.error("Geo update failed")); } } async function copyDebug(n: Node) { setMenuNodeId(null); try { const res = await debug.mutateAsync(n.id); await navigator.clipboard.writeText(res.debug_text); toast.success("Debug bundle copied"); } catch { toast.error("Could not copy debug bundle"); } } return (
setCreateOpen(false)} /> setEditing(null)} /> setManaging(null)} /> setLogging(null)} />

{t("nodes.managementTitle")}

{nodes.length} {t("nodes.registeredNodes")}

Fleet monitoring & management

{canManage && ( )}
{/* Fleet summary — active / warning / offline / total users at a glance */}

{summary.active}

{t("nodes.filterOnline")}

0 ? (summary.active / nodes.length) * 100 : 0}%` }} />

{summary.warning}

{t("nodes.filterWarning")}

0 ? (summary.warning / nodes.length) * 100 : 0}%` }} />

{summary.offline}

{t("nodes.filterOffline")}

0 ? (summary.offline / nodes.length) * 100 : 0}%` }} />

{summary.users}

{t("nodes.users")}

0 ? (summary.users / Math.max(...nodes.map(n => n.users_count ?? 0), 1)) * 100 : 0}%` }} />
{FLEET_FILTERS.map((f) => ( ))}
{isLoading && (
{Array.from({ length: 3 }).map((_, i) => (
))}
)} {!isLoading && filteredNodes.length === 0 && ( setCreateOpen(true) } : undefined} /> )} {!isLoading && filteredNodes.length > 0 && (
{filteredNodes.map((n) => { const online = isNodeOnline(n); const status = nodeDisplayStatus(n); const location = n.location || n.region || n.name; const isMenuOpen = menuNodeId === n.id; return (
{flagEmoji(n.country_code)}

{n.name}

{location}

{isMenuOpen && (
{ setManaging(n); setMenuNodeId(null); }}> {t("nodes.inbounds")} { setLogging(n); setMenuNodeId(null); }}> Logs {!online && ( copyDebug(n)}> Debug )} {canManage && ( <> {online ? ( doStop(n)}> Stop ) : ( { setMenuNodeId(null); restart .mutateAsync(n.id) .then(() => toast.success("Core started")) .catch(() => toast.error("Start failed")); }} > Start )} { setMenuNodeId(null); restart .mutateAsync(n.id) .then(() => toast.success("Core restarted")) .catch(() => toast.error("Restart failed")); }} > Restart doUpdateGeo(n)}>Update Geo { setEditing(n); setMenuNodeId(null); }}> {t("common.edit")} remove(n)}> {t("common.delete")} )}
)}
{(n.core_version || n.enrollment_phase) && (
{n.enrollment_phase && n.enrollment_phase !== "synced" && ( {phaseLabel(n.enrollment_phase)} )} {!online && n.diagnostics && n.diagnostics.code !== "ok" && ( {diagLabel(n.diagnostics.code)} )}
)}
} label="CPU" value={n.health.cpu_percent} /> } label="RAM" value={n.health.mem_percent} /> } label="Disk" value={n.health.disk_percent} />

Latency

{n.ping_ms && n.ping_ms > 0 ? `${n.ping_ms}ms` : "—"}

{t("nodes.users")}

{n.users_count ?? 0}

{t("nodes.connections")}

{n.health.connections}

{t("nodes.core")}

{n.address} {online ? "· live" : `${t("nodes.lastSeen")} ${timeAgoShort(n.last_seen)}`}
); })}
)} {!isLoading && filteredNodes.length > 0 && (

{t("users.showingOf") .replace("{count}", String(filteredNodes.length)) .replace("{total}", String(nodes.length))}

)}
); } function MenuAction({ children, onClick, className, }: { children: React.ReactNode; onClick: () => void; className?: string; }) { return ( ); }