653 lines
26 KiB
C#
653 lines
26 KiB
C#
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("'", "'");
|
|
}
|
|
}
|
|
}
|