"use client"; import { Chip, Skeleton, Stack, Typography } from "@mui/material"; import { useQuery } from "@tanstack/react-query"; import { fetchGatewayStatus, fetchNodeInfo } from "../../app/api"; import type { LastProbeResult, NodeDescription } from "../../app/api/types"; import ExplorerCard from "../cards/ExplorerCard"; import ExplorerListItem from "../list/ListItem"; import StarRating from "../starRating/StarRating"; interface INodeRoleCardProps { id: number; // Node ID } type NodeDescriptionNotNull = NonNullable; type DeclaredRoleKey = keyof NodeDescriptionNotNull["declared_role"]; type RoleString = "Entry Node" | "Exit IPR Node" | "Exit NR Node" | "Mix Node"; const roleMapping: Record = { entry: "Entry Node", exit_ipr: "Exit IPR Node", exit_nr: "Exit NR Node", mixnode: "Mix Node", }; const getNodeRoles = ( declaredRoles: NodeDescriptionNotNull["declared_role"], ): RoleString[] => { return Object.entries(declaredRoles) .filter(([, isActive]) => isActive) .map(([role]) => roleMapping[role as DeclaredRoleKey]); }; const calculateQualityOfServiceStars = (quality: number): number => { switch (true) { case quality < 0.3: return 1; case quality < 0.5: return 2; case quality < 0.7: return 3; default: return 4; } }; function calculateConfigScoreStars(probeResult: LastProbeResult): number { const { as_entry, as_exit } = probeResult.outcome; if (as_entry && as_exit) { const allResults = [ as_entry.can_connect, as_entry.can_route, as_exit.can_connect, as_exit.can_route_ip_external_v4, as_exit.can_route_ip_external_v6, as_exit.can_route_ip_v4, as_exit.can_route_ip_v6, ]; const combinedScore = allResults.filter(Boolean).length; switch (combinedScore) { case 7: return 4; case 6: return 3; case 5: return 2; default: return 1; } } if (as_entry) { const entryScore = [as_entry.can_connect, as_entry.can_route].filter( Boolean, ).length; return entryScore === 2 ? 4 : entryScore === 1 ? 2 : 1; } if (as_exit) { const exitScore = [ as_exit.can_connect, as_exit.can_route_ip_external_v4, as_exit.can_route_ip_external_v6, as_exit.can_route_ip_v4, as_exit.can_route_ip_v6, ].filter(Boolean).length; switch (exitScore) { case 5: return 4; case 4: return 3; case 3: return 2; default: return 1; } } return 0; } function calculateWireguardPerformance(probeResult: LastProbeResult): number { const { wg, as_exit } = probeResult.outcome; if (!wg) return 1; const pingPerformance = (wg.ping_hosts_performance_v4 + wg.ping_hosts_performance_v6 + wg.ping_ips_performance_v4 + wg.ping_ips_performance_v6) / 4; switch (true) { case wg.can_register && wg.can_handshake_v4 && wg.can_handshake_v6 && wg.can_resolve_dns_v4 && wg.can_resolve_dns_v6 && pingPerformance > 0.75: return 4; case wg.can_register && wg.can_handshake_v4 && wg.can_handshake_v6 && wg.can_resolve_dns_v4 && wg.can_resolve_dns_v6 && pingPerformance <= 0.75: return 3; case wg.can_register && (!wg.can_handshake_v4 || !wg.can_handshake_v6): return 2; case as_exit && (!as_exit.can_route_ip_v4 || !as_exit.can_route_ip_v6): return 1; default: return 1; } } export const NodeRoleCard = ({ id }: INodeRoleCardProps) => { // Fetch node info const { data: nodeInfo, isLoading, isError, } = useQuery({ queryKey: ["nodeInfo", id], queryFn: () => fetchNodeInfo(id), }); // Extract node roles once `nodeInfo` is available const nodeRoles = nodeInfo ? getNodeRoles(nodeInfo.description.declared_role) : []; // Define whether to fetch gateway status const shouldFetchGatewayStatus = nodeRoles.some((role) => ["Entry Node", "Exit IPR Node", "Exit NR Node"].includes(role), ); // Fetch gateway status only if `shouldFetchGatewayStatus` is true const { data: gatewayStatus } = useQuery({ queryKey: ["gatewayStatus", nodeInfo?.identity_key], queryFn: () => fetchGatewayStatus(nodeInfo?.identity_key || ""), enabled: !!nodeInfo?.identity_key && shouldFetchGatewayStatus, // ✅ Only fetch if needed }); if (isLoading) { return ( ); } if (isError || !nodeInfo) { return ( Failed to load node data. ); } const NodeRoles = nodeRoles.map((role) => ( )); const qualityOfServiceStars = nodeInfo?.uptime ? calculateQualityOfServiceStars(nodeInfo.uptime) : gatewayStatus ? calculateQualityOfServiceStars(gatewayStatus.performance) : 1; const configScoreStars = gatewayStatus ? calculateConfigScoreStars(gatewayStatus.last_probe_result) : 0; const wireguardPerformanceStars = gatewayStatus ? calculateWireguardPerformance(gatewayStatus.last_probe_result) : 0; const nodeIsMixNodeOnly = NodeRoles.length === 1 && nodeRoles[0] === "Mix Node"; return ( {NodeRoles} } /> } /> {!nodeIsMixNodeOnly && gatewayStatus && ( } /> )} {!nodeIsMixNodeOnly && gatewayStatus && ( } /> )} ); };