Created detailed implementation plans for Sprint 2 backend work: Story 1: Audit Log Foundation (Phase 1) - Task 1: Design AuditLog database schema and create migration - Task 2: Create AuditLog entity and Repository - Task 3: Implement EF Core SaveChangesInterceptor - Task 4: Write unit tests for audit logging - Task 5: Integrate with ProjectManagement Module Story 2: Audit Log Core Features (Phase 2) - Task 1: Implement Changed Fields Detection (JSON Diff) - Task 2: Integrate User Context Tracking - Task 3: Add Multi-Tenant Isolation - Task 4: Implement Audit Query API - Task 5: Write Integration Tests Story 3: Sprint Management Module - Task 1: Create Sprint Aggregate Root and Domain Events - Task 2: Implement Sprint Repository and EF Core Configuration - Task 3: Create CQRS Commands and Queries - Task 4: Implement Burndown Chart Calculation - Task 5: Add SignalR Real-Time Notifications - Task 6: Write Integration Tests Total: 3 Stories, 16 Tasks, 24 Story Points (8+8+8) Estimated Duration: 10-12 days All tasks include: - Detailed technical implementation guidance - Code examples and file paths - Testing requirements (>= 90% coverage) - Performance benchmarks (< 5ms audit overhead) - Multi-tenant security validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
8.9 KiB
8.9 KiB
task_id, story, status, estimated_hours, created_date, assignee
| task_id | story | status | estimated_hours | created_date | assignee |
|---|---|---|---|---|---|
| sprint_2_story_3_task_5 | sprint_2_story_3 | not_started | 3 | 2025-11-05 | Backend Team |
Task 5: Add SignalR Real-Time Notifications
Story: Story 3 - Sprint Management Module Estimated: 3 hours
Description
Integrate SignalR to broadcast real-time notifications for Sprint events (Created, Updated, Started, Completed, Deleted) to connected clients.
Acceptance Criteria
- Domain event handlers created for 5 Sprint events
- IRealtimeNotificationService extended with Sprint methods
- SignalR notifications sent to project groups
- Integration tests verify notifications are sent
- Frontend can receive Sprint notifications
Implementation Details
Files to Create:
- Extend Notification Service:
colaflow-api/src/ColaFlow.Application/Common/Interfaces/IRealtimeNotificationService.cs
public interface IRealtimeNotificationService
{
// ... existing methods ...
// Sprint notifications
Task NotifySprintCreatedAsync(Guid sprintId, string sprintName, Guid projectId);
Task NotifySprintUpdatedAsync(Guid sprintId, string sprintName, Guid projectId);
Task NotifySprintStartedAsync(Guid sprintId, string sprintName, Guid projectId);
Task NotifySprintCompletedAsync(Guid sprintId, string sprintName, Guid projectId);
Task NotifySprintDeletedAsync(Guid sprintId, string sprintName, Guid projectId);
}
- Implement Service:
colaflow-api/src/ColaFlow.Infrastructure/SignalR/RealtimeNotificationService.cs
public class RealtimeNotificationService : IRealtimeNotificationService
{
private readonly IHubContext<ProjectHub> _projectHubContext;
// ... existing code ...
public async Task NotifySprintCreatedAsync(Guid sprintId, string sprintName, Guid projectId)
{
await _projectHubContext.Clients
.Group($"project-{projectId}")
.SendAsync("SprintCreated", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
}
public async Task NotifySprintUpdatedAsync(Guid sprintId, string sprintName, Guid projectId)
{
await _projectHubContext.Clients
.Group($"project-{projectId}")
.SendAsync("SprintUpdated", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
}
public async Task NotifySprintStartedAsync(Guid sprintId, string sprintName, Guid projectId)
{
await _projectHubContext.Clients
.Group($"project-{projectId}")
.SendAsync("SprintStarted", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
}
public async Task NotifySprintCompletedAsync(Guid sprintId, string sprintName, Guid projectId)
{
await _projectHubContext.Clients
.Group($"project-{projectId}")
.SendAsync("SprintCompleted", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
}
public async Task NotifySprintDeletedAsync(Guid sprintId, string sprintName, Guid projectId)
{
await _projectHubContext.Clients
.Group($"project-{projectId}")
.SendAsync("SprintDeleted", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
}
}
- Domain Event Handlers:
colaflow-api/src/ColaFlow.Application/Sprints/EventHandlers/
// SprintCreatedEventHandler.cs
public class SprintCreatedEventHandler : INotificationHandler<SprintCreatedEvent>
{
private readonly IRealtimeNotificationService _notificationService;
private readonly ILogger<SprintCreatedEventHandler> _logger;
public SprintCreatedEventHandler(
IRealtimeNotificationService notificationService,
ILogger<SprintCreatedEventHandler> logger)
{
_notificationService = notificationService;
_logger = logger;
}
public async Task Handle(SprintCreatedEvent notification, CancellationToken cancellationToken)
{
try
{
await _notificationService.NotifySprintCreatedAsync(
notification.SprintId,
notification.SprintName,
notification.ProjectId
);
_logger.LogInformation(
"Real-time notification sent for Sprint created: {SprintId}",
notification.SprintId
);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Failed to send real-time notification for Sprint created: {SprintId}",
notification.SprintId
);
}
}
}
// SprintUpdatedEventHandler.cs
public class SprintUpdatedEventHandler : INotificationHandler<SprintUpdatedEvent>
{
private readonly IRealtimeNotificationService _notificationService;
public async Task Handle(SprintUpdatedEvent notification, CancellationToken cancellationToken)
{
// Get ProjectId from repository
await _notificationService.NotifySprintUpdatedAsync(
notification.SprintId,
notification.SprintName,
projectId // Need to get from Sprint
);
}
}
// SprintStartedEventHandler.cs
public class SprintStartedEventHandler : INotificationHandler<SprintStartedEvent>
{
// Similar implementation
}
// SprintCompletedEventHandler.cs
public class SprintCompletedEventHandler : INotificationHandler<SprintCompletedEvent>
{
// Similar implementation
}
// SprintDeletedEventHandler.cs
public class SprintDeletedEventHandler : INotificationHandler<SprintDeletedEvent>
{
// Similar implementation
}
- Publish Domain Events: Update Command Handlers to publish domain events
// In CreateSprintCommandHandler
public async Task<Guid> Handle(CreateSprintCommand request, CancellationToken cancellationToken)
{
var sprint = Sprint.Create(...);
await _repository.AddAsync(sprint, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
// Publish domain events
foreach (var domainEvent in sprint.DomainEvents)
{
await _mediator.Publish(domainEvent, cancellationToken);
}
sprint.ClearDomainEvents();
return sprint.Id;
}
SignalR Event Types:
SprintCreated- New sprint createdSprintUpdated- Sprint details updatedSprintStarted- Sprint status changed to ActiveSprintCompleted- Sprint status changed to CompletedSprintDeleted- Sprint deleted
Frontend Integration:
// Frontend receives notifications
connection.on('SprintCreated', (data) => {
console.log('Sprint created:', data);
// Update UI, refresh sprint list
});
connection.on('SprintStarted', (data) => {
console.log('Sprint started:', data);
// Update sprint status in UI
});
Technical Notes
- Use MediatR to publish domain events after SaveChanges
- Event handlers are fire-and-forget (don't block command execution)
- Log errors if SignalR notification fails
- Use project groups for targeted notifications (
project-{projectId}) - Include timestamp in all notifications
Testing
Integration Tests: colaflow-api/tests/ColaFlow.Application.IntegrationTests/Sprints/SprintSignalRTests.cs
public class SprintSignalRTests : IntegrationTestBase
{
[Fact]
public async Task CreateSprint_ShouldSendSprintCreatedNotification()
{
// Arrange
var notificationService = GetMock<IRealtimeNotificationService>();
var command = new CreateSprintCommand
{
ProjectId = TestProjectId,
Name = "Sprint 1",
StartDate = DateTime.UtcNow,
EndDate = DateTime.UtcNow.AddDays(14)
};
// Act
var sprintId = await Mediator.Send(command);
// Assert
notificationService.Verify(
s => s.NotifySprintCreatedAsync(sprintId, "Sprint 1", TestProjectId),
Times.Once
);
}
[Fact]
public async Task StartSprint_ShouldSendSprintStartedNotification()
{
// Arrange
var sprintId = await CreateTestSprint();
var notificationService = GetMock<IRealtimeNotificationService>();
// Act
await Mediator.Send(new StartSprintCommand(sprintId));
// Assert
notificationService.Verify(
s => s.NotifySprintStartedAsync(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<Guid>()),
Times.Once
);
}
}
Created: 2025-11-05 by Backend Agent