using Microsoft.Extensions.Diagnostics.HealthChecks; using System.Text; namespace ECMJobRunner.WebCron.HealthCheck; /// /// Generates HTML representation of ASP.NET Core health check reports. /// Produces a Bootstrap 5-based UI with auto-refresh, status indicators, and detailed metrics. /// public static class HealthCheckHtmlGenerator { private static readonly string CacheId = Guid.NewGuid().ToString(); /// /// Generates a complete HTML page for displaying health check results. /// /// The health check report containing service status information. /// A complete HTML document string ready to be sent as HTTP response. /// /// The generated UI includes: /// /// Navigation links to Hangfire, Serilog.UI, and health endpoints /// Overall health status with color-coded badge /// Summary card with total duration and check count /// Individual check cards with detailed metrics and exception info /// Auto-refresh countdown (10 seconds) /// /// External dependencies: /// /// Bootstrap 5.3.0 (CDN) /// /css/health-ui.css (custom styles) /// /js/health-ui.js (auto-refresh logic) /// /// public static string Generate(HealthReport report) { var sb = new StringBuilder(); sb.AppendLine(@$" Health Check - ECMJobRunner "); // Navigation AppendNavigation(sb); // Overall Status Header AppendOverallStatus(sb, report); // Summary Card AppendSummaryCard(sb, report); // Individual Checks AppendIndividualChecks(sb, report); // Refresh Info AppendRefreshInfo(sb); sb.AppendLine(@" "); return sb.ToString(); } /// /// Appends the navigation menu with links to application dashboards. /// /// The StringBuilder to append HTML to. private static void AppendNavigation(StringBuilder sb) { sb.Append(@"
"); // Append emojis separately (not in verbatim string) sb.AppendLine(); sb.Append(" 🏠 Home"); sb.AppendLine(); sb.Append(" 🔧 Hangfire Dashboard"); sb.AppendLine(); sb.Append(" 📋 Logs (Serilog.UI)"); sb.AppendLine(); sb.Append(" 📊 Health API"); sb.AppendLine(); sb.AppendLine("
"); } /// /// Appends the overall status header showing aggregate health and timestamp. /// /// The StringBuilder to append HTML to. /// The health check report. private static void AppendOverallStatus(StringBuilder sb, HealthReport report) { var statusClass = GetStatusClass(report.Status); var badgeClass = GetBadgeClass(report.Status); var icon = GetStatusIcon(report.Status); sb.Append($@"

"); sb.Append(icon); sb.Append($@" Health Check Status

Last checked: {DateTime.Now:yyyy-MM-dd HH:mm:ss} UTC

{report.Status}

"); } /// /// Appends the summary card displaying aggregate metrics (status, duration, check count). /// /// The StringBuilder to append HTML to. /// The health check report. private static void AppendSummaryCard(StringBuilder sb, HealthReport report) { var statusClass = GetStatusClass(report.Status); sb.Append(@"
"); sb.AppendLine(); sb.Append(" 📊 Summary"); sb.AppendLine(); sb.Append($@"
Overall Status: {report.Status}
Total Duration: {report.TotalDuration.TotalMilliseconds:F0} ms
Checks: {report.Entries.Count}
"); } /// /// Appends individual health check result cards with detailed metrics. /// /// The StringBuilder to append HTML to. /// The health check report containing check entries. private static void AppendIndividualChecks(StringBuilder sb, HealthReport report) { foreach (var entry in report.Entries) { var checkStatusClass = GetStatusClass(entry.Value.Status); var checkBadgeClass = GetBadgeClass(entry.Value.Status); var checkIcon = GetStatusIcon(entry.Value.Status); sb.Append($@"
"); sb.Append(checkIcon); sb.Append($@" {entry.Key} {entry.Value.Status}
Description: {entry.Value.Description ?? "N/A"}
Duration: {entry.Value.Duration.TotalMilliseconds:F0} ms
"); if (entry.Value.Exception != null) { sb.AppendLine($@"
Exception: {System.Net.WebUtility.HtmlEncode(entry.Value.Exception.Message)}
"); } if (entry.Value.Data.Any()) { AppendCheckDetails(sb, entry.Value.Data); } sb.AppendLine(@"
"); } sb.AppendLine(@"
"); } /// /// Appends a detail table for additional health check data (e.g., timestamps, counters). /// /// The StringBuilder to append HTML to. /// Dictionary of custom data provided by the health check. private static void AppendCheckDetails(StringBuilder sb, IReadOnlyDictionary data) { sb.AppendLine(@"
Details: "); foreach (var item in data) { var value = FormatDataValue(item.Value); sb.AppendLine($@" "); } sb.AppendLine(@"
{item.Key} {value}
"); } /// /// Appends the auto-refresh indicator widget with countdown timer. /// /// The StringBuilder to append HTML to. private static void AppendRefreshInfo(StringBuilder sb) { sb.AppendLine(@"
"); sb.AppendLine(); sb.Append(" Auto-refresh in 3s"); sb.AppendLine(); sb.AppendLine(@"
"); } /// /// Maps a health status to the corresponding CSS class name for styling. /// /// The health status. /// CSS class name (e.g., "status-healthy", "status-degraded"). private static string GetStatusClass(HealthStatus status) { return status switch { HealthStatus.Healthy => "status-healthy", HealthStatus.Degraded => "status-degraded", HealthStatus.Unhealthy => "status-unhealthy", _ => "" }; } /// /// Maps a health status to the corresponding Bootstrap badge CSS class. /// /// The health status. /// Badge CSS class name (e.g., "badge-healthy", "badge-degraded"). private static string GetBadgeClass(HealthStatus status) { return status switch { HealthStatus.Healthy => "badge-healthy", HealthStatus.Degraded => "badge-degraded", HealthStatus.Unhealthy => "badge-unhealthy", _ => "badge-secondary" }; } /// /// Maps a health status to the corresponding emoji icon. /// /// The health status. /// Unicode emoji character (✅ for healthy, ⚠️ for degraded, ❌ for unhealthy). private static string GetStatusIcon(HealthStatus status) { return status switch { HealthStatus.Healthy => "✅", HealthStatus.Degraded => "⚠️", HealthStatus.Unhealthy => "❌", _ => "❓" }; } /// /// Formats health check data values for display. /// Applies special formatting for common types (DateTime, TimeSpan). /// /// The data value to format. /// /// Formatted string representation: /// /// → "yyyy-MM-dd HH:mm:ss" /// → "{seconds}s" /// Other types → ToString() or "N/A" if null /// /// private static string FormatDataValue(object? value) { if (value == null) return "N/A"; if (value is DateTime dt) return dt.ToString("yyyy-MM-dd HH:mm:ss"); if (value is TimeSpan ts) return $"{ts.TotalSeconds:F1}s"; return value.ToString() ?? "N/A"; } }