namespace marlin.web.Controllers; using core.models; using Data; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Models; using Util; [Route("jobs")] public class JobsController(ApplicationDbContext dbContext) : Controller { // list jobs (pending, active, cancelled, success, failed), filter to vcs, filter to recipe public record JobListFilter(JobStatus? Status = null, Guid? Vcs = null, Guid? Branch = null, string? Recipe = null, Guid? Agent = null); [HttpGet("list")] public async Task List([FromQuery] JobListFilter filter, CancellationToken ct) { var results = await dbContext .Jobs .WhereIf(filter.Status != null, x => x.Status == filter.Status) .WhereIf(filter.Vcs != null, x => x.SourceId == filter.Vcs) .WhereIf(filter.Branch != null, x => x.BranchId == filter.Branch) .WhereIf(filter.Recipe != null, x => x.Recipe == filter.Recipe) .WhereIf(filter.Agent != null, x => x.AgentId == filter.Agent) .OrderByDescending(x => x.CreatedAt) .Include(x => x.Commit) .Include(x => x.Branch) .Include(x => x.Source) .Include(x => x.Agent) .ToListAsync(ct); return this.View(results); } public record JobSpawnDetails( ICollection Sources, Guid? Vcs = null, string? Ref = null, string? Commit = null, string? Recipe = null); [HttpGet("spawn")] public async Task Spawn(CancellationToken ct) { var sources = await dbContext .Sources .Include(x => x.Branches) .ThenInclude(x => x.Commit) .ThenInclude(x => x.WorkflowSnapshot) .ToListAsync(ct); var details = new JobSpawnDetails(sources); return this.View(details); } public record JobSpawnRequest(Guid Vcs, Guid Branch, string Recipe); [HttpPost("spawn")] public async Task SpawnPost(JobSpawnRequest request, CancellationToken ct) { var branch = await dbContext .SourceBranches .FirstOrDefaultAsync(x => x.Id == request.Branch, ct); if (branch == null) return this.NotFound(); Job job = new() { SourceId = branch.SourceId, BranchId = branch.Id, CommitId = branch.CommitId, Recipe = request.Recipe, CreatedAt = DateTime.UtcNow, CreatedReason = "user created", Status = JobStatus.Pending }; await dbContext.Jobs.AddAsync(job, ct); await dbContext.SaveChangesAsync(ct); return this.RedirectToAction("Details", new { id = job.Id }); } // cancel job // details job (recipe, status, agent, target, plan, log, artifacts) [HttpGet("details/{id}")] public async Task Details(Guid id) { var job = await dbContext.Jobs .Include(x => x.Agent) .Include(x => x.Source) .Include(x => x.StatusUpdates) .Include(x => x.Logs) .Include(x => x.Branch) .Include(x => x.Commit) .AsSplitQuery() .FirstOrDefaultAsync(x => x.Id == id); if (job == null) return this.NotFound(); return this.View(job); } public async Task Logs(Guid id) { var job = await dbContext.Jobs .Include(x => x.Logs.OrderBy(x => x.At)) .FirstOrDefaultAsync(x => x.Id == id); if (job == null) return this.NotFound(); return this.View(job); } }