Updated
This commit is contained in:
@@ -0,0 +1,652 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Text;
|
||||
|
||||
namespace EonaCat.Connections.Helpers
|
||||
{
|
||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
|
||||
|
||||
public class NetworkMonitor : IDisposable
|
||||
{
|
||||
private readonly string _targetHost;
|
||||
private readonly int _pingIntervalMs;
|
||||
private readonly int _maxHistory;
|
||||
private readonly ConcurrentQueue<NetworkSample> _samples = new ConcurrentQueue<NetworkSample>();
|
||||
private CancellationTokenSource _cts;
|
||||
private Task _monitorTask;
|
||||
private volatile bool _isRunning;
|
||||
|
||||
private CancellationTokenSource _autoReportCts;
|
||||
private Task _autoReportTask;
|
||||
private volatile bool _autoReportRunning;
|
||||
|
||||
public event EventHandler<NetworkOutageEventArgs> OnOutageDetected;
|
||||
public event EventHandler<NetworkOutageEventArgs> OnOutageRecovered;
|
||||
public event EventHandler<NetworkSample> OnSampleRecorded;
|
||||
|
||||
public bool IsRunning => _isRunning;
|
||||
public string TargetHost => _targetHost;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether automatic HTML report generation is currently running.
|
||||
/// </summary>
|
||||
public bool IsAutoHtmlReportRunning => _autoReportRunning;
|
||||
|
||||
public NetworkMonitor(string targetHost = "127.0.0.1", int pingIntervalMs = 5000, int maxHistory = 1000)
|
||||
{
|
||||
_targetHost = targetHost;
|
||||
_pingIntervalMs = Math.Max(1000, pingIntervalMs);
|
||||
_maxHistory = Math.Max(10, maxHistory);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_isRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cts?.Dispose();
|
||||
_cts = new CancellationTokenSource();
|
||||
_isRunning = true;
|
||||
_monitorTask = Task.Run(() => MonitorLoopAsync(_cts.Token));
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_isRunning = false;
|
||||
_cts?.Cancel();
|
||||
|
||||
try
|
||||
{
|
||||
_monitorTask?.Wait(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
catch (AggregateException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MonitorLoopAsync(CancellationToken token)
|
||||
{
|
||||
bool wasReachable = true;
|
||||
DateTime? outageStartedAt = null;
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
var sample = await CollectSampleAsync().ConfigureAwait(false);
|
||||
RecordSample(sample);
|
||||
|
||||
OnSampleRecorded?.Invoke(this, sample);
|
||||
|
||||
if (!sample.IsReachable && wasReachable)
|
||||
{
|
||||
outageStartedAt = sample.Timestamp;
|
||||
OnOutageDetected?.Invoke(this, new NetworkOutageEventArgs
|
||||
{
|
||||
TargetHost = _targetHost,
|
||||
OutageStartedAt = sample.Timestamp,
|
||||
Sample = sample
|
||||
});
|
||||
}
|
||||
else if (sample.IsReachable && !wasReachable && outageStartedAt.HasValue)
|
||||
{
|
||||
OnOutageRecovered?.Invoke(this, new NetworkOutageEventArgs
|
||||
{
|
||||
TargetHost = _targetHost,
|
||||
OutageStartedAt = outageStartedAt.Value,
|
||||
OutageEndedAt = sample.Timestamp,
|
||||
OutageDuration = sample.Timestamp - outageStartedAt.Value,
|
||||
Sample = sample
|
||||
});
|
||||
outageStartedAt = null;
|
||||
}
|
||||
|
||||
wasReachable = sample.IsReachable;
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(_pingIntervalMs, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<NetworkSample> CollectSampleAsync()
|
||||
{
|
||||
var sample = new NetworkSample
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
TargetHost = _targetHost
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
using (var ping = new Ping())
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var reply = await ping.SendPingAsync(_targetHost, 5000).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
|
||||
sample.RoundTripTimeMs = reply.RoundtripTime;
|
||||
sample.MeasuredLatencyMs = sw.ElapsedMilliseconds;
|
||||
sample.IsReachable = reply.Status == IPStatus.Success;
|
||||
sample.PingStatus = reply.Status;
|
||||
}
|
||||
}
|
||||
catch (PingException ex)
|
||||
{
|
||||
sample.IsReachable = false;
|
||||
sample.Error = ex.InnerException?.Message ?? ex.Message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sample.IsReachable = false;
|
||||
sample.Error = ex.Message;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var interfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||
long totalBytesReceived = 0;
|
||||
long totalBytesSent = 0;
|
||||
|
||||
foreach (var ni in interfaces)
|
||||
{
|
||||
if (ni.OperationalStatus != OperationalStatus.Up)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var stats = ni.GetIPv4Statistics();
|
||||
totalBytesReceived += stats.BytesReceived;
|
||||
totalBytesSent += stats.BytesSent;
|
||||
}
|
||||
|
||||
sample.SystemBytesReceived = totalBytesReceived;
|
||||
sample.SystemBytesSent = totalBytesSent;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Network interface stats may not be available on all platforms
|
||||
}
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
private void RecordSample(NetworkSample sample)
|
||||
{
|
||||
_samples.Enqueue(sample);
|
||||
|
||||
while (_samples.Count > _maxHistory)
|
||||
{
|
||||
_samples.TryDequeue(out _);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<NetworkSample> GetSamples()
|
||||
{
|
||||
return _samples.ToArray();
|
||||
}
|
||||
|
||||
public IReadOnlyList<NetworkSample> GetRecentSamples(int count)
|
||||
{
|
||||
var all = _samples.ToArray();
|
||||
int skip = Math.Max(0, all.Length - count);
|
||||
var result = new NetworkSample[Math.Min(count, all.Length)];
|
||||
Array.Copy(all, skip, result, 0, result.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
public NetworkHealthReport GetHealthReport()
|
||||
{
|
||||
var samples = _samples.ToArray();
|
||||
var report = new NetworkHealthReport
|
||||
{
|
||||
TargetHost = _targetHost,
|
||||
GeneratedAt = DateTime.UtcNow,
|
||||
TotalSamples = samples.Length
|
||||
};
|
||||
|
||||
if (samples.Length == 0)
|
||||
{
|
||||
return report;
|
||||
}
|
||||
|
||||
var reachable = new List<NetworkSample>();
|
||||
var unreachable = new List<NetworkSample>();
|
||||
|
||||
foreach (var s in samples)
|
||||
{
|
||||
if (s.IsReachable)
|
||||
{
|
||||
reachable.Add(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
unreachable.Add(s);
|
||||
}
|
||||
}
|
||||
|
||||
report.SuccessfulPings = reachable.Count;
|
||||
report.FailedPings = unreachable.Count;
|
||||
report.UptimePercentage = samples.Length > 0
|
||||
? (double)reachable.Count / samples.Length * 100.0
|
||||
: 0;
|
||||
|
||||
if (reachable.Count > 0)
|
||||
{
|
||||
long totalRtt = 0;
|
||||
long minRtt = long.MaxValue;
|
||||
long maxRtt = long.MinValue;
|
||||
|
||||
foreach (var s in reachable)
|
||||
{
|
||||
totalRtt += s.RoundTripTimeMs;
|
||||
if (s.RoundTripTimeMs < minRtt)
|
||||
{
|
||||
minRtt = s.RoundTripTimeMs;
|
||||
}
|
||||
if (s.RoundTripTimeMs > maxRtt)
|
||||
{
|
||||
maxRtt = s.RoundTripTimeMs;
|
||||
}
|
||||
}
|
||||
|
||||
report.AverageLatencyMs = (double)totalRtt / reachable.Count;
|
||||
report.MinLatencyMs = minRtt;
|
||||
report.MaxLatencyMs = maxRtt;
|
||||
|
||||
// Calculate jitter (standard deviation of latency)
|
||||
double sumSquaredDiff = 0;
|
||||
foreach (var s in reachable)
|
||||
{
|
||||
var diff = s.RoundTripTimeMs - report.AverageLatencyMs;
|
||||
sumSquaredDiff += diff * diff;
|
||||
}
|
||||
|
||||
report.JitterMs = Math.Sqrt(sumSquaredDiff / reachable.Count);
|
||||
}
|
||||
|
||||
// Detect outage windows
|
||||
bool inOutage = false;
|
||||
DateTime outageStart = DateTime.MinValue;
|
||||
|
||||
foreach (var s in samples)
|
||||
{
|
||||
if (!s.IsReachable && !inOutage)
|
||||
{
|
||||
inOutage = true;
|
||||
outageStart = s.Timestamp;
|
||||
}
|
||||
else if (s.IsReachable && inOutage)
|
||||
{
|
||||
inOutage = false;
|
||||
report.OutageWindows.Add(new OutageWindow
|
||||
{
|
||||
Start = outageStart,
|
||||
End = s.Timestamp,
|
||||
Duration = s.Timestamp - outageStart
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (inOutage)
|
||||
{
|
||||
report.OutageWindows.Add(new OutageWindow
|
||||
{
|
||||
Start = outageStart,
|
||||
End = DateTime.UtcNow,
|
||||
Duration = DateTime.UtcNow - outageStart,
|
||||
IsOngoing = true
|
||||
});
|
||||
}
|
||||
|
||||
// Bandwidth estimate between last two samples
|
||||
if (samples.Length >= 2)
|
||||
{
|
||||
var prev = samples[samples.Length - 2];
|
||||
var curr = samples[samples.Length - 1];
|
||||
var elapsed = (curr.Timestamp - prev.Timestamp).TotalSeconds;
|
||||
|
||||
if (elapsed > 0 && prev.SystemBytesReceived > 0 && curr.SystemBytesReceived > 0)
|
||||
{
|
||||
report.CurrentReceiveBytesPerSecond = (curr.SystemBytesReceived - prev.SystemBytesReceived) / elapsed;
|
||||
report.CurrentSendBytesPerSecond = (curr.SystemBytesSent - prev.SystemBytesSent) / elapsed;
|
||||
}
|
||||
}
|
||||
|
||||
report.IsStable = report.UptimePercentage >= 99.0
|
||||
&& report.JitterMs < 50
|
||||
&& report.OutageWindows.Count(o => o.IsOngoing) == 0;
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
StopAutoHtmlReport();
|
||||
_cts?.Dispose();
|
||||
_autoReportCts?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts periodic automatic generation of the network health HTML report.
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">Directory where the HTML file is written.</param>
|
||||
/// <param name="intervalSeconds">Interval in seconds between report generations.</param>
|
||||
/// <param name="fileName">The HTML file name.</param>
|
||||
public void StartAutoHtmlReport(string outputDirectory, int intervalSeconds = 60, string fileName = "status-network.html")
|
||||
{
|
||||
if (_autoReportRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_autoReportCts?.Dispose();
|
||||
_autoReportCts = new CancellationTokenSource();
|
||||
_autoReportRunning = true;
|
||||
|
||||
var interval = TimeSpan.FromSeconds(Math.Max(5, intervalSeconds));
|
||||
var token = _autoReportCts.Token;
|
||||
|
||||
_autoReportTask = Task.Run(async () =>
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
}
|
||||
|
||||
var report = GetHealthReport();
|
||||
var html = report.GenerateHtmlReport();
|
||||
var filePath = Path.Combine(outputDirectory, fileName);
|
||||
File.WriteAllText(filePath, html, Encoding.UTF8);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Swallow to keep the loop alive
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(interval, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the automatic HTML report generation.
|
||||
/// </summary>
|
||||
public void StopAutoHtmlReport()
|
||||
{
|
||||
if (!_autoReportRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_autoReportRunning = false;
|
||||
_autoReportCts?.Cancel();
|
||||
|
||||
try
|
||||
{
|
||||
_autoReportTask?.Wait(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
catch (AggregateException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class NetworkSample
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string TargetHost { get; set; }
|
||||
public bool IsReachable { get; set; }
|
||||
public long RoundTripTimeMs { get; set; }
|
||||
public long MeasuredLatencyMs { get; set; }
|
||||
public IPStatus PingStatus { get; set; }
|
||||
public string Error { get; set; }
|
||||
public long SystemBytesReceived { get; set; }
|
||||
public long SystemBytesSent { get; set; }
|
||||
}
|
||||
|
||||
public class NetworkOutageEventArgs : EventArgs
|
||||
{
|
||||
public string TargetHost { get; set; }
|
||||
public DateTime OutageStartedAt { get; set; }
|
||||
public DateTime? OutageEndedAt { get; set; }
|
||||
public TimeSpan? OutageDuration { get; set; }
|
||||
public NetworkSample Sample { get; set; }
|
||||
}
|
||||
|
||||
public class OutageWindow
|
||||
{
|
||||
public DateTime Start { get; set; }
|
||||
public DateTime End { get; set; }
|
||||
public TimeSpan Duration { get; set; }
|
||||
public bool IsOngoing { get; set; }
|
||||
}
|
||||
|
||||
public class NetworkHealthReport
|
||||
{
|
||||
public string TargetHost { get; set; }
|
||||
public DateTime GeneratedAt { get; set; }
|
||||
public int TotalSamples { get; set; }
|
||||
public int SuccessfulPings { get; set; }
|
||||
public int FailedPings { get; set; }
|
||||
public double UptimePercentage { get; set; }
|
||||
public double AverageLatencyMs { get; set; }
|
||||
public long MinLatencyMs { get; set; }
|
||||
public long MaxLatencyMs { get; set; }
|
||||
public double JitterMs { get; set; }
|
||||
public double CurrentReceiveBytesPerSecond { get; set; }
|
||||
public double CurrentSendBytesPerSecond { get; set; }
|
||||
public bool IsStable { get; set; }
|
||||
public List<OutageWindow> OutageWindows { get; set; } = new List<OutageWindow>();
|
||||
|
||||
public string GetSummary()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"=== Network Health Report ===");
|
||||
sb.AppendLine($"Target: {TargetHost}");
|
||||
sb.AppendLine($"Generated: {GeneratedAt:O}");
|
||||
sb.AppendLine($"Samples: {TotalSamples}");
|
||||
sb.AppendLine($"Success: {SuccessfulPings} / Failed: {FailedPings}");
|
||||
sb.AppendLine($"Uptime: {UptimePercentage:F2}%");
|
||||
sb.AppendLine($"Latency (avg): {AverageLatencyMs:F1} ms");
|
||||
sb.AppendLine($"Latency (min): {MinLatencyMs} ms / (max): {MaxLatencyMs} ms");
|
||||
sb.AppendLine($"Jitter: {JitterMs:F1} ms");
|
||||
sb.AppendLine($"Recv BW: {CurrentReceiveBytesPerSecond:F0} B/s");
|
||||
sb.AppendLine($"Send BW: {CurrentSendBytesPerSecond:F0} B/s");
|
||||
sb.AppendLine($"Stable: {(IsStable ? "Yes" : "No")}");
|
||||
sb.AppendLine($"Outages: {OutageWindows.Count}");
|
||||
|
||||
foreach (var o in OutageWindows)
|
||||
{
|
||||
var status = o.IsOngoing ? " (ONGOING)" : string.Empty;
|
||||
sb.AppendLine($" {o.Start:HH:mm:ss} - {o.End:HH:mm:ss} ({o.Duration.TotalSeconds:F0}s){status}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string GenerateHtmlReport(string title = "Network Health Report")
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<!DOCTYPE html>");
|
||||
sb.AppendLine("<html lang=\"en\">");
|
||||
sb.AppendLine("<head>");
|
||||
sb.AppendLine("<meta charset=\"UTF-8\">");
|
||||
sb.AppendLine("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
|
||||
sb.AppendLine($"<title>{HtmlEncode(title)}</title>");
|
||||
sb.AppendLine("<style>");
|
||||
sb.AppendLine("body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; color: #333; }");
|
||||
sb.AppendLine("h1 { color: #1a1a2e; }");
|
||||
sb.AppendLine("h2 { color: #16213e; margin-top: 30px; }");
|
||||
sb.AppendLine(".container { max-width: 1200px; margin: 0 auto; }");
|
||||
sb.AppendLine(".summary-cards { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 24px; }");
|
||||
sb.AppendLine(".card { background: #fff; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); min-width: 180px; }");
|
||||
sb.AppendLine(".card .label { font-size: 0.85em; color: #666; text-transform: uppercase; }");
|
||||
sb.AppendLine(".card .value { font-size: 1.8em; font-weight: bold; margin-top: 4px; }");
|
||||
sb.AppendLine(".card.ok .value { color: #27ae60; }");
|
||||
sb.AppendLine(".card.warn .value { color: #f39c12; }");
|
||||
sb.AppendLine(".card.error .value { color: #e74c3c; }");
|
||||
sb.AppendLine(".status-banner { padding: 16px 24px; border-radius: 8px; margin-bottom: 24px; font-size: 1.2em; font-weight: bold; }");
|
||||
sb.AppendLine(".status-stable { background: #d4edda; color: #155724; }");
|
||||
sb.AppendLine(".status-unstable { background: #f8d7da; color: #721c24; }");
|
||||
sb.AppendLine("table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }");
|
||||
sb.AppendLine("th { background: #1a1a2e; color: #fff; padding: 12px 16px; text-align: left; font-size: 0.85em; text-transform: uppercase; }");
|
||||
sb.AppendLine("td { padding: 10px 16px; border-bottom: 1px solid #eee; font-size: 0.9em; }");
|
||||
sb.AppendLine("tr:hover td { background: #f0f4ff; }");
|
||||
sb.AppendLine("tr.ongoing td { background: #fff3cd; }");
|
||||
sb.AppendLine(".progress-bg { background: #eee; border-radius: 6px; overflow: hidden; height: 22px; }");
|
||||
sb.AppendLine(".progress-bar { height: 100%; border-radius: 6px; text-align: center; color: #fff; font-size: 0.8em; line-height: 22px; }");
|
||||
sb.AppendLine(".no-issues { text-align: center; padding: 60px 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }");
|
||||
sb.AppendLine(".no-issues .icon { font-size: 3em; }");
|
||||
sb.AppendLine(".no-issues p { color: #27ae60; font-size: 1.2em; }");
|
||||
sb.AppendLine(".footer { margin-top: 30px; text-align: center; color: #999; font-size: 0.85em; }");
|
||||
sb.AppendLine("</style>");
|
||||
sb.AppendLine("</head>");
|
||||
sb.AppendLine("<body>");
|
||||
sb.AppendLine("<div class=\"container\">");
|
||||
sb.AppendLine($"<h1>{HtmlEncode(title)}</h1>");
|
||||
|
||||
// Status banner
|
||||
if (TotalSamples > 0)
|
||||
{
|
||||
var bannerClass = IsStable ? "status-stable" : "status-unstable";
|
||||
var bannerText = IsStable ? "✔ Network is Stable" : "⚠ Network is Unstable";
|
||||
sb.AppendLine($"<div class=\"status-banner {bannerClass}\">{bannerText}</div>");
|
||||
}
|
||||
|
||||
// Summary cards
|
||||
var uptimeClass = UptimePercentage >= 99 ? "ok" : UptimePercentage >= 95 ? "warn" : "error";
|
||||
var latencyClass = AverageLatencyMs < 50 ? "ok" : AverageLatencyMs < 150 ? "warn" : "error";
|
||||
var jitterClass = JitterMs < 20 ? "ok" : JitterMs < 50 ? "warn" : "error";
|
||||
|
||||
sb.AppendLine("<div class=\"summary-cards\">");
|
||||
sb.AppendLine($"<div class=\"card\"><div class=\"label\">Target</div><div class=\"value\" style=\"font-size:1em\">{HtmlEncode(TargetHost)}</div></div>");
|
||||
sb.AppendLine($"<div class=\"card\"><div class=\"label\">Samples</div><div class=\"value\">{TotalSamples}</div></div>");
|
||||
sb.AppendLine($"<div class=\"card {uptimeClass}\"><div class=\"label\">Uptime</div><div class=\"value\">{UptimePercentage:F1}%</div></div>");
|
||||
sb.AppendLine($"<div class=\"card {latencyClass}\"><div class=\"label\">Avg Latency</div><div class=\"value\">{AverageLatencyMs:F1} ms</div></div>");
|
||||
sb.AppendLine($"<div class=\"card {jitterClass}\"><div class=\"label\">Jitter</div><div class=\"value\">{JitterMs:F1} ms</div></div>");
|
||||
sb.AppendLine($"<div class=\"card {(FailedPings > 0 ? "error" : "ok")}\"><div class=\"label\">Failed Pings</div><div class=\"value\">{FailedPings}</div></div>");
|
||||
sb.AppendLine("</div>");
|
||||
|
||||
// Uptime progress bar
|
||||
sb.AppendLine("<h2>Uptime</h2>");
|
||||
var barColor = UptimePercentage >= 99 ? "#27ae60" : UptimePercentage >= 95 ? "#f39c12" : "#e74c3c";
|
||||
var barWidth = Math.Max(0, Math.Min(100, UptimePercentage));
|
||||
sb.AppendLine($"<div class=\"progress-bg\"><div class=\"progress-bar\" style=\"width:{barWidth:F1}%;background:{barColor}\">{UptimePercentage:F2}%</div></div>");
|
||||
|
||||
// Latency / Bandwidth
|
||||
sb.AppendLine("<h2>Performance</h2>");
|
||||
sb.AppendLine("<div class=\"summary-cards\">");
|
||||
sb.AppendLine($"<div class=\"card\"><div class=\"label\">Min Latency</div><div class=\"value\">{MinLatencyMs} ms</div></div>");
|
||||
sb.AppendLine($"<div class=\"card\"><div class=\"label\">Max Latency</div><div class=\"value\">{MaxLatencyMs} ms</div></div>");
|
||||
sb.AppendLine($"<div class=\"card\"><div class=\"label\">Recv Bandwidth</div><div class=\"value\" style=\"font-size:1em\">{FormatBytes(CurrentReceiveBytesPerSecond)}/s</div></div>");
|
||||
sb.AppendLine($"<div class=\"card\"><div class=\"label\">Send Bandwidth</div><div class=\"value\" style=\"font-size:1em\">{FormatBytes(CurrentSendBytesPerSecond)}/s</div></div>");
|
||||
sb.AppendLine("</div>");
|
||||
|
||||
// Outage windows
|
||||
sb.AppendLine("<h2>Outage History</h2>");
|
||||
if (OutageWindows.Count == 0)
|
||||
{
|
||||
sb.AppendLine("<div class=\"no-issues\">");
|
||||
sb.AppendLine("<div class=\"icon\">✔</div>");
|
||||
sb.AppendLine("<p>No outages detected.</p>");
|
||||
sb.AppendLine("</div>");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("<table>");
|
||||
sb.AppendLine("<thead><tr><th>#</th><th>Start (UTC)</th><th>End (UTC)</th><th>Duration</th><th>Status</th></tr></thead>");
|
||||
sb.AppendLine("<tbody>");
|
||||
|
||||
for (int i = 0; i < OutageWindows.Count; i++)
|
||||
{
|
||||
var o = OutageWindows[i];
|
||||
var rowClass = o.IsOngoing ? "ongoing" : "";
|
||||
var statusText = o.IsOngoing
|
||||
? "<span style=\"color:#e74c3c;font-weight:bold\">ONGOING</span>"
|
||||
: "<span style=\"color:#27ae60\">Recovered</span>";
|
||||
|
||||
sb.AppendLine($"<tr class=\"{rowClass}\">");
|
||||
sb.AppendLine($"<td>{i + 1}</td>");
|
||||
sb.AppendLine($"<td>{o.Start:yyyy-MM-dd HH:mm:ss}</td>");
|
||||
sb.AppendLine($"<td>{(o.IsOngoing ? "-" : o.End.ToString("yyyy-MM-dd HH:mm:ss"))}</td>");
|
||||
sb.AppendLine($"<td>{o.Duration.TotalSeconds:F0}s</td>");
|
||||
sb.AppendLine($"<td>{statusText}</td>");
|
||||
sb.AppendLine("</tr>");
|
||||
}
|
||||
|
||||
sb.AppendLine("</tbody>");
|
||||
sb.AppendLine("</table>");
|
||||
}
|
||||
|
||||
sb.AppendLine($"<div class=\"footer\">Generated at {GeneratedAt:yyyy-MM-dd HH:mm:ss} UTC — EonaCat.Connections</div>");
|
||||
sb.AppendLine("</div>");
|
||||
sb.AppendLine("</body>");
|
||||
sb.AppendLine("</html>");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void SaveHtmlReport(string filePath, string title = "Network Health Report")
|
||||
{
|
||||
var html = GenerateHtmlReport(title);
|
||||
File.WriteAllText(filePath, html, Encoding.UTF8);
|
||||
}
|
||||
|
||||
private static string FormatBytes(double bytes)
|
||||
{
|
||||
if (bytes >= 1_073_741_824)
|
||||
{
|
||||
return $"{bytes / 1_073_741_824:F2} GB";
|
||||
}
|
||||
if (bytes >= 1_048_576)
|
||||
{
|
||||
return $"{bytes / 1_048_576:F2} MB";
|
||||
}
|
||||
if (bytes >= 1024)
|
||||
{
|
||||
return $"{bytes / 1024:F2} KB";
|
||||
}
|
||||
return $"{bytes:F0} B";
|
||||
}
|
||||
|
||||
private static string HtmlEncode(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return value
|
||||
.Replace("&", "&")
|
||||
.Replace("<", "<")
|
||||
.Replace(">", ">")
|
||||
.Replace("\"", """)
|
||||
.Replace("'", "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user