feat: WebSocket presence tracking with page_focus and presence_update

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-23 09:54:36 -04:00
parent a4d74baf51
commit 242150d54c
2 changed files with 140 additions and 1 deletions

View File

@@ -32,6 +32,8 @@ class WebSocketManager {
const clientInfo = {
authenticated: false,
userId: null,
adminName: null,
currentPage: null,
subscribedSessions: new Set(),
lastPing: Date.now()
};
@@ -96,6 +98,15 @@ class WebSocketManager {
clientInfo.lastPing = Date.now();
this.send(ws, { type: 'pong' });
break;
case 'page_focus':
if (!clientInfo.authenticated) {
this.sendError(ws, 'Not authenticated');
return;
}
clientInfo.currentPage = message.page || null;
this.broadcastPresence();
break;
default:
this.sendError(ws, `Unknown message type: ${message.type}`);
@@ -117,7 +128,13 @@ class WebSocketManager {
if (clientInfo) {
clientInfo.authenticated = true;
clientInfo.userId = decoded.role; // 'admin' for now
clientInfo.userId = decoded.role;
clientInfo.adminName = decoded.name || null;
if (!decoded.name) {
this.sendError(ws, 'Token missing admin identity, please re-login', 'auth_error');
return;
}
this.send(ws, {
type: 'auth_success',
@@ -283,9 +300,31 @@ class WebSocketManager {
this.clients.delete(ws);
console.log('[WebSocket] Client disconnected and cleaned up');
this.broadcastPresence();
}
}
broadcastPresence() {
const admins = [];
this.clients.forEach((info) => {
if (info.authenticated && info.adminName && info.currentPage) {
admins.push({ name: info.adminName, page: info.currentPage });
}
});
const message = {
type: 'presence_update',
timestamp: new Date().toISOString(),
admins
};
this.clients.forEach((info, ws) => {
if (info.authenticated && ws.readyState === ws.OPEN) {
this.send(ws, message);
}
});
}
/**
* Start heartbeat to detect dead connections
*/