mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
added Network Performance infos
This commit is contained in:
+173
-1
@@ -1,8 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Management;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using AutopilotMonitor.Agent.Core.Logging;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
using Microsoft.Win32;
|
||||
@@ -53,6 +56,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
|
||||
CollectBitLockerStatus();
|
||||
CollectTpmStatus();
|
||||
CollectAadJoinStatus();
|
||||
CollectActiveNetworkInterfaceInfo();
|
||||
|
||||
return (profileResult.enrollmentType, profileResult.isHybridJoin, espConfig.skipUserStatusPage, espConfig.skipDeviceStatusPage);
|
||||
}
|
||||
@@ -68,7 +72,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
|
||||
// BitLocker can be enabled during enrollment via policy
|
||||
CollectBitLockerStatus();
|
||||
|
||||
// Add more collectors here as needed
|
||||
// Re-collect active network interface info (link speed, WiFi signal may have changed)
|
||||
CollectActiveNetworkInterfaceInfo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -662,6 +667,173 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
|
||||
return (skipUser, skipDevice);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects active network interface details (name, type, link speed, MAC) using
|
||||
/// System.Net.NetworkInformation — instant, no process spawn.
|
||||
/// If the active interface is WiFi, fires off a separate async task to collect
|
||||
/// WiFi signal info via netsh (fire-and-forget, never blocks).
|
||||
/// </summary>
|
||||
private void CollectActiveNetworkInterfaceInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
var activeNic = FindActiveNetworkInterface();
|
||||
|
||||
if (activeNic == null)
|
||||
{
|
||||
EmitDeviceInfoEvent("network_interface_info", "No active network interface found",
|
||||
new Dictionary<string, object> { { "status", "no_active_interface" } });
|
||||
return;
|
||||
}
|
||||
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ "adapterName", activeNic.Name },
|
||||
{ "adapterDescription", activeNic.Description },
|
||||
{ "macAddress", activeNic.GetPhysicalAddress().ToString() },
|
||||
{ "interfaceType", activeNic.NetworkInterfaceType.ToString() },
|
||||
{ "linkSpeedMbps", activeNic.Speed / 1_000_000 },
|
||||
{ "interfaceId", activeNic.Id }
|
||||
};
|
||||
|
||||
var isWifi = activeNic.NetworkInterfaceType == NetworkInterfaceType.Wireless80211;
|
||||
data["connectionType"] = isWifi ? "WiFi" : "Ethernet";
|
||||
|
||||
var gateways = activeNic.GetIPProperties().GatewayAddresses;
|
||||
if (gateways.Count > 0)
|
||||
{
|
||||
var gwList = new List<string>();
|
||||
foreach (var gw in gateways)
|
||||
gwList.Add(gw.Address.ToString());
|
||||
data["gateways"] = string.Join(", ", gwList);
|
||||
}
|
||||
|
||||
var message = $"Active interface: {activeNic.Description} ({data["connectionType"]}, {data["linkSpeedMbps"]} Mbps)";
|
||||
EmitDeviceInfoEvent("network_interface_info", message, data);
|
||||
|
||||
// Fire-and-forget WiFi signal collection — separate event, never blocks
|
||||
if (isWifi)
|
||||
{
|
||||
Task.Run(() => CollectWiFiSignalInfo());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"EnrollmentTracker: failed to collect active network interface info: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the active network interface: Up, not Loopback/Tunnel, has a non-0.0.0.0 gateway.
|
||||
/// </summary>
|
||||
private static NetworkInterface FindActiveNetworkInterface()
|
||||
{
|
||||
try
|
||||
{
|
||||
var interfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
foreach (var nic in interfaces)
|
||||
{
|
||||
if (nic.OperationalStatus != OperationalStatus.Up)
|
||||
continue;
|
||||
if (nic.NetworkInterfaceType == NetworkInterfaceType.Loopback ||
|
||||
nic.NetworkInterfaceType == NetworkInterfaceType.Tunnel)
|
||||
continue;
|
||||
|
||||
var ipProps = nic.GetIPProperties();
|
||||
foreach (var gw in ipProps.GatewayAddresses)
|
||||
{
|
||||
if (gw.Address.ToString() != "0.0.0.0")
|
||||
return nic;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Caller handles null
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects WiFi signal info via netsh wlan show interfaces and emits a separate
|
||||
/// wifi_signal_info event. Designed to run as fire-and-forget via Task.Run —
|
||||
/// never blocks other collection. VMs without WiFi service simply get no event.
|
||||
/// </summary>
|
||||
private void CollectWiFiSignalInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "netsh.exe",
|
||||
Arguments = "wlan show interfaces",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
string output;
|
||||
using (var process = Process.Start(psi))
|
||||
{
|
||||
output = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit(5000);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(output))
|
||||
return;
|
||||
|
||||
var data = new Dictionary<string, object>();
|
||||
|
||||
foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
var colonIndex = trimmed.IndexOf(':');
|
||||
if (colonIndex < 0) continue;
|
||||
|
||||
var key = trimmed.Substring(0, colonIndex).Trim();
|
||||
var value = trimmed.Substring(colonIndex + 1).Trim();
|
||||
|
||||
if (key.Equals("SSID", StringComparison.OrdinalIgnoreCase) &&
|
||||
!key.Equals("BSSID", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
data["wifiSsid"] = value;
|
||||
}
|
||||
else if (key.Equals("Signal", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (int.TryParse(value.TrimEnd('%'), out var signal))
|
||||
data["wifiSignalPercent"] = signal;
|
||||
}
|
||||
else if (key.Equals("Radio type", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
data["wifiRadioType"] = value;
|
||||
}
|
||||
else if (key.Equals("Channel", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (int.TryParse(value, out var channel))
|
||||
data["wifiChannel"] = channel;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.Count > 0)
|
||||
{
|
||||
var message = data.ContainsKey("wifiSsid")
|
||||
? $"WiFi: {data["wifiSsid"]}"
|
||||
: "WiFi signal info";
|
||||
if (data.ContainsKey("wifiSignalPercent"))
|
||||
message += $", Signal: {data["wifiSignalPercent"]}%";
|
||||
if (data.ContainsKey("wifiRadioType"))
|
||||
message += $" ({data["wifiRadioType"]})";
|
||||
|
||||
EmitDeviceInfoEvent("wifi_signal_info", message, data);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug($"WiFi signal info collection failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitDeviceInfoEvent(string eventType, string message, Dictionary<string, object> data)
|
||||
{
|
||||
_emitEvent(new EnrollmentEvent
|
||||
|
||||
+159
-1
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Management;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Threading;
|
||||
using AutopilotMonitor.Agent.Core.Logging;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
@@ -25,6 +26,14 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
|
||||
private PerformanceCounter _cpuCounter;
|
||||
private PerformanceCounter _diskQueueCounter;
|
||||
|
||||
// Network throughput tracking
|
||||
private string _activeNicId;
|
||||
private string _activeNicName;
|
||||
private long _prevBytesSent;
|
||||
private long _prevBytesReceived;
|
||||
private DateTime _prevNetSampleTime;
|
||||
private bool _networkInitialized;
|
||||
|
||||
public PerformanceCollector(string sessionId, string tenantId, Action<EnrollmentEvent> onEventCollected,
|
||||
AgentLogger logger, int intervalSeconds = 60)
|
||||
{
|
||||
@@ -52,6 +61,31 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
|
||||
_logger.Warning($"Failed to initialize performance counters: {ex.Message}");
|
||||
}
|
||||
|
||||
// Initialize network throughput baseline
|
||||
try
|
||||
{
|
||||
var activeNic = FindActiveNetworkInterface();
|
||||
if (activeNic != null)
|
||||
{
|
||||
_activeNicId = activeNic.Id;
|
||||
_activeNicName = activeNic.Description;
|
||||
var stats = activeNic.GetIPStatistics();
|
||||
_prevBytesSent = stats.BytesSent;
|
||||
_prevBytesReceived = stats.BytesReceived;
|
||||
_prevNetSampleTime = DateTime.UtcNow;
|
||||
_networkInitialized = true;
|
||||
_logger.Debug($"Network throughput tracking initialized on: {_activeNicName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("No active network interface found for throughput tracking");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug($"Failed to initialize network throughput baseline: {ex.Message}");
|
||||
}
|
||||
|
||||
// Start with an initial delay, then poll on interval
|
||||
_pollTimer = new Timer(
|
||||
_ => CollectMetrics(),
|
||||
@@ -129,6 +163,96 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
|
||||
_logger.Debug($"Disk space query failed: {ex.Message}");
|
||||
}
|
||||
|
||||
// Network throughput (bytes sent/received delta + rate)
|
||||
try
|
||||
{
|
||||
if (_networkInitialized)
|
||||
{
|
||||
// Re-find the NIC by cached ID
|
||||
NetworkInterface currentNic = null;
|
||||
try
|
||||
{
|
||||
foreach (var nic in NetworkInterface.GetAllNetworkInterfaces())
|
||||
{
|
||||
if (nic.Id == _activeNicId)
|
||||
{
|
||||
currentNic = nic;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /* swallow */ }
|
||||
|
||||
// If cached NIC is gone, try to find a new active one
|
||||
if (currentNic == null || currentNic.OperationalStatus != OperationalStatus.Up)
|
||||
{
|
||||
currentNic = FindActiveNetworkInterface();
|
||||
if (currentNic != null)
|
||||
{
|
||||
_activeNicId = currentNic.Id;
|
||||
_activeNicName = currentNic.Description;
|
||||
var freshStats = currentNic.GetIPStatistics();
|
||||
_prevBytesSent = freshStats.BytesSent;
|
||||
_prevBytesReceived = freshStats.BytesReceived;
|
||||
_prevNetSampleTime = DateTime.UtcNow;
|
||||
_logger.Debug($"Network throughput tracking switched to: {_activeNicName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_networkInitialized = false;
|
||||
_logger.Debug("Active network interface lost, throughput tracking paused");
|
||||
}
|
||||
}
|
||||
|
||||
if (currentNic != null && currentNic.OperationalStatus == OperationalStatus.Up)
|
||||
{
|
||||
var stats = currentNic.GetIPStatistics();
|
||||
var now = DateTime.UtcNow;
|
||||
var elapsedSeconds = (now - _prevNetSampleTime).TotalSeconds;
|
||||
|
||||
if (elapsedSeconds > 0)
|
||||
{
|
||||
var deltaSent = stats.BytesSent - _prevBytesSent;
|
||||
var deltaReceived = stats.BytesReceived - _prevBytesReceived;
|
||||
|
||||
// Guard against counter reset (adapter reconnect)
|
||||
if (deltaSent < 0) deltaSent = 0;
|
||||
if (deltaReceived < 0) deltaReceived = 0;
|
||||
|
||||
data["net_adapter"] = _activeNicName;
|
||||
data["net_bytes_sent_delta"] = deltaSent;
|
||||
data["net_bytes_received_delta"] = deltaReceived;
|
||||
data["net_send_rate_kbps"] = Math.Round(deltaSent * 8.0 / 1000.0 / elapsedSeconds, 1);
|
||||
data["net_receive_rate_kbps"] = Math.Round(deltaReceived * 8.0 / 1000.0 / elapsedSeconds, 1);
|
||||
}
|
||||
|
||||
_prevBytesSent = stats.BytesSent;
|
||||
_prevBytesReceived = stats.BytesReceived;
|
||||
_prevNetSampleTime = now;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try to re-initialize (e.g., network came up after boot)
|
||||
var activeNic = FindActiveNetworkInterface();
|
||||
if (activeNic != null)
|
||||
{
|
||||
_activeNicId = activeNic.Id;
|
||||
_activeNicName = activeNic.Description;
|
||||
var stats = activeNic.GetIPStatistics();
|
||||
_prevBytesSent = stats.BytesSent;
|
||||
_prevBytesReceived = stats.BytesReceived;
|
||||
_prevNetSampleTime = DateTime.UtcNow;
|
||||
_networkInitialized = true;
|
||||
_logger.Debug($"Network throughput tracking re-initialized on: {_activeNicName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug($"Network throughput collection failed: {ex.Message}");
|
||||
}
|
||||
|
||||
if (data.Count > 0)
|
||||
{
|
||||
_onEventCollected(new EnrollmentEvent
|
||||
@@ -142,7 +266,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
|
||||
Phase = EnrollmentPhase.Unknown,
|
||||
Message = $"CPU: {(data.ContainsKey("cpu_percent") ? data["cpu_percent"] : "?")}%, " +
|
||||
$"Memory: {(data.ContainsKey("memory_used_percent") ? data["memory_used_percent"] : "?")}%, " +
|
||||
$"Disk Free: {(data.ContainsKey("disk_free_gb") ? data["disk_free_gb"] : "?")} GB",
|
||||
$"Disk Free: {(data.ContainsKey("disk_free_gb") ? data["disk_free_gb"] : "?")} GB" +
|
||||
(data.ContainsKey("net_receive_rate_kbps")
|
||||
? $", Net: ↓{data["net_receive_rate_kbps"]} / ↑{data["net_send_rate_kbps"]} kbps"
|
||||
: ""),
|
||||
Data = data
|
||||
});
|
||||
}
|
||||
@@ -153,6 +280,37 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the active network interface: Up, not Loopback/Tunnel, has a non-0.0.0.0 gateway.
|
||||
/// </summary>
|
||||
private static NetworkInterface FindActiveNetworkInterface()
|
||||
{
|
||||
try
|
||||
{
|
||||
var interfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
foreach (var nic in interfaces)
|
||||
{
|
||||
if (nic.OperationalStatus != OperationalStatus.Up)
|
||||
continue;
|
||||
if (nic.NetworkInterfaceType == NetworkInterfaceType.Loopback ||
|
||||
nic.NetworkInterfaceType == NetworkInterfaceType.Tunnel)
|
||||
continue;
|
||||
|
||||
var ipProps = nic.GetIPProperties();
|
||||
foreach (var gw in ipProps.GatewayAddresses)
|
||||
{
|
||||
if (gw.Address.ToString() != "0.0.0.0")
|
||||
return nic;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Caller handles null
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
|
||||
@@ -1 +1 @@
|
||||
214
|
||||
219
|
||||
|
||||
@@ -1125,6 +1125,8 @@ function DeviceDetailsCard({ events }: { events: EnrollmentEvent[] }) {
|
||||
const networkAdapters = getEventData("network_adapters");
|
||||
const dnsConfig = getEventData("dns_configuration");
|
||||
const proxyConfig = getEventData("proxy_configuration");
|
||||
const networkInterfaceInfo = getEventData("network_interface_info");
|
||||
const wifiSignalInfo = getEventData("wifi_signal_info");
|
||||
const autopilotProfile = normalizeAutopilotProfile(getEventData("autopilot_profile"));
|
||||
const aadJoinStatus = getEventData("aad_join_status");
|
||||
const imeVersion = getEventData("ime_agent_version");
|
||||
@@ -1134,8 +1136,8 @@ function DeviceDetailsCard({ events }: { events: EnrollmentEvent[] }) {
|
||||
const deviceLocation = getEventData("device_location");
|
||||
|
||||
// Check if we have any device detail events at all
|
||||
const hasData = agentStarted || bootTime || osInfo || networkAdapters || dnsConfig || proxyConfig || autopilotProfile ||
|
||||
aadJoinStatus || imeVersion || bitLockerStatus || secureBootStatus || deviceLocation;
|
||||
const hasData = agentStarted || bootTime || osInfo || networkAdapters || dnsConfig || proxyConfig || networkInterfaceInfo || wifiSignalInfo ||
|
||||
autopilotProfile || aadJoinStatus || imeVersion || bitLockerStatus || secureBootStatus || deviceLocation;
|
||||
|
||||
if (!hasData) return null;
|
||||
|
||||
@@ -1192,7 +1194,7 @@ function DeviceDetailsCard({ events }: { events: EnrollmentEvent[] }) {
|
||||
)}
|
||||
|
||||
{/* Network */}
|
||||
{(networkAdapters || dnsConfig || proxyConfig) && (
|
||||
{(networkAdapters || dnsConfig || proxyConfig || networkInterfaceInfo || wifiSignalInfo) && (
|
||||
<DetailSection title="Network">
|
||||
{/* Network Adapters */}
|
||||
{networkAdapters && networkAdapters.adapters && (
|
||||
@@ -1247,6 +1249,36 @@ function DeviceDetailsCard({ events }: { events: EnrollmentEvent[] }) {
|
||||
})
|
||||
)}
|
||||
|
||||
{/* Active Network Interface */}
|
||||
{networkInterfaceInfo && networkInterfaceInfo.status !== "no_active_interface" && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-200">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Active Interface</div>
|
||||
<DetailRow label="Connection" value={networkInterfaceInfo.connectionType === "WiFi" ? "WiFi" : "Ethernet"} />
|
||||
{networkInterfaceInfo.adapterDescription && <DetailRow label="Adapter" value={networkInterfaceInfo.adapterDescription} />}
|
||||
{networkInterfaceInfo.linkSpeedMbps !== undefined && (
|
||||
<DetailRow label="Link Speed" value={
|
||||
networkInterfaceInfo.linkSpeedMbps >= 1000
|
||||
? `${(networkInterfaceInfo.linkSpeedMbps / 1000).toFixed(1)} Gbps`
|
||||
: `${networkInterfaceInfo.linkSpeedMbps} Mbps`
|
||||
} />
|
||||
)}
|
||||
{networkInterfaceInfo.gateways && <DetailRow label="Gateway" value={networkInterfaceInfo.gateways} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* WiFi Signal */}
|
||||
{wifiSignalInfo && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-200">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">WiFi</div>
|
||||
{wifiSignalInfo.wifiSsid && <DetailRow label="SSID" value={wifiSignalInfo.wifiSsid} />}
|
||||
{wifiSignalInfo.wifiSignalPercent !== undefined && (
|
||||
<DetailRow label="Signal" value={`${wifiSignalInfo.wifiSignalPercent}%`} />
|
||||
)}
|
||||
{wifiSignalInfo.wifiRadioType && <DetailRow label="Radio" value={wifiSignalInfo.wifiRadioType} />}
|
||||
{wifiSignalInfo.wifiChannel !== undefined && <DetailRow label="Channel" value={`${wifiSignalInfo.wifiChannel}`} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Proxy Configuration */}
|
||||
{proxyConfig && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-200">
|
||||
|
||||
@@ -237,8 +237,11 @@ export default function PerformanceChart({ events }: PerformanceChartProps) {
|
||||
const memoryUsedPercent: number[] = [];
|
||||
const diskQueueValues: number[] = [];
|
||||
const diskFreeValues: number[] = [];
|
||||
const netReceiveValues: number[] = [];
|
||||
const netSendValues: number[] = [];
|
||||
const timestamps: string[] = [];
|
||||
const diskTimestamps: string[] = [];
|
||||
const netTimestamps: string[] = [];
|
||||
|
||||
for (const evt of events) {
|
||||
const d = evt.data;
|
||||
@@ -262,6 +265,15 @@ export default function PerformanceChart({ events }: PerformanceChartProps) {
|
||||
diskFreeValues.push(dfg);
|
||||
diskTimestamps.push(new Date(evt.timestamp).toLocaleTimeString());
|
||||
}
|
||||
|
||||
// Network throughput (kbps → Mbps for display)
|
||||
const netRecv = parseFloat(d.net_receive_rate_kbps ?? d.netReceiveRateKbps ?? "");
|
||||
const netSend = parseFloat(d.net_send_rate_kbps ?? d.netSendRateKbps ?? "");
|
||||
if (!isNaN(netRecv) && !isNaN(netSend)) {
|
||||
netReceiveValues.push(netRecv / 1000); // kbps → Mbps
|
||||
netSendValues.push(netSend / 1000);
|
||||
netTimestamps.push(new Date(evt.timestamp).toLocaleTimeString());
|
||||
}
|
||||
}
|
||||
|
||||
const last = events[events.length - 1]?.data;
|
||||
@@ -273,11 +285,16 @@ export default function PerformanceChart({ events }: PerformanceChartProps) {
|
||||
memoryUsedPercent,
|
||||
diskQueueValues,
|
||||
diskFreeValues,
|
||||
netReceiveValues,
|
||||
netSendValues,
|
||||
diskTimestamps,
|
||||
netTimestamps,
|
||||
timestamps,
|
||||
latestCpu: cpuValues[cpuValues.length - 1] ?? 0,
|
||||
latestMemory: memoryUsedPercent[memoryUsedPercent.length - 1] ?? 0,
|
||||
latestDiskQueue: diskQueueValues[diskQueueValues.length - 1] ?? 0,
|
||||
latestNetReceive: netReceiveValues[netReceiveValues.length - 1] ?? 0,
|
||||
latestNetSend: netSendValues[netSendValues.length - 1] ?? 0,
|
||||
diskFreeGb,
|
||||
diskTotalGb,
|
||||
sampleCount: events.length,
|
||||
@@ -298,8 +315,8 @@ export default function PerformanceChart({ events }: PerformanceChartProps) {
|
||||
<span className="text-xs text-gray-400">({metrics.sampleCount} samples)</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* CPU - top left */}
|
||||
<div className={`grid grid-cols-1 sm:grid-cols-2 ${metrics.netReceiveValues.length >= 2 ? "lg:grid-cols-3" : "lg:grid-cols-4"} gap-4`}>
|
||||
{/* CPU */}
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-medium text-gray-500 uppercase">CPU</span>
|
||||
@@ -397,6 +414,50 @@ export default function PerformanceChart({ events }: PerformanceChartProps) {
|
||||
<div className="text-xs text-gray-400 mt-2">No data</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Network Download */}
|
||||
{metrics.netReceiveValues.length >= 2 && (
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-medium text-gray-500 uppercase">Net ↓</span>
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
{metrics.latestNetReceive >= 1
|
||||
? `${metrics.latestNetReceive.toFixed(1)} Mbps`
|
||||
: `${(metrics.latestNetReceive * 1000).toFixed(0)} kbps`}
|
||||
</span>
|
||||
</div>
|
||||
<Sparkline
|
||||
values={metrics.netReceiveValues}
|
||||
maxValue={Math.max(1, ...metrics.netReceiveValues)}
|
||||
color="#0891b2"
|
||||
timestamps={metrics.netTimestamps}
|
||||
unit=" Mbps"
|
||||
formatValue={(v) => v >= 1 ? `${v.toFixed(1)} Mbps` : `${(v * 1000).toFixed(0)} kbps`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Network Upload */}
|
||||
{metrics.netSendValues.length >= 2 && (
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-medium text-gray-500 uppercase">Net ↑</span>
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
{metrics.latestNetSend >= 1
|
||||
? `${metrics.latestNetSend.toFixed(1)} Mbps`
|
||||
: `${(metrics.latestNetSend * 1000).toFixed(0)} kbps`}
|
||||
</span>
|
||||
</div>
|
||||
<Sparkline
|
||||
values={metrics.netSendValues}
|
||||
maxValue={Math.max(1, ...metrics.netSendValues)}
|
||||
color="#7c3aed"
|
||||
timestamps={metrics.netTimestamps}
|
||||
unit=" Mbps"
|
||||
formatValue={(v) => v >= 1 ? `${v.toFixed(1)} Mbps` : `${(v * 1000).toFixed(0)} kbps`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user