diff --git a/ECMJobRunner.WebCron/HealthCheck/HealthCheckHtmlGenerator.cs b/ECMJobRunner.WebCron/HealthCheck/HealthCheckHtmlGenerator.cs new file mode 100644 index 0000000..871c515 --- /dev/null +++ b/ECMJobRunner.WebCron/HealthCheck/HealthCheckHtmlGenerator.cs @@ -0,0 +1,257 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using System.Text; + +namespace ECMJobRunner.WebCron.HealthCheck; + +/// +/// Generates HTML representation of health check reports. +/// +public static class HealthCheckHtmlGenerator +{ + 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(); + } + + 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("
"); + } + + 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.UtcNow:yyyy-MM-dd HH:mm:ss} UTC

+
+
+

{report.Status}

+
+
+
+ +
+
"); + } + + 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} +
+
+
+
+ +
"); + } + + 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(@" +
+
+
"); + } + + 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}
+
"); + } + + private static void AppendRefreshInfo(StringBuilder sb) + { + sb.AppendLine(@" +
"); + sb.AppendLine(); + sb.Append(" Auto-refresh in 10s"); + sb.AppendLine(); + sb.AppendLine(@"
"); + } + + private static string GetStatusClass(HealthStatus status) + { + return status switch + { + HealthStatus.Healthy => "status-healthy", + HealthStatus.Degraded => "status-degraded", + HealthStatus.Unhealthy => "status-unhealthy", + _ => "" + }; + } + + private static string GetBadgeClass(HealthStatus status) + { + return status switch + { + HealthStatus.Healthy => "badge-healthy", + HealthStatus.Degraded => "badge-degraded", + HealthStatus.Unhealthy => "badge-unhealthy", + _ => "badge-secondary" + }; + } + + private static string GetStatusIcon(HealthStatus status) + { + return status switch + { + HealthStatus.Healthy => "✅", + HealthStatus.Degraded => "⚠️", + HealthStatus.Unhealthy => "❌", + _ => "❓" + }; + } + + 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"; + } +} diff --git a/ECMJobRunner.WebCron/Program.cs b/ECMJobRunner.WebCron/Program.cs index 19c1177..c961fb9 100644 --- a/ECMJobRunner.WebCron/Program.cs +++ b/ECMJobRunner.WebCron/Program.cs @@ -1,6 +1,7 @@ using ECMJobRunner.Application; using ECMJobRunner.Infrastructure; using ECMJobRunner.WebCron; +using ECMJobRunner.WebCron.HealthCheck; using ECMJobRunner.WebCron.ProfileWorker; using Hangfire; using Hangfire.SqlServer; @@ -10,7 +11,6 @@ using Serilog; using Serilog.Ui.Core.Extensions; using Serilog.Ui.SqliteDataProvider.Extensions; using Serilog.Ui.Web.Extensions; -using System.Text; // Enable Serilog self-diagnostics Serilog.Debugging.SelfLog.Enable(msg => Console.WriteLine($"[SERILOG] {msg}")); @@ -148,7 +148,7 @@ app.MapGet("/health-ui", async (HealthCheckService healthCheckService, HttpConte { var report = await healthCheckService.CheckHealthAsync(context.RequestAborted); - var html = GenerateHealthCheckHtml(report); + var html = HealthCheckHtmlGenerator.Generate(report); context.Response.ContentType = "text/html"; await context.Response.WriteAsync(html); @@ -204,216 +204,3 @@ finally { Log.CloseAndFlush(); } - -// Helper method to generate health check HTML -static string GenerateHealthCheckHtml(HealthReport report) -{ - var sb = new StringBuilder(); - - sb.AppendLine(@" - - - - - - Health Check - ECMJobRunner - - - - -"); - - // Navigation - 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("
"); - - // Overall Status Header - var statusClass = report.Status switch - { - HealthStatus.Healthy => "status-healthy", - HealthStatus.Degraded => "status-degraded", - HealthStatus.Unhealthy => "status-unhealthy", - _ => "" - }; - - var badgeClass = report.Status switch - { - HealthStatus.Healthy => "badge-healthy", - HealthStatus.Degraded => "badge-degraded", - HealthStatus.Unhealthy => "badge-unhealthy", - _ => "badge-secondary" - }; - - var icon = report.Status switch - { - HealthStatus.Healthy => "✅", - HealthStatus.Degraded => "⚠️", - HealthStatus.Unhealthy => "❌", - _ => "❓" - }; - - sb.Append($@" -
-
-
-

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

-

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

-
-
-

{report.Status}

-
-
-
- -
-
-
-
"); - sb.AppendLine(); - sb.Append(" 📊 Summary"); - sb.AppendLine(); - sb.Append($@"
-
-
- Overall Status: - {report.Status} -
-
- Total Duration: - {report.TotalDuration.TotalMilliseconds:F0} ms -
-
- Checks: - {report.Entries.Count} -
-
-
-
- -
"); - - // Individual Checks - foreach (var entry in report.Entries) - { - var checkStatusClass = entry.Value.Status switch - { - HealthStatus.Healthy => "status-healthy", - HealthStatus.Degraded => "status-degraded", - HealthStatus.Unhealthy => "status-unhealthy", - _ => "" - }; - - var checkBadgeClass = entry.Value.Status switch - { - HealthStatus.Healthy => "badge-healthy", - HealthStatus.Degraded => "badge-degraded", - HealthStatus.Unhealthy => "badge-unhealthy", - _ => "badge-secondary" - }; - - var checkIcon = entry.Value.Status switch - { - HealthStatus.Healthy => "✅", - HealthStatus.Degraded => "⚠️", - HealthStatus.Unhealthy => "❌", - _ => "❓" - }; - - 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()) - { - sb.AppendLine(@" -
- Details: - "); - - foreach (var data in entry.Value.Data) - { - var value = data.Value?.ToString() ?? "N/A"; - - // Format DateTime values - if (data.Value is DateTime dt) - { - value = dt.ToString("yyyy-MM-dd HH:mm:ss"); - } - else if (data.Value is TimeSpan ts) - { - value = $"{ts.TotalSeconds:F1}s"; - } - - sb.AppendLine($@" - - - - "); - } - - sb.AppendLine(@" -
{data.Key}{value}
-
"); - } - - sb.AppendLine(@" -
-
"); - } - - sb.AppendLine(@" -
-
-
- -
"); - sb.AppendLine(); - sb.Append(" Auto-refresh in 10s"); - sb.AppendLine(); - sb.AppendLine(@"
- - -"); - - return sb.ToString(); -}