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