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>
This commit is contained in:
2026-03-01 23:30:01 -05:00
parent 5a273ba667
commit d784f9fea8
8 changed files with 125 additions and 29 deletions

View File

@@ -87,35 +87,32 @@
<section>
<h2 class="section-title">Recent Activity</h2>
<div class="surface">
@if (Model.ActivityItems.Count == 0)
{
<div class="search-empty">No activity recorded in this time range.</div>
}
else
{
<div id="activity-feed" class="activity-feed">
@foreach (var item in Model.ActivityItems)
{
<div class="activity-item">
<span class="activity-dot" style="background: var(--color-accent)"></span>
<div class="activity-line"></div>
<div class="activity-info">
<span class="activity-app">@item.AppName</span>
<span class="activity-title">@(string.IsNullOrEmpty(item.Url) ? item.WindowTitle : item.Url)</span>
<span class="activity-time">@AnalyticsModel.FormatRelativeTime(item.Timestamp)</span>
</div>
</div>
}
</div>
@if (Model.TotalActivityCount > Model.ActivityItems.Count)
<div id="activity-feed" class="activity-feed">
@if (Model.ActivityItems.Count == 0)
{
<button class="btn btn--ghost btn--full"
hx-get="/analytics?handler=ActivityFeed&minutes=@Model.Minutes&taskId=@Model.TaskId&offset=@Model.ActivityItems.Count"
hx-target="#activity-feed"
hx-swap="beforeend">
Load more (@(Model.TotalActivityCount - Model.ActivityItems.Count) remaining)
</button>
<div class="search-empty" id="activity-empty">No activity recorded in this time range.</div>
}
@foreach (var item in Model.ActivityItems)
{
<div class="activity-item">
<span class="activity-dot" style="background: var(--color-accent)"></span>
<div class="activity-line"></div>
<div class="activity-info">
<span class="activity-app">@item.AppName</span>
<span class="activity-title">@(string.IsNullOrEmpty(item.Url) ? item.WindowTitle : item.Url)</span>
<span class="activity-time">@AnalyticsModel.FormatRelativeTime(item.Timestamp)</span>
</div>
</div>
}
</div>
@if (Model.TotalActivityCount > Model.ActivityItems.Count)
{
<button class="btn btn--ghost btn--full"
hx-get="/analytics?handler=ActivityFeed&minutes=@Model.Minutes&taskId=@Model.TaskId&offset=@Model.ActivityItems.Count"
hx-target="#activity-feed"
hx-swap="beforeend">
Load more (@(Model.TotalActivityCount - Model.ActivityItems.Count) remaining)
</button>
}
</div>
</section>

View File

@@ -78,6 +78,7 @@
<script src="~/lib/htmx.min.js"></script>
<script src="~/lib/Sortable.min.js"></script>
<script src="~/lib/chart.umd.min.js"></script>
<script src="~/lib/signalr.min.js"></script>
<script src="~/js/app.js"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>