Files
TaskTracker/TaskTracker.Api/Controllers/ContextController.cs
AJ Isaacs d784f9fea8 feat(web): add real-time activity feed via SignalR
- Add ActivityHub and wire up SignalR in Program.cs
- Broadcast new context events from ContextController
- Connect SignalR client on Analytics page for live feed updates
- Restructure activity feed HTML to support live prepending
- Add slide-in animation for new activity items
- Update CORS to allow credentials for SignalR

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 23:30:01 -05:00

98 lines
3.4 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using TaskTracker.Api.Hubs;
using TaskTracker.Core.DTOs;
using TaskTracker.Core.Entities;
using TaskTracker.Core.Interfaces;
namespace TaskTracker.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ContextController(
IContextEventRepository contextRepo,
IAppMappingRepository mappingRepo,
ITaskRepository taskRepo,
IHubContext<ActivityHub> hubContext,
ILogger<ContextController> logger) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Ingest([FromBody] ContextEventRequest request)
{
// Try to auto-link to the active task
var activeTask = await taskRepo.GetActiveTaskAsync();
var contextEvent = new ContextEvent
{
Source = request.Source,
AppName = request.AppName,
WindowTitle = request.WindowTitle,
Url = request.Url,
WorkTaskId = activeTask?.Id
};
// Try to find a matching app mapping for category enrichment
var mapping = await mappingRepo.FindMatchAsync(request.AppName, request.WindowTitle, request.Url);
if (mapping is not null)
{
logger.LogDebug("Matched context event to category {Category} via {MatchType}",
mapping.Category, mapping.MatchType);
}
var created = await contextRepo.CreateAsync(contextEvent);
await hubContext.Clients.All.SendAsync("NewActivity", new
{
id = created.Id,
appName = created.AppName,
windowTitle = created.WindowTitle,
url = created.Url,
timestamp = created.Timestamp,
workTaskId = created.WorkTaskId
});
return Ok(ApiResponse<ContextEvent>.Ok(created));
}
[HttpGet("recent")]
public async Task<IActionResult> GetRecent([FromQuery] int minutes = 30)
{
var events = await contextRepo.GetRecentAsync(minutes);
return Ok(ApiResponse<List<ContextEvent>>.Ok(events));
}
[HttpGet("summary")]
public async Task<IActionResult> GetSummary()
{
// Get today's events (last 8 hours)
var events = await contextRepo.GetRecentAsync(480);
var mappings = await mappingRepo.GetAllAsync();
var summary = events
.GroupBy(e => e.AppName)
.Select(g =>
{
var match = mappings.FirstOrDefault(m => m.MatchType switch
{
"ProcessName" => g.Key.Contains(m.Pattern, StringComparison.OrdinalIgnoreCase),
"TitleContains" => g.Any(e => e.WindowTitle.Contains(m.Pattern, StringComparison.OrdinalIgnoreCase)),
"UrlContains" => g.Any(e => e.Url?.Contains(m.Pattern, StringComparison.OrdinalIgnoreCase) == true),
_ => false
});
return new ContextSummaryItem
{
AppName = match?.FriendlyName ?? g.Key,
Category = match?.Category ?? "Unknown",
EventCount = g.Count(),
FirstSeen = g.Min(e => e.Timestamp),
LastSeen = g.Max(e => e.Timestamp)
};
})
.OrderByDescending(s => s.EventCount)
.ToList();
return Ok(ApiResponse<List<ContextSummaryItem>>.Ok(summary));
}
}