using ControlPlane.Api.Services; using ControlPlane.Core.Models; using ControlPlane.Core.Services; using System.Text.Json; namespace ControlPlane.Api.Endpoints; public static class ProjectBuildEndpoints { private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web) { Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }, }; public static IEndpointRouteBuilder MapProjectBuildEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/builds").WithTags("Builds"); group.MapGet("/projects", GetProjects); group.MapGet("/history", GetHistory); group.MapGet("/by-sha", GetBySha); group.MapPost("/{projectName}", TriggerProjectBuild); return app; } /// Returns the list of known projects the build monitor can track. private static IResult GetProjects(ProjectBuildService svc) => Results.Ok(svc.GetProjects()); private static async Task GetHistory(BuildHistoryService history) => Results.Ok(await history.GetBuildsAsync()); /// /// Returns the latest SolutionBuild record for the given commit SHA, or null if none exists. /// Used by the OPC CommitsTab to render per-commit CI status dots. /// private static async Task GetBySha(string sha, BuildHistoryService history) { if (string.IsNullOrWhiteSpace(sha)) return Results.BadRequest("sha is required"); var builds = await history.GetBuildsByShaAsync(sha); var latest = builds .Where(b => b.Kind == BuildKind.SolutionBuild) .MaxBy(b => b.StartedAt); return Results.Ok(latest is null ? null : new { latest.Id, latest.Status, latest.StartedAt, latest.FinishedAt, latest.DurationMs, latest.CommitSha, }); } /// /// Triggers a build for a named project and streams SSE output. /// projectName must match one of the names returned by GET /api/builds/projects. /// private static async Task TriggerProjectBuild( string projectName, HttpContext ctx, ProjectBuildService svc, CancellationToken ct) { if (string.IsNullOrWhiteSpace(svc.RepoRoot)) { ctx.Response.StatusCode = 503; await ctx.Response.WriteAsJsonAsync( new { error = "Docker:RepoRoot is not configured on the server." }, ct); return; } ctx.Response.Headers.ContentType = "text/event-stream"; ctx.Response.Headers.CacheControl = "no-cache"; ctx.Response.Headers.Connection = "keep-alive"; async Task Send(object payload) { var json = JsonSerializer.Serialize(payload, JsonOpts); await ctx.Response.WriteAsync($"data: {json}\n\n", ct); await ctx.Response.Body.FlushAsync(ct); } void OnLine(string line) => Send(new { line }).GetAwaiter().GetResult(); var record = await svc.BuildProjectAsync(projectName, OnLine, ct); await Send(new { done = true, build = record }); } }