feat: Phase 2 — Delegated State, Auth, Review-Gate, Notifications, Zombie-Reset
CI - Build & Test / Backend (.NET) (push) Successful in 37s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 24s
CI - Build & Test / Security Check (push) Successful in 4s

This commit is contained in:
2026-06-18 23:47:41 +02:00
parent 12998170e3
commit dcc8450c62
32 changed files with 1758 additions and 38 deletions
+75 -6
View File
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Nexus.Api.Data;
using Nexus.Api.Models;
@@ -5,9 +7,13 @@ using Nexus.Api.Services;
namespace Nexus.Api.Controllers;
[Authorize]
[ApiController]
[Route("api/dashboard")]
public class DashboardController(IDashboardService dashboardService, ITaskService taskService) : ControllerBase
public class DashboardController(
IDashboardService dashboardService,
ITaskService taskService,
IHttpContextAccessor httpContextAccessor) : ControllerBase
{
[HttpGet("status")]
public async Task<DashboardStatus> GetStatus()
@@ -115,9 +121,16 @@ public class DashboardController(IDashboardService dashboardService, ITaskServic
if (string.IsNullOrWhiteSpace(request.Title))
return BadRequest(new { error = "Title is required." });
var task = await taskService.CreateDashboardTaskAsync(
request.Title, request.Detail, request.Source, request.Priority, request.AssignedTo, ct);
return Created($"/api/dashboard/tasks/{task.Id}", MapToDto(task));
try
{
var task = await taskService.CreateDashboardTaskAsync(
request.Title, request.Detail, request.Source, request.Priority, request.AssignedTo, request.ParentTaskId, ct);
return Created($"/api/dashboard/tasks/{task.Id}", MapToDto(task));
}
catch (ArgumentException ex)
{
return BadRequest(new { error = ex.Message });
}
}
[HttpPut("tasks/{id:guid}")]
@@ -125,7 +138,7 @@ public class DashboardController(IDashboardService dashboardService, ITaskServic
Guid id, [FromBody] UpdateDashboardTaskRequest request, CancellationToken ct)
{
var result = await taskService.UpdateDashboardTaskAsync(
id, request.Title, request.Detail, request.Source, request.Priority, request.AssignedTo, ct);
id, request.Title, request.Detail, request.Source, request.Priority, request.AssignedTo, request.DueDate, ct);
return result.Outcome switch
{
TaskOperationOutcome.NotFound => NotFound(new { error = "Task not found." }),
@@ -149,6 +162,20 @@ public class DashboardController(IDashboardService dashboardService, ITaskServic
public async Task<ActionResult<DashboardTaskDto>> UpdateTaskStatus(
Guid id, [FromBody] UpdateDashboardTaskStatusRequest request, CancellationToken ct)
{
// Bao review gate: Check if moving OUT of Review
var currentTask = await taskService.GetByIdAsync(id, ct);
if (currentTask is not null &&
string.Equals(currentTask.State, "Review", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(request.Status, "Review", StringComparison.OrdinalIgnoreCase))
{
var user = httpContextAccessor.HttpContext?.User;
var isOwner = user?.IsInRole("Owner") == true ||
user?.IsInRole("owner") == true ||
user?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value == "bao";
if (!isOwner)
return StatusCode(403, new { error = "Only the owner can move tasks out of Review." });
}
var result = await taskService.UpdateStatusAsync(id, request.Status, ct);
return result.Outcome switch
{
@@ -171,6 +198,20 @@ public class DashboardController(IDashboardService dashboardService, ITaskServic
if (string.IsNullOrWhiteSpace(request.State))
return BadRequest(new { error = "State is required." });
// Bao review gate: Check if moving OUT of Review
var currentTask = await taskService.GetByIdAsync(id, ct);
if (currentTask is not null &&
string.Equals(currentTask.State, "Review", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(request.State, "Review", StringComparison.OrdinalIgnoreCase))
{
var user = httpContextAccessor.HttpContext?.User;
var isOwner = user?.IsInRole("Owner") == true ||
user?.IsInRole("owner") == true ||
user?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value == "bao";
if (!isOwner)
return StatusCode(403, new { error = "Only the owner can move tasks out of Review." });
}
var result = await taskService.MoveTaskAsync(id, request.State, ct);
return result.Outcome switch
{
@@ -180,6 +221,33 @@ public class DashboardController(IDashboardService dashboardService, ITaskServic
};
}
// ── New Endpoints: Reset Stale, Children, Activity ──
[HttpPost("tasks/reset-stale")]
public async Task<ActionResult<ResetStaleResponse>> ResetStale(
[FromBody] ResetStaleRequest request, CancellationToken ct)
{
var threshold = TimeSpan.FromHours(Math.Max(1, request.StaleHours));
var count = await taskService.ResetStaleInProgressTasksAsync(threshold, ct);
return Ok(new ResetStaleResponse(count));
}
[HttpGet("tasks/{id:guid}/children")]
public async Task<ActionResult<List<DashboardTaskDto>>> GetChildren(Guid id, CancellationToken ct)
{
var children = await taskService.GetChildTasksAsync(id, ct);
return Ok(children.Select(MapToDto).ToList());
}
[HttpGet("tasks/{id:guid}/activity")]
public async Task<ActionResult<List<ActivityEvent>>> GetTaskActivity(Guid id, CancellationToken ct)
{
var events = await taskService.GetTaskActivityAsync(id, ct);
return Ok(events);
}
// ── Import ──
[HttpPost("tasks/import-from-iris-todo")]
public async Task<ActionResult<ImportResultDto>> ImportFromIrisTodo(
[FromQuery] bool delete = false, CancellationToken ct = default)
@@ -189,5 +257,6 @@ public class DashboardController(IDashboardService dashboardService, ITaskServic
}
private static DashboardTaskDto MapToDto(WorkTask t) => new(
t.Id, t.Title, t.Detail, t.Source, t.State, t.Priority, t.AssignedTo, t.CreatedAt, t.UpdatedAt);
t.Id, t.Title, t.Detail, t.Source, t.State, t.Priority, t.AssignedTo,
t.ParentTaskId, t.DueDate, t.CreatedAt, t.UpdatedAt);
}