Files
Netcatty/components/terminal/hooks/useServerStats.ts
T
bincxz 95bc7f018c feat: display Linux server stats in terminal statusbar
Add real-time monitoring of Linux server resources in the terminal statusbar:
- CPU usage with per-core breakdown in hover popup
- Memory usage with htop-style colored bar (Used/Buffers/Cache/Free)
- Top 10 processes by memory consumption
- Disk usage for all mounted partitions
- Network speed (RX/TX) with per-interface details

Features:
- Only enabled for Linux servers (detected automatically)
- Configurable refresh interval (default 5 seconds)
- Toggle in Settings > Terminal tab
- Hover-triggered popups using Radix UI HoverCard
- BusyBox compatibility with fallback commands

Closes #108

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-21 17:58:55 +08:00

202 lines
6.0 KiB
TypeScript

import { useState, useEffect, useCallback, useRef } from 'react';
import { netcattyBridge } from '../../../infrastructure/services/netcattyBridge';
export interface DiskInfo {
mountPoint: string;
used: number; // Used in GB
total: number; // Total in GB
percent: number; // Usage percentage
}
export interface NetInterfaceInfo {
name: string; // Interface name (e.g., eth0, ens33)
rxBytes: number; // Total received bytes
txBytes: number; // Total transmitted bytes
rxSpeed: number; // Receive speed (bytes/sec)
txSpeed: number; // Transmit speed (bytes/sec)
}
export interface ProcessInfo {
pid: string;
memPercent: number;
command: string;
}
export interface ServerStats {
cpu: number | null; // CPU usage percentage (0-100)
cpuCores: number | null; // Number of CPU cores
cpuPerCore: number[]; // Per-core CPU usage array
memTotal: number | null; // Total memory in MB
memUsed: number | null; // Used memory in MB (excluding buffers/cache)
memFree: number | null; // Free memory in MB
memBuffers: number | null; // Buffers in MB
memCached: number | null; // Cached in MB
topProcesses: ProcessInfo[]; // Top 10 processes by memory
diskPercent: number | null; // Disk usage percentage for root partition
diskUsed: number | null; // Disk used in GB
diskTotal: number | null; // Total disk in GB
disks: DiskInfo[]; // All mounted disks
netRxSpeed: number; // Total network receive speed (bytes/sec)
netTxSpeed: number; // Total network transmit speed (bytes/sec)
netInterfaces: NetInterfaceInfo[]; // Per-interface network stats
lastUpdated: number | null; // Timestamp of last successful update
}
interface UseServerStatsOptions {
sessionId: string;
enabled: boolean; // Whether stats collection is enabled (from settings)
refreshInterval: number; // Refresh interval in seconds
isLinux: boolean; // Only collect stats for Linux servers
isConnected: boolean; // Only collect when connected
}
export function useServerStats({
sessionId,
enabled,
refreshInterval,
isLinux,
isConnected,
}: UseServerStatsOptions) {
const [stats, setStats] = useState<ServerStats>({
cpu: null,
cpuCores: null,
cpuPerCore: [],
memTotal: null,
memUsed: null,
memFree: null,
memBuffers: null,
memCached: null,
topProcesses: [],
diskPercent: null,
diskUsed: null,
diskTotal: null,
disks: [],
netRxSpeed: 0,
netTxSpeed: 0,
netInterfaces: [],
lastUpdated: null,
});
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const isMountedRef = useRef(true);
const fetchStats = useCallback(async () => {
if (!enabled || !isLinux || !isConnected || !sessionId) {
return;
}
const bridge = netcattyBridge.get();
if (!bridge?.getServerStats) {
return;
}
setIsLoading(true);
setError(null);
try {
const result = await bridge.getServerStats(sessionId);
if (!isMountedRef.current) return;
if (result.success && result.stats) {
setStats({
cpu: result.stats.cpu,
cpuCores: result.stats.cpuCores,
cpuPerCore: result.stats.cpuPerCore || [],
memTotal: result.stats.memTotal,
memUsed: result.stats.memUsed,
memFree: result.stats.memFree,
memBuffers: result.stats.memBuffers,
memCached: result.stats.memCached,
topProcesses: result.stats.topProcesses || [],
diskPercent: result.stats.diskPercent,
diskUsed: result.stats.diskUsed,
diskTotal: result.stats.diskTotal,
disks: result.stats.disks || [],
netRxSpeed: result.stats.netRxSpeed || 0,
netTxSpeed: result.stats.netTxSpeed || 0,
netInterfaces: result.stats.netInterfaces || [],
lastUpdated: Date.now(),
});
} else if (result.error) {
setError(result.error);
}
} catch (err) {
if (isMountedRef.current) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
} finally {
if (isMountedRef.current) {
setIsLoading(false);
}
}
}, [sessionId, enabled, isLinux, isConnected]);
// Initial fetch and periodic refresh
useEffect(() => {
isMountedRef.current = true;
// Clear any existing interval
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
// Don't run if not enabled or not a Linux system
if (!enabled || !isLinux || !isConnected) {
// Reset stats when disabled or not connected
setStats({
cpu: null,
cpuCores: null,
cpuPerCore: [],
memTotal: null,
memUsed: null,
memFree: null,
memBuffers: null,
memCached: null,
topProcesses: [],
diskPercent: null,
diskUsed: null,
diskTotal: null,
disks: [],
netRxSpeed: 0,
netTxSpeed: 0,
netInterfaces: [],
lastUpdated: null,
});
return;
}
// Initial fetch with a small delay to let the connection stabilize
const initialTimer = setTimeout(() => {
fetchStats();
}, 2000);
// Set up periodic refresh
const intervalMs = Math.max(5, refreshInterval) * 1000; // Minimum 5 seconds
intervalRef.current = setInterval(fetchStats, intervalMs);
return () => {
isMountedRef.current = false;
clearTimeout(initialTimer);
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [enabled, isLinux, isConnected, refreshInterval, fetchStats]);
// Manual refresh function
const refresh = useCallback(() => {
fetchStats();
}, [fetchStats]);
return {
stats,
isLoading,
error,
refresh,
};
}