import React from 'react'; import { Activity, ArrowDownToLine, ArrowUpFromLine, Cpu, HardDrive, MemoryStick } from 'lucide-react'; import { useI18n } from '../../application/i18n/I18nProvider'; import { cn } from '../../lib/utils'; import { HoverCard, HoverCardContent, HoverCardTrigger } from '../ui/hover-card'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; import { formatNetSpeed } from './terminalHelpers'; import { useServerStats } from '../../application/state/useServerStats'; import { formatDiskCapacityRange, resolveTerminalDiskSummary } from './serverStatsFormat'; interface TerminalServerStatsProps { sessionId: string; enabled: boolean; refreshInterval: number; isSupportedOs: boolean; isConnected: boolean; } const formatLatency = (latencyMs: number | null): string => ( typeof latencyMs === 'number' && Number.isFinite(latencyMs) ? `${latencyMs}ms` : '--ms' ); /** * Self-contained server-stats (CPU / Memory / Disk / Network) indicator. * * Owns the `useServerStats` polling itself so the periodic (~5s) stats refresh * only re-renders this small widget — previously the hook lived at the top of * and `serverStats` was threaded through the giant TerminalView ctx, * so every refresh re-rendered the whole terminal subtree (~45ms each, even * while idle). */ export const TerminalServerStats: React.FC = ({ sessionId, enabled, refreshInterval, isSupportedOs, isConnected, }) => { const { t } = useI18n(); const { stats: serverStats } = useServerStats({ sessionId, enabled, refreshInterval, isSupportedOs, isConnected, }); const hasNetworkDetails = serverStats.netInterfaces.length > 0; const hasLatency = serverStats.latencyMs !== null; const diskSummary = resolveTerminalDiskSummary(serverStats); if (!enabled || !isConnected || !serverStats.lastUpdated) return null; return (
{/* CPU with HoverCard for per-core details */}
{t("terminal.serverStats.cpuCores")}
{serverStats.cpuPerCore.length > 0 ? (
{serverStats.cpuPerCore.map((usage, index) => (
Core {index}
= 90 ? "bg-red-500" : usage >= 70 ? "bg-amber-500" : "bg-emerald-500" )} style={{ width: `${usage}%` }} />
= 90 ? "text-red-400" : usage >= 70 ? "text-amber-400" : "text-emerald-400" )}> {usage}%
))}
) : serverStats.cpu !== null ? (
= 90 ? "bg-red-500" : serverStats.cpu >= 70 ? "bg-amber-500" : "bg-emerald-500" )} style={{ width: `${serverStats.cpu}%` }} />
= 90 ? "text-red-400" : serverStats.cpu >= 70 ? "text-amber-400" : "text-emerald-400" )}> {serverStats.cpu}% · {serverStats.cpuCores ?? '?'} cores
) : (
{t("terminal.serverStats.noData")}
)}
{/* Memory with HoverCard for htop-style bar and top processes */}
{t("terminal.serverStats.memoryDetails")}
{/* htop-style memory bar */} {serverStats.memTotal !== null && (
{/* Used (green) — exact value shown in legend below */} {serverStats.memUsed !== null && serverStats.memUsed > 0 && (
)} {/* Buffers (blue) */} {serverStats.memBuffers !== null && serverStats.memBuffers > 0 && (
)} {/* Cached (amber/orange) */} {serverStats.memCached !== null && serverStats.memCached > 0 && (
)}
{/* Legend */}
{t("terminal.serverStats.memUsed")}: {serverStats.memUsed !== null ? `${(serverStats.memUsed / 1024).toFixed(1)}G` : '--'}
{t("terminal.serverStats.memBuffers")}: {serverStats.memBuffers !== null ? `${(serverStats.memBuffers / 1024).toFixed(1)}G` : '--'}
{t("terminal.serverStats.memCached")}: {serverStats.memCached !== null ? `${(serverStats.memCached / 1024).toFixed(1)}G` : '--'}
{t("terminal.serverStats.memFree")}: {serverStats.memFree !== null ? `${(serverStats.memFree / 1024).toFixed(1)}G` : '--'}
)} {/* Swap bar */} {serverStats.swapTotal !== null && serverStats.swapTotal > 0 && (
{t("terminal.serverStats.swap")}
{serverStats.swapUsed !== null && serverStats.swapUsed > 0 && (
)}
{t("terminal.serverStats.swapUsed")}: {serverStats.swapUsed !== null ? `${(serverStats.swapUsed / 1024).toFixed(1)}G` : '--'}
{t("terminal.serverStats.swapFree")}: {serverStats.swapTotal !== null && serverStats.swapUsed !== null ? `${((serverStats.swapTotal - serverStats.swapUsed) / 1024).toFixed(1)}G` : '--'}
{t("terminal.serverStats.swapTotal")}: {`${(serverStats.swapTotal / 1024).toFixed(1)}G`}
)} {/* Top 10 processes */} {serverStats.topProcesses.length > 0 && (
{t("terminal.serverStats.topProcesses")}
{serverStats.topProcesses.map((proc, index) => (
{proc.memPercent.toFixed(1)}%
{proc.command.split('/').pop()?.split(' ')[0] || proc.command} {proc.command}
))}
)}
{/* Disk - with HoverCard for disk details */}
{t("terminal.serverStats.diskDetails")}
{serverStats.disks.length > 0 ? (
{serverStats.disks.map((disk, index) => (
{disk.mountPoint} {disk.mountPoint} = 90 ? "text-red-400" : disk.percent >= 80 ? "text-amber-400" : "text-emerald-400" )}> {formatDiskCapacityRange(disk.used, disk.total)} ({disk.percent}%)
= 90 ? "bg-red-500" : disk.percent >= 80 ? "bg-amber-500" : "bg-emerald-500" )} style={{ width: `${disk.percent}%` }} />
))}
) : (
{t("terminal.serverStats.noData")}
)}
{/* Network - with HoverCard for per-interface details */} {(hasNetworkDetails || hasLatency) && (
{t("terminal.serverStats.networkDetails")}
{t("terminal.serverStats.latency")} {formatLatency(serverStats.latencyMs)}
{hasNetworkDetails ? (
{serverStats.netInterfaces.map((iface, index) => (
{iface.name}
{formatNetSpeed(iface.rxSpeed)} {formatNetSpeed(iface.txSpeed)}
))}
) : (
{t("terminal.serverStats.noData")}
)}
)}
); };