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; }