diff --git a/ECMJobRunner.WebCron/AllowAllDashboardAuthorizationFilter.cs b/ECMJobRunner.WebCron/AllowAllDashboardAuthorizationFilter.cs
index 38a0616..8a2ef3c 100644
--- a/ECMJobRunner.WebCron/AllowAllDashboardAuthorizationFilter.cs
+++ b/ECMJobRunner.WebCron/AllowAllDashboardAuthorizationFilter.cs
@@ -8,6 +8,11 @@ namespace ECMJobRunner.WebCron
///
public class AllowAllDashboardAuthorizationFilter : IDashboardAuthorizationFilter
{
+ ///
+ /// Determines whether the current user is authorized to access the Hangfire Dashboard.
+ ///
+ /// The Hangfire dashboard context containing request information.
+ /// Always returns true to allow all users (development only).
public bool Authorize(DashboardContext context)
{
// Allow all users - FOR DEVELOPMENT ONLY
diff --git a/ECMJobRunner.WebCron/Extensions/DtoExtensions.cs b/ECMJobRunner.WebCron/Extensions/DtoExtensions.cs
index 0441f38..b2c9676 100644
--- a/ECMJobRunner.WebCron/Extensions/DtoExtensions.cs
+++ b/ECMJobRunner.WebCron/Extensions/DtoExtensions.cs
@@ -4,13 +4,30 @@ using MediatR;
namespace ECMJobRunner.WebCron.Extensions;
+///
+/// Extension methods for profile DTOs to support Hangfire job operations.
+///
public static class DtoExtensions
{
+ ///
+ /// Generates a unique Hangfire job identifier for a profile.
+ ///
+ /// The profile configuration DTO.
+ /// A unique job identifier in the format "profile-{Id}-{normalized-name}".
+ ///
+ /// Example: profile with Id=123 and ProfileName="Import Data"
+ /// returns "profile-123-import_data"
+ ///
public static string JobId(this CfgProfileDto profile)
{
return $"profile-{profile.Id}-{profile.ProfileName.Replace(' ', '_').ToLowerInvariant()}";
}
+ ///
+ /// Converts a profile DTO to a DEX job batch command.
+ ///
+ /// The profile configuration DTO.
+ /// A ready to execute the profile job.
public static TriggeringDEXJobBatchCommand ToJob(this CfgProfileDto profile)
{
return new TriggeringDEXJobBatchCommand
diff --git a/ECMJobRunner.WebCron/HealthCheck/HealthCheckHtmlGenerator.cs b/ECMJobRunner.WebCron/HealthCheck/HealthCheckHtmlGenerator.cs
index 871c515..718108e 100644
--- a/ECMJobRunner.WebCron/HealthCheck/HealthCheckHtmlGenerator.cs
+++ b/ECMJobRunner.WebCron/HealthCheck/HealthCheckHtmlGenerator.cs
@@ -4,10 +4,32 @@ using System.Text;
namespace ECMJobRunner.WebCron.HealthCheck;
///
-/// Generates HTML representation of health check reports.
+/// 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
{
+ ///
+ /// 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();
@@ -47,6 +69,10 @@ public static class HealthCheckHtmlGenerator
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(@"
@@ -66,6 +92,11 @@ public static class HealthCheckHtmlGenerator
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);
@@ -91,6 +122,11 @@ public static class HealthCheckHtmlGenerator
");
}
+ ///
+ /// 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);
@@ -122,6 +158,11 @@ public static class HealthCheckHtmlGenerator
");
}
+ ///
+ /// 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)
@@ -175,6 +216,11 @@ public static class HealthCheckHtmlGenerator
");
}
+ ///
+ /// 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(@"
@@ -198,6 +244,10 @@ public static class HealthCheckHtmlGenerator
");
}
+ ///
+ /// Appends the auto-refresh indicator widget with countdown timer.
+ ///
+ /// The StringBuilder to append HTML to.
private static void AppendRefreshInfo(StringBuilder sb)
{
sb.AppendLine(@"
@@ -208,6 +258,11 @@ public static class HealthCheckHtmlGenerator
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
@@ -219,6 +274,11 @@ public static class HealthCheckHtmlGenerator
};
}
+ ///
+ /// 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
@@ -230,6 +290,11 @@ public static class HealthCheckHtmlGenerator
};
}
+ ///
+ /// 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
@@ -241,6 +306,19 @@ public static class HealthCheckHtmlGenerator
};
}
+ ///
+ /// 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)
diff --git a/ECMJobRunner.WebCron/ProfileWorker/DependencyInjection.cs b/ECMJobRunner.WebCron/ProfileWorker/DependencyInjection.cs
index c5e38ef..0bd4778 100644
--- a/ECMJobRunner.WebCron/ProfileWorker/DependencyInjection.cs
+++ b/ECMJobRunner.WebCron/ProfileWorker/DependencyInjection.cs
@@ -2,8 +2,26 @@
namespace ECMJobRunner.WebCron.ProfileWorker;
+///
+/// Extension methods for configuring ProfileWorker services in the DI container.
+///
public static class DependencyInjection
{
+ ///
+ /// Registers ProfileWorker background service and related dependencies.
+ ///
+ /// The service collection to configure.
+ /// Application configuration containing ProfileWorker settings.
+ /// The configured service collection for method chaining.
+ ///
+ /// Registers the following services:
+ ///
+ /// - - Configuration options from appsettings.json
+ /// - - Singleton background service (also registered as IHostedService)
+ /// - - Singleton cache for profile state
+ /// - - Scoped service for profile synchronization logic
+ ///
+ ///
public static IServiceCollection AddProfileWorker(this IServiceCollection services, IConfiguration configuration)
{
// Configure ProfileWorker options from appsettings.json
@@ -25,10 +43,27 @@ public static class DependencyInjection
}
///
-/// Validates ProfileWorkerOptions configuration at startup.
+/// Validates configuration at application startup.
+/// Ensures IntervalMS is within acceptable bounds to prevent misconfiguration.
///
internal class ProfileWorkerOptionsValidator : IValidateOptions
{
+ ///
+ /// Validates the ProfileWorker options.
+ ///
+ /// The name of the options instance (not used).
+ /// The options to validate.
+ ///
+ /// if valid,
+ /// or with error message if invalid.
+ ///
+ ///
+ /// Validation rules:
+ ///
+ /// - IntervalMS must be greater than 0
+ /// - IntervalMS should be at least 100ms to avoid excessive CPU usage
+ ///
+ ///
public ValidateOptionsResult Validate(string? name, ProfileWorkerOptions options)
{
if (options.IntervalMS <= 0)
diff --git a/ECMJobRunner.WebCron/ProfileWorker/ProfileCache.cs b/ECMJobRunner.WebCron/ProfileWorker/ProfileCache.cs
index 70ac756..a0ccd7b 100644
--- a/ECMJobRunner.WebCron/ProfileWorker/ProfileCache.cs
+++ b/ECMJobRunner.WebCron/ProfileWorker/ProfileCache.cs
@@ -5,19 +5,46 @@ namespace ECMJobRunner.WebCron.ProfileWorker;
///
/// Thread-safe cache for storing active profile configurations.
-/// Used to track profile state and detect changes in schedule or removal.
+/// Uses to track profile state
+/// and detect changes in schedule or removal.
///
public class ProfileCache
{
private readonly ConcurrentDictionary _cache = new();
+ ///
+ /// Retrieves a profile from the cache by job identifier.
+ ///
+ /// The unique job identifier.
+ /// The cached profile, or null if not found.
public CfgProfileDto? Get(string jobId) => _cache.TryGetValue(jobId, out var profile) ? profile : null;
+ ///
+ /// Adds a new profile or updates an existing profile in the cache.
+ ///
+ /// The unique job identifier.
+ /// The profile configuration to cache.
public void AddOrUpdate(string jobId, CfgProfileDto profile) => _cache[jobId] = profile;
+ ///
+ /// Attempts to remove a profile from the cache.
+ ///
+ /// The unique job identifier.
+ /// The removed profile, or null if not found.
+ /// true if the profile was removed; otherwise, false.
public bool TryRemove(string jobId, out CfgProfileDto? profile) => _cache.TryRemove(jobId, out profile);
+ ///
+ /// Gets all job identifiers currently stored in the cache.
+ ///
+ /// A collection of job identifiers.
public IEnumerable GetAllJobIds() => _cache.Keys;
+ ///
+ /// Attempts to retrieve a profile from the cache.
+ ///
+ /// The unique job identifier.
+ /// The cached profile, or null if not found.
+ /// true if the profile was found; otherwise, false.
public bool TryGetValue(string jobId, out CfgProfileDto? profile) => _cache.TryGetValue(jobId, out profile);
}
diff --git a/ECMJobRunner.WebCron/ProfileWorker/ProfileWork.cs b/ECMJobRunner.WebCron/ProfileWorker/ProfileWork.cs
index 40a220b..59e1334 100644
--- a/ECMJobRunner.WebCron/ProfileWorker/ProfileWork.cs
+++ b/ECMJobRunner.WebCron/ProfileWorker/ProfileWork.cs
@@ -5,9 +5,31 @@ using MediatR;
namespace ECMJobRunner.WebCron.ProfileWorker;
-
+///
+/// Handles the core business logic for synchronizing profiles between the database and Hangfire.
+/// Responsible for adding, updating, and removing recurring jobs based on profile configuration.
+///
+/// Logger for diagnostic output.
+/// Hangfire recurring job manager.
+/// MediatR mediator for executing commands and queries.
+/// Thread-safe cache for tracking profile state.
public class ProfileWork(ILogger Logger, IRecurringJobManager JobManager, IMediator Mediator, ProfileCache ProfileCache)
{
+ ///
+ /// Synchronizes active profiles from the database with Hangfire recurring jobs.
+ ///
+ /// Cancellation token for graceful shutdown.
+ ///
+ /// Execution flow:
+ ///
+ /// - Fetches active profiles from database via MediatR query
+ /// - Removes jobs from Hangfire that no longer exist in the database
+ /// - Adds or updates jobs with changed schedules
+ /// - Updates local cache to reflect current state
+ ///
+ /// Uses HashSet for O(1) job existence checks to optimize performance.
+ ///
+ /// A task representing the asynchronous synchronization operation.
public async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Fetch active profiles from database
diff --git a/ECMJobRunner.WebCron/ProfileWorker/ProfileWorker.cs b/ECMJobRunner.WebCron/ProfileWorker/ProfileWorker.cs
index 26d04d5..13a9e48 100644
--- a/ECMJobRunner.WebCron/ProfileWorker/ProfileWorker.cs
+++ b/ECMJobRunner.WebCron/ProfileWorker/ProfileWorker.cs
@@ -3,6 +3,28 @@ using Microsoft.Extensions.Options;
namespace ECMJobRunner.WebCron.ProfileWorker;
+///
+/// Background service that continuously synchronizes active profiles with Hangfire recurring jobs.
+/// Implements health check monitoring to track service status and failure conditions.
+///
+/// Logger for diagnostic output.
+/// Factory for creating service scopes (required for scoped service resolution).
+/// Configuration options for interval timing.
+///
+/// This service runs on a configurable interval (default 1 second) and performs the following:
+///
+/// - Fetches active profiles from the database
+/// - Synchronizes profiles with Hangfire recurring jobs
+/// - Tracks health status based on success/failure patterns
+/// - Gracefully handles cancellation during application shutdown
+///
+/// Health states:
+///
+/// - Healthy: Last successful run within 3x interval
+/// - Degraded: 1-2 consecutive failures but within time limit
+/// - Unhealthy: No success for 3x interval or 3+ consecutive failures
+///
+///
public class ProfileWorker(
ILogger Logger,
IServiceScopeFactory ScopeFactory,
@@ -15,6 +37,19 @@ public class ProfileWorker(
private int _consecutiveFailures = 0;
private Exception? _lastException = null;
+ ///
+ /// Background service execution loop that synchronizes profiles on a configurable interval.
+ ///
+ /// Cancellation token for graceful shutdown.
+ /// A task representing the background execution.
+ ///
+ /// The loop continues until:
+ ///
+ /// - Application shutdown is requested (via stoppingToken)
+ /// - An unhandled exception causes service failure
+ ///
+ /// Uses scoped services for each iteration to ensure proper lifetime management.
+ ///
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Logger.LogInformation("ProfileWorker started");
@@ -61,6 +96,28 @@ public class ProfileWorker(
Logger.LogInformation("ProfileWorker stopped");
}
+ ///
+ /// Performs a health check by evaluating recent success/failure patterns.
+ ///
+ /// Health check context (unused).
+ /// Cancellation token.
+ ///
+ /// A indicating the current health status:
+ ///
+ /// - - Service running normally
+ /// - - Recent failures but still operational
+ /// - - No successful run for extended period
+ ///
+ ///
+ ///
+ /// Health determination logic:
+ ///
+ /// - Unhealthy: Time since last success exceeds 3x the configured interval
+ /// - Degraded: 1+ consecutive failures within time threshold
+ /// - Healthy: Recent successful run with no failures
+ ///
+ /// Includes diagnostic data in the result for monitoring and alerting.
+ ///
public Task CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
diff --git a/ECMJobRunner.WebCron/ProfileWorker/ProfileWorkerOptions.cs b/ECMJobRunner.WebCron/ProfileWorker/ProfileWorkerOptions.cs
index c69d367..2803ae7 100644
--- a/ECMJobRunner.WebCron/ProfileWorker/ProfileWorkerOptions.cs
+++ b/ECMJobRunner.WebCron/ProfileWorker/ProfileWorkerOptions.cs
@@ -2,14 +2,22 @@ namespace ECMJobRunner.WebCron.ProfileWorker;
///
/// Configuration options for ProfileWorker background service.
+/// Binds to "ProfileWorker" section in appsettings.json.
///
public class ProfileWorkerOptions
{
+ ///
+ /// Configuration section name for binding options.
+ ///
public const string SectionName = "ProfileWorker";
///
/// Interval in milliseconds between profile synchronization checks.
- /// Default: 1000ms (1 second)
+ /// Default: 1000ms (1 second).
///
+ ///
+ /// Lower values increase responsiveness but consume more resources.
+ /// Recommended range: 1000-5000ms.
+ ///
public int IntervalMS { get; set; } = 1000;
}