Files
jobtrackingapp/JobTrackerApi/Controllers/StrategySnapshotController.cs
T
2026-08-09 12:51:46 +02:00

87 lines
4.6 KiB
C#

using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace JobTrackerApi.Controllers;
[ApiController]
[Route("api/jobapplications/{jobId:int}/focus-plan")]
[Authorize(AuthenticationSchemes = "local", Policy = ProEntitlement.Policy)]
public sealed class StrategySnapshotController(
JobTrackerContext db,
AiOperationAdmission admission,
StrategySnapshotService snapshots) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<FocusPlanDto>> Get(int jobId, [FromQuery] string? attachmentIds, CancellationToken cancellationToken)
{
IReadOnlyList<int> ids;
try { ids = StrategySnapshotService.ParseAttachmentIds(attachmentIds); }
catch (StrategySnapshotValidationException exception) { return Problem(exception); }
var result = await snapshots.GetCachedAsync(jobId, StrategySnapshotService.NormalizeAttachmentIds(ids), cancellationToken);
return result is null
? NotFound(new { code = "strategy_not_generated", message = "No strategy snapshot has been generated for this context." })
: Ok(result);
}
[HttpPost("operations")]
public async Task<IActionResult> Enqueue(int jobId, [FromBody] StrategySnapshotRequest? request, CancellationToken cancellationToken)
{
try
{
var ids = StrategySnapshotService.ParseAttachmentIds(request?.AttachmentIds);
await snapshots.ValidateRequestAsync(jobId, ids, cancellationToken);
var signature = StrategySnapshotService.NormalizeAttachmentIds(ids);
var subject = StrategySnapshotService.EncodeSubject(jobId, ids);
var activeStatuses = new[] { OperationStatuses.Queued, OperationStatuses.Running, OperationStatuses.WaitingForRetry, OperationStatuses.WaitingForExternalFallback };
var active = await db.UserOperations.AsNoTracking()
.Where(item => item.TaskType == StrategySnapshotService.TaskType && item.SubjectId == subject && activeStatuses.Contains(item.Status))
.OrderByDescending(item => item.CreatedAtUtc)
.FirstOrDefaultAsync(cancellationToken);
if (active is not null)
{
var statusUrl = $"/api/operations/{active.Id:D}";
return Accepted(statusUrl, new StrategySnapshotOperationResponse(OperationDto.From(active), statusUrl, false));
}
var key = await snapshots.BuildIdempotencyKeyAsync(jobId, signature, cancellationToken);
var result = await admission.EnqueueAsync(
StrategySnapshotService.TaskType,
key,
"job_strategy",
subject,
AiOperationPriorities.Interactive,
cancellationToken);
return Accepted(result.StatusUrl, new StrategySnapshotOperationResponse(OperationDto.From(result.Operation), result.StatusUrl, result.Created));
}
catch (StrategySnapshotValidationException exception) { return Problem(exception); }
catch (AiOperationAdmissionException exception)
{
if (exception.RetryAfterSeconds is int seconds) Response.Headers.RetryAfter = seconds.ToString();
return StatusCode(exception.StatusCode, new { code = exception.Code, message = exception.Message });
}
}
[HttpGet("operation")]
public async Task<ActionResult<OperationDto>> LatestOperation(int jobId, [FromQuery] string? attachmentIds, CancellationToken cancellationToken)
{
IReadOnlyList<int> ids;
try { ids = StrategySnapshotService.ParseAttachmentIds(attachmentIds); }
catch (StrategySnapshotValidationException exception) { return Problem(exception); }
var subject = StrategySnapshotService.EncodeSubject(jobId, ids);
var operation = await db.UserOperations.AsNoTracking()
.Where(item => item.TaskType == StrategySnapshotService.TaskType && item.SubjectId == subject)
.OrderByDescending(item => item.CreatedAtUtc)
.FirstOrDefaultAsync(cancellationToken);
return operation is null ? NotFound() : Ok(OperationDto.From(operation));
}
private ObjectResult Problem(StrategySnapshotValidationException exception) =>
StatusCode(exception.StatusCode, new { code = exception.Code, message = exception.Message });
}
public sealed record StrategySnapshotRequest(string? AttachmentIds);
public sealed record StrategySnapshotOperationResponse(OperationDto Operation, string StatusUrl, bool Created);