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

@@ -1281,6 +1281,21 @@ body::before {
flex-shrink: 0;
}
@keyframes activity-slide-in {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.activity-item--new {
animation: activity-slide-in 0.3s ease-out;
}
/* ============================================================
CHART CONTAINER

View File

@@ -272,4 +272,60 @@
}
});
// ========================================
// SignalR — Real-time Activity Feed
// ========================================
function initActivityHub() {
var feed = document.getElementById('activity-feed');
if (!feed) return; // Not on the Analytics page
var connection = new signalR.HubConnectionBuilder()
.withUrl('/hubs/activity')
.withAutomaticReconnect()
.build();
connection.on('NewActivity', function(data) {
// Remove the empty-state placeholder if present
var empty = document.getElementById('activity-empty');
if (empty) empty.remove();
var item = document.createElement('div');
item.className = 'activity-item activity-item--new';
var displayText = data.url || data.windowTitle || '';
// Escape HTML to prevent XSS
var div = document.createElement('div');
div.textContent = data.appName || '';
var safeApp = div.innerHTML;
div.textContent = displayText;
var safeTitle = div.innerHTML;
item.innerHTML =
'<span class="activity-dot" style="background: var(--color-accent)"></span>' +
'<div class="activity-line"></div>' +
'<div class="activity-info">' +
'<span class="activity-app">' + safeApp + '</span>' +
'<span class="activity-title">' + safeTitle + '</span>' +
'<span class="activity-time">just now</span>' +
'</div>';
feed.insertBefore(item, feed.firstChild);
// Cap visible items at 100 to prevent memory bloat
var items = feed.querySelectorAll('.activity-item');
if (items.length > 100) {
items[items.length - 1].remove();
}
});
connection.start().catch(function(err) {
console.error('SignalR connection error:', err);
});
}
document.addEventListener('DOMContentLoaded', function() {
initActivityHub();
});
})();

File diff suppressed because one or more lines are too long