Skip to content

Commit 004fc68

Browse files
authored
Merge pull request #667 from Chris0Jeky/feature/anl-01-board-metrics
Add board metrics dashboard (ANL-01)
2 parents 6cd6072 + 6cc8960 commit 004fc68

File tree

16 files changed

+2153
-0
lines changed

16 files changed

+2153
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Taskdeck.Api.Contracts;
4+
using Taskdeck.Api.Extensions;
5+
using Taskdeck.Application.DTOs;
6+
using Taskdeck.Application.Interfaces;
7+
using Taskdeck.Application.Services;
8+
9+
namespace Taskdeck.Api.Controllers;
10+
11+
/// <summary>
12+
/// Board metrics endpoints: throughput, cycle time, WIP, blocked trends.
13+
/// </summary>
14+
[ApiController]
15+
[Authorize]
16+
[Route("api/[controller]")]
17+
[Produces("application/json")]
18+
public class MetricsController : AuthenticatedControllerBase
19+
{
20+
private readonly IBoardMetricsService _metricsService;
21+
22+
public MetricsController(IBoardMetricsService metricsService, IUserContext userContext)
23+
: base(userContext)
24+
{
25+
_metricsService = metricsService;
26+
}
27+
28+
/// <summary>
29+
/// Get board metrics (throughput, cycle time, WIP, blocked) for a date range.
30+
/// </summary>
31+
/// <param name="boardId">The board to compute metrics for.</param>
32+
/// <param name="from">Start of date range (ISO 8601).</param>
33+
/// <param name="to">End of date range (ISO 8601).</param>
34+
/// <param name="labelId">Optional label filter.</param>
35+
/// <returns>Aggregated board metrics.</returns>
36+
/// <response code="200">Metrics computed successfully.</response>
37+
/// <response code="400">Invalid query parameters.</response>
38+
/// <response code="401">Authentication required.</response>
39+
/// <response code="403">No read access to the board.</response>
40+
/// <response code="404">Board not found.</response>
41+
[HttpGet("boards/{boardId}")]
42+
[ProducesResponseType(typeof(BoardMetricsResponse), StatusCodes.Status200OK)]
43+
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status400BadRequest)]
44+
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status401Unauthorized)]
45+
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status403Forbidden)]
46+
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status404NotFound)]
47+
public async Task<IActionResult> GetBoardMetrics(
48+
Guid boardId,
49+
[FromQuery] DateTimeOffset? from,
50+
[FromQuery] DateTimeOffset? to,
51+
[FromQuery] Guid? labelId)
52+
{
53+
if (!TryGetCurrentUserId(out var userId, out var errorResult))
54+
return errorResult!;
55+
56+
// Default to last 30 days if not specified
57+
var toDate = to ?? DateTimeOffset.UtcNow;
58+
var fromDate = from ?? toDate.AddDays(-30);
59+
60+
var query = new BoardMetricsQuery(boardId, fromDate, toDate, labelId);
61+
var result = await _metricsService.GetBoardMetricsAsync(query, userId);
62+
return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult();
63+
}
64+
}

backend/src/Taskdeck.Api/Extensions/ApplicationServiceRegistration.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Taskdeck.Api.Realtime;
2+
using Taskdeck.Application.Interfaces;
23
using Taskdeck.Application.Services;
34
using Taskdeck.Application.Services.Tools;
45
using Taskdeck.Domain.Agents;
@@ -49,6 +50,10 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
4950
services.AddScoped<IStarterPackApplyService, StarterPackApplyService>();
5051
services.AddScoped<IStarterPackCatalogService, StarterPackCatalogService>();
5152
services.AddScoped<IOutboundWebhookService, OutboundWebhookService>();
53+
services.AddScoped<IBoardMetricsService>(sp =>
54+
new BoardMetricsService(
55+
sp.GetRequiredService<IUnitOfWork>(),
56+
sp.GetRequiredService<IAuthorizationService>()));
5257
services.AddScoped<AgentProfileService>();
5358
services.AddScoped<AgentRunService>();
5459
services.AddScoped<SignalRBoardRealtimeNotifier>();
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
namespace Taskdeck.Application.DTOs;
2+
3+
/// <summary>
4+
/// Query parameters for board metrics.
5+
/// </summary>
6+
public sealed record BoardMetricsQuery(
7+
Guid BoardId,
8+
DateTimeOffset From,
9+
DateTimeOffset To,
10+
Guid? LabelId = null);
11+
12+
/// <summary>
13+
/// Throughput data point: number of cards completed in a time bucket.
14+
/// </summary>
15+
public sealed record ThroughputDataPoint(
16+
DateTimeOffset Date,
17+
int CompletedCount);
18+
19+
/// <summary>
20+
/// Cycle time entry: how long a card took from creation to reaching the
21+
/// final (rightmost) column.
22+
/// </summary>
23+
public sealed record CycleTimeEntry(
24+
Guid CardId,
25+
string CardTitle,
26+
double CycleTimeDays);
27+
28+
/// <summary>
29+
/// Snapshot of work-in-progress across columns.
30+
/// </summary>
31+
public sealed record WipSnapshot(
32+
Guid ColumnId,
33+
string ColumnName,
34+
int CardCount,
35+
int? WipLimit);
36+
37+
/// <summary>
38+
/// Blocked card summary.
39+
/// </summary>
40+
public sealed record BlockedCardSummary(
41+
Guid CardId,
42+
string CardTitle,
43+
string? BlockReason,
44+
double BlockedDurationDays);
45+
46+
/// <summary>
47+
/// Aggregate response containing all board metrics.
48+
/// </summary>
49+
public sealed record BoardMetricsResponse(
50+
Guid BoardId,
51+
DateTimeOffset From,
52+
DateTimeOffset To,
53+
IReadOnlyList<ThroughputDataPoint> Throughput,
54+
double AverageCycleTimeDays,
55+
IReadOnlyList<CycleTimeEntry> CycleTimeEntries,
56+
IReadOnlyList<WipSnapshot> WipSnapshots,
57+
int TotalWip,
58+
int BlockedCount,
59+
IReadOnlyList<BlockedCardSummary> BlockedCards);

0 commit comments

Comments
 (0)