feat: complete Nexus mission-control workflows

This commit is contained in:
2026-07-09 23:40:36 +02:00
parent 436ddfee0f
commit aaec3eb4ed
39 changed files with 3281 additions and 97 deletions
+38 -1
View File
@@ -1,8 +1,10 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using Nexus.Api.Data;
using Nexus.Api.DTOs;
using Nexus.Api.Models;
using Nexus.Api.Repositories;
using Nexus.Api.Services;
namespace Nexus.Api.Controllers;
@@ -10,7 +12,11 @@ namespace Nexus.Api.Controllers;
[Authorize]
[ApiController]
[Route("api/v1/tasks")]
public class TasksController(ITaskService taskService, IAgentService agentService, IConfiguration configuration) : ControllerBase
public class TasksController(
ITaskService taskService,
IAgentService agentService,
IConfiguration configuration,
IActivityRepository activityRepository) : ControllerBase
{
[HttpGet]
public async Task<IResult> GetAll(CancellationToken ct)
@@ -27,6 +33,7 @@ public class TasksController(ITaskService taskService, IAgentService agentServic
}
[HttpGet("pending-approval")]
[Authorize(Roles = "owner")]
public async Task<IResult> GetPendingApproval(CancellationToken ct)
{
var pending = await taskService.GetPendingApprovalAsync(ct);
@@ -34,9 +41,11 @@ public class TasksController(ITaskService taskService, IAgentService agentServic
}
[HttpPost("{id:guid}/approve")]
[Authorize(Roles = "owner")]
public async Task<IResult> Approve(Guid id, CancellationToken ct)
{
var result = await taskService.ApproveAsync(id, ct);
await WriteApprovalAuditAsync(id, "approve", result.Outcome, result.Task?.State, ct);
return result.Outcome switch
{
TaskOperationOutcome.NotFound => Results.NotFound(),
@@ -49,9 +58,11 @@ public class TasksController(ITaskService taskService, IAgentService agentServic
}
[HttpPost("{id:guid}/reject")]
[Authorize(Roles = "owner")]
public async Task<IResult> Reject(Guid id, CancellationToken ct)
{
var result = await taskService.RejectAsync(id, ct);
await WriteApprovalAuditAsync(id, "reject", result.Outcome, result.Task?.State, ct);
return result.Outcome switch
{
TaskOperationOutcome.NotFound => Results.NotFound(),
@@ -160,4 +171,30 @@ public class TasksController(ITaskService taskService, IAgentService agentServic
var count = await taskService.ResetStaleAsync(request.StaleHours, ct);
return Results.Ok(new ResetStaleResponse(count));
}
private async Task WriteApprovalAuditAsync(
Guid taskId,
string action,
TaskOperationOutcome outcome,
string? state,
CancellationToken ct)
{
await activityRepository.AddAsync(new ActivityEvent
{
Type = "task_approval_audit",
Message = $"Task approval task={taskId} action={action} caller={DescribeCaller(HttpContext.User)} outcome={outcome} checkpoint={(state ?? "none")}",
TaskId = taskId
}, ct);
}
private static string DescribeCaller(ClaimsPrincipal user)
{
var subject = user.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? user.FindFirst(ClaimTypes.Email)?.Value
?? user.Identity?.Name
?? "unknown";
var role = user.FindFirst(ClaimTypes.Role)?.Value ?? "owner";
return $"{role}:{subject}".ToLowerInvariant();
}
}