- 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>
98 lines
3.4 KiB
C#
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));
|
|
}
|
|
}
|