#nullable enable using System.Text.Json; using Managing.Api.Models.Responses; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Shared; using Managing.Domain.Backtests; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using static Managing.Common.Enums; namespace Managing.Api.Controllers; /// /// Controller for managing job operations. /// Provides endpoints for querying job status and progress. /// Requires admin authorization for access. /// [ApiController] [Authorize] [Route("[controller]")] [Produces("application/json")] public class JobController : BaseController { private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IAdminConfigurationService _adminService; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// The service for user management. /// The service scope factory for creating scoped services. /// The admin configuration service for authorization checks. /// The logger instance. public JobController( IUserService userService, IServiceScopeFactory serviceScopeFactory, IAdminConfigurationService adminService, ILogger logger) : base(userService) { _serviceScopeFactory = serviceScopeFactory; _adminService = adminService; _logger = logger; } /// /// Checks if the current user is an admin /// private async Task IsUserAdmin() { try { var user = await GetUser(); return await _adminService.IsUserAdminAsync(user.Name); } catch (Exception ex) { _logger.LogError(ex, "Error checking if user is admin"); return false; } } /// /// Gets the status of a job by its ID. /// Admin only endpoint. /// /// The job ID to query /// The job status and result if completed [HttpGet("{jobId}")] public async Task> GetJobStatus(string jobId) { if (!await IsUserAdmin()) { _logger.LogWarning("Non-admin user attempted to access job status endpoint"); return StatusCode(403, new { error = "Only admin users can access job status" }); } if (!Guid.TryParse(jobId, out var jobGuid)) { return BadRequest("Invalid job ID format. Must be a valid GUID."); } using var serviceScope = _serviceScopeFactory.CreateScope(); var jobRepository = serviceScope.ServiceProvider.GetRequiredService(); var job = await jobRepository.GetByIdAsync(jobGuid); if (job == null) { return NotFound($"Job with ID {jobId} not found."); } var response = new JobStatusResponse { JobId = job.Id, Status = job.Status.ToString(), ProgressPercentage = job.ProgressPercentage, CreatedAt = job.CreatedAt, StartedAt = job.StartedAt, CompletedAt = job.CompletedAt, ErrorMessage = job.ErrorMessage, Result = job.Status == JobStatus.Completed && !string.IsNullOrEmpty(job.ResultJson) ? JsonSerializer.Deserialize(job.ResultJson) : null }; return Ok(response); } /// /// Gets a paginated list of jobs with optional filters and sorting. /// Admin only endpoint. /// /// Page number (defaults to 1) /// Number of items per page (defaults to 50, max 100) /// Field to sort by (CreatedAt, StartedAt, CompletedAt, Priority, Status, JobType) - defaults to CreatedAt /// Sort order - "asc" or "desc" (defaults to "desc") /// Optional status filter (Pending, Running, Completed, Failed, Cancelled) /// Optional job type filter (Backtest, GeneticBacktest) /// Optional user ID filter /// Optional worker ID filter /// Optional bundle request ID filter /// A paginated list of jobs [HttpGet] public async Task> GetJobs( [FromQuery] int page = 1, [FromQuery] int pageSize = 50, [FromQuery] string sortBy = "CreatedAt", [FromQuery] string sortOrder = "desc", [FromQuery] string? status = null, [FromQuery] string? jobType = null, [FromQuery] int? userId = null, [FromQuery] string? workerId = null, [FromQuery] string? bundleRequestId = null) { if (!await IsUserAdmin()) { _logger.LogWarning("Non-admin user attempted to list jobs"); return StatusCode(403, new { error = "Only admin users can list jobs" }); } // Validate pagination parameters if (page < 1) { return BadRequest("Page must be greater than 0"); } if (pageSize < 1 || pageSize > 100) { return BadRequest("Page size must be between 1 and 100"); } if (sortOrder != "asc" && sortOrder != "desc") { return BadRequest("Sort order must be 'asc' or 'desc'"); } // Parse status filter JobStatus? statusFilter = null; if (!string.IsNullOrEmpty(status)) { if (Enum.TryParse(status, true, out var parsedStatus)) { statusFilter = parsedStatus; } else { return BadRequest($"Invalid status value. Valid values are: {string.Join(", ", Enum.GetNames())}"); } } // Parse job type filter JobType? jobTypeFilter = null; if (!string.IsNullOrEmpty(jobType)) { if (Enum.TryParse(jobType, true, out var parsedJobType)) { jobTypeFilter = parsedJobType; } else { return BadRequest($"Invalid job type value. Valid values are: {string.Join(", ", Enum.GetNames())}"); } } // Parse bundle request ID Guid? bundleRequestIdFilter = null; if (!string.IsNullOrEmpty(bundleRequestId)) { if (!Guid.TryParse(bundleRequestId, out var bundleGuid)) { return BadRequest("Invalid bundle request ID format. Must be a valid GUID."); } bundleRequestIdFilter = bundleGuid; } using var serviceScope = _serviceScopeFactory.CreateScope(); var jobRepository = serviceScope.ServiceProvider.GetRequiredService(); var (jobs, totalCount) = await jobRepository.GetPaginatedAsync( page, pageSize, sortBy, sortOrder, statusFilter, jobTypeFilter, userId, workerId, bundleRequestIdFilter); var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); var response = new PaginatedJobsResponse { Jobs = jobs.Select(j => new JobListItemResponse { JobId = j.Id, Status = j.Status.ToString(), JobType = j.JobType.ToString(), ProgressPercentage = j.ProgressPercentage, Priority = j.Priority, UserId = j.UserId, BundleRequestId = j.BundleRequestId, GeneticRequestId = j.GeneticRequestId, AssignedWorkerId = j.AssignedWorkerId, CreatedAt = j.CreatedAt, StartedAt = j.StartedAt, CompletedAt = j.CompletedAt, LastHeartbeat = j.LastHeartbeat, ErrorMessage = j.ErrorMessage, StartDate = j.StartDate, EndDate = j.EndDate }).ToList(), TotalCount = totalCount, CurrentPage = page, PageSize = pageSize, TotalPages = totalPages, HasNextPage = page < totalPages, HasPreviousPage = page > 1 }; return Ok(response); } /// /// Gets a summary of jobs grouped by status and job type with counts. /// Admin only endpoint. /// /// Summary statistics of jobs [HttpGet("summary")] public async Task> GetJobSummary() { if (!await IsUserAdmin()) { _logger.LogWarning("Non-admin user attempted to get job summary"); return StatusCode(403, new { error = "Only admin users can access job summary" }); } using var serviceScope = _serviceScopeFactory.CreateScope(); var jobRepository = serviceScope.ServiceProvider.GetRequiredService(); var summary = await jobRepository.GetSummaryAsync(); var response = new JobSummaryResponse { StatusSummary = summary.StatusCounts.Select(s => new JobStatusSummary { Status = s.Status.ToString(), Count = s.Count }).ToList(), JobTypeSummary = summary.JobTypeCounts.Select(j => new JobTypeSummary { JobType = j.JobType.ToString(), Count = j.Count }).ToList(), StatusTypeSummary = summary.StatusTypeCounts.Select(st => new JobStatusTypeSummary { Status = st.Status.ToString(), JobType = st.JobType.ToString(), Count = st.Count }).ToList(), TotalJobs = summary.TotalJobs }; return Ok(response); } }