Files
ColaFlow/colaflow-api/src/ColaFlow.API/Services/RealtimeNotificationService.cs
Yaojia Wang 96fed691ab feat(backend): Add SignalR real-time notifications for Sprint events - Sprint 2 Story 3 Task 5
Implemented comprehensive SignalR notifications for Sprint lifecycle events.

Features:
- Extended IRealtimeNotificationService with 5 Sprint notification methods
- Implemented Sprint notification service methods in RealtimeNotificationService
- Created SprintEventHandlers to handle all 5 Sprint domain events
- Updated UpdateSprintCommandHandler to publish SprintUpdatedEvent
- SignalR events broadcast to both project and tenant groups

Sprint Events Implemented:
1. SprintCreated - New sprint created
2. SprintUpdated - Sprint details modified
3. SprintStarted - Sprint transitioned to Active status
4. SprintCompleted - Sprint transitioned to Completed status
5. SprintDeleted - Sprint removed

Technical Details:
- Event handlers catch and log errors (fire-and-forget pattern)
- Notifications include SprintId, SprintName, ProjectId, and Timestamp
- Multi-tenant isolation via tenant groups
- Project-level targeting via project groups

Frontend Integration:
- Frontend can listen to 'SprintCreated', 'SprintUpdated', 'SprintStarted', 'SprintCompleted', 'SprintDeleted' events
- Real-time UI updates for sprint changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 00:35:33 +01:00

341 lines
12 KiB
C#

using Microsoft.AspNetCore.SignalR;
using ColaFlow.API.Hubs;
namespace ColaFlow.API.Services;
public class RealtimeNotificationService : IRealtimeNotificationService
{
private readonly IHubContext<ProjectHub> _projectHubContext;
private readonly IHubContext<NotificationHub> _notificationHubContext;
private readonly ILogger<RealtimeNotificationService> _logger;
public RealtimeNotificationService(
IHubContext<ProjectHub> projectHubContext,
IHubContext<NotificationHub> notificationHubContext,
ILogger<RealtimeNotificationService> logger)
{
_projectHubContext = projectHubContext;
_notificationHubContext = notificationHubContext;
_logger = logger;
}
public async Task NotifyProjectCreated(Guid tenantId, Guid projectId, object project)
{
var tenantGroupName = $"tenant-{tenantId}";
_logger.LogInformation("Notifying tenant {TenantId} of new project {ProjectId}", tenantId, projectId);
await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("ProjectCreated", project);
}
public async Task NotifyProjectUpdated(Guid tenantId, Guid projectId, object project)
{
var projectGroupName = $"project-{projectId}";
var tenantGroupName = $"tenant-{tenantId}";
_logger.LogInformation("Notifying project {ProjectId} updated", projectId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("ProjectUpdated", project);
await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("ProjectUpdated", project);
}
public async Task NotifyProjectArchived(Guid tenantId, Guid projectId)
{
var projectGroupName = $"project-{projectId}";
var tenantGroupName = $"tenant-{tenantId}";
_logger.LogInformation("Notifying project {ProjectId} archived", projectId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("ProjectArchived", new { ProjectId = projectId });
await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("ProjectArchived", new { ProjectId = projectId });
}
public async Task NotifyProjectUpdate(Guid tenantId, Guid projectId, object data)
{
var groupName = $"project-{projectId}";
_logger.LogInformation("Sending project update to group {GroupName}", groupName);
await _projectHubContext.Clients.Group(groupName).SendAsync("ProjectUpdated", data);
}
// Epic notifications
public async Task NotifyEpicCreated(Guid tenantId, Guid projectId, Guid epicId, object epic)
{
var projectGroupName = $"project-{projectId}";
var tenantGroupName = $"tenant-{tenantId}";
_logger.LogInformation("Notifying epic {EpicId} created in project {ProjectId}", epicId, projectId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("EpicCreated", epic);
await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("EpicCreated", epic);
}
public async Task NotifyEpicUpdated(Guid tenantId, Guid projectId, Guid epicId, object epic)
{
var projectGroupName = $"project-{projectId}";
_logger.LogInformation("Notifying epic {EpicId} updated", epicId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("EpicUpdated", epic);
}
public async Task NotifyEpicDeleted(Guid tenantId, Guid projectId, Guid epicId)
{
var projectGroupName = $"project-{projectId}";
_logger.LogInformation("Notifying epic {EpicId} deleted", epicId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("EpicDeleted", new { EpicId = epicId });
}
// Story notifications
public async Task NotifyStoryCreated(Guid tenantId, Guid projectId, Guid epicId, Guid storyId, object story)
{
var projectGroupName = $"project-{projectId}";
var tenantGroupName = $"tenant-{tenantId}";
_logger.LogInformation("Notifying story {StoryId} created in epic {EpicId}", storyId, epicId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("StoryCreated", story);
await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("StoryCreated", story);
}
public async Task NotifyStoryUpdated(Guid tenantId, Guid projectId, Guid epicId, Guid storyId, object story)
{
var projectGroupName = $"project-{projectId}";
_logger.LogInformation("Notifying story {StoryId} updated", storyId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("StoryUpdated", story);
}
public async Task NotifyStoryDeleted(Guid tenantId, Guid projectId, Guid epicId, Guid storyId)
{
var projectGroupName = $"project-{projectId}";
_logger.LogInformation("Notifying story {StoryId} deleted", storyId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("StoryDeleted", new { StoryId = storyId });
}
// Task notifications
public async Task NotifyTaskCreated(Guid tenantId, Guid projectId, Guid storyId, Guid taskId, object task)
{
var projectGroupName = $"project-{projectId}";
var tenantGroupName = $"tenant-{tenantId}";
_logger.LogInformation("Notifying task {TaskId} created in story {StoryId}", taskId, storyId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("TaskCreated", task);
await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("TaskCreated", task);
}
public async Task NotifyTaskUpdated(Guid tenantId, Guid projectId, Guid storyId, Guid taskId, object task)
{
var projectGroupName = $"project-{projectId}";
_logger.LogInformation("Notifying task {TaskId} updated", taskId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("TaskUpdated", task);
}
public async Task NotifyTaskDeleted(Guid tenantId, Guid projectId, Guid storyId, Guid taskId)
{
var projectGroupName = $"project-{projectId}";
_logger.LogInformation("Notifying task {TaskId} deleted", taskId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("TaskDeleted", new { TaskId = taskId });
}
public async Task NotifyTaskAssigned(Guid tenantId, Guid projectId, Guid taskId, Guid assigneeId)
{
var projectGroupName = $"project-{projectId}";
_logger.LogInformation("Notifying task {TaskId} assigned to {AssigneeId}", taskId, assigneeId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("TaskAssigned", new
{
TaskId = taskId,
AssigneeId = assigneeId,
AssignedAt = DateTime.UtcNow
});
}
public async Task NotifyIssueCreated(Guid tenantId, Guid projectId, object issue)
{
var groupName = $"project-{projectId}";
await _projectHubContext.Clients.Group(groupName).SendAsync("IssueCreated", issue);
}
public async Task NotifyIssueUpdated(Guid tenantId, Guid projectId, object issue)
{
var groupName = $"project-{projectId}";
await _projectHubContext.Clients.Group(groupName).SendAsync("IssueUpdated", issue);
}
public async Task NotifyIssueDeleted(Guid tenantId, Guid projectId, Guid issueId)
{
var groupName = $"project-{projectId}";
await _projectHubContext.Clients.Group(groupName).SendAsync("IssueDeleted", new { IssueId = issueId });
}
public async Task NotifyIssueStatusChanged(
Guid tenantId,
Guid projectId,
Guid issueId,
string oldStatus,
string newStatus)
{
var groupName = $"project-{projectId}";
await _projectHubContext.Clients.Group(groupName).SendAsync("IssueStatusChanged", new
{
IssueId = issueId,
OldStatus = oldStatus,
NewStatus = newStatus,
ChangedAt = DateTime.UtcNow
});
}
// Sprint notifications
public async Task NotifySprintCreated(Guid tenantId, Guid projectId, Guid sprintId, string sprintName)
{
var projectGroupName = $"project-{projectId}";
var tenantGroupName = $"tenant-{tenantId}";
_logger.LogInformation("Notifying sprint {SprintId} created in project {ProjectId}", sprintId, projectId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintCreated", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintCreated", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
}
public async Task NotifySprintUpdated(Guid tenantId, Guid projectId, Guid sprintId, string sprintName)
{
var projectGroupName = $"project-{projectId}";
_logger.LogInformation("Notifying sprint {SprintId} updated", sprintId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintUpdated", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
}
public async Task NotifySprintStarted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName)
{
var projectGroupName = $"project-{projectId}";
var tenantGroupName = $"tenant-{tenantId}";
_logger.LogInformation("Notifying sprint {SprintId} started", sprintId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintStarted", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintStarted", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
}
public async Task NotifySprintCompleted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName)
{
var projectGroupName = $"project-{projectId}";
var tenantGroupName = $"tenant-{tenantId}";
_logger.LogInformation("Notifying sprint {SprintId} completed", sprintId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintCompleted", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintCompleted", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
}
public async Task NotifySprintDeleted(Guid tenantId, Guid projectId, Guid sprintId, string sprintName)
{
var projectGroupName = $"project-{projectId}";
var tenantGroupName = $"tenant-{tenantId}";
_logger.LogInformation("Notifying sprint {SprintId} deleted", sprintId);
await _projectHubContext.Clients.Group(projectGroupName).SendAsync("SprintDeleted", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
await _projectHubContext.Clients.Group(tenantGroupName).SendAsync("SprintDeleted", new
{
SprintId = sprintId,
SprintName = sprintName,
ProjectId = projectId,
Timestamp = DateTime.UtcNow
});
}
public async Task NotifyUser(Guid userId, string message, string type = "info")
{
var userConnectionId = $"user-{userId}";
await _notificationHubContext.Clients.User(userId.ToString()).SendAsync("Notification", new
{
Message = message,
Type = type,
Timestamp = DateTime.UtcNow
});
}
public async Task NotifyUsersInTenant(Guid tenantId, string message, string type = "info")
{
var groupName = $"tenant-{tenantId}";
await _notificationHubContext.Clients.Group(groupName).SendAsync("Notification", new
{
Message = message,
Type = type,
Timestamp = DateTime.UtcNow
});
}
}