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