feat(meetings): call log, past pagination + date filter; last-seen exact time (batch83)
#7 The past list was hard-capped at 12 (a .slice(0,12) in the client) and there was NO record of how many people were ever in a finished call — so the "only calls with >2 people" rule was impossible to apply. Added a call_history table: signaling tracks the HIGH-WATER participant count per room and logs the call when the room tears down (scheduled meetings are skipped — they already have their own row). Past meetings now follow the rules asked for: • a plain 1:1 direct call is NOT listed — unless it produced a recording/transcript (those already surface as recording entries); • a call that ever held MORE than 2 people IS listed (e.g. a 1:1 a third person joined), showing its participant count and duration; • entries are visible only to people who were actually in the call (or the group). Server-side pagination (10/page) + a from/to date filter; nothing is double-listed. #2 Last seen now shows the exact time/date, WhatsApp-style — "last seen today at 1:36 PM", "last seen yesterday at 10:15 AM", "last seen 14/07/2026 at 9:00 AM" — instead of "10 minutes ago". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+34
-1
@@ -5,7 +5,7 @@
|
||||
const R = require('./repos');
|
||||
const A = require('./auth');
|
||||
const { currentUser, audit } = require('./session');
|
||||
const { onlineAgents, liveSessions, pendingShares, meetingRooms, roomToDmCall, roomHost, transcriptBuffers, transcriptSubs } = require('./presence');
|
||||
const { onlineAgents, liveSessions, pendingShares, meetingRooms, roomToDmCall, roomToGroupCall, roomHost, transcriptBuffers, transcriptSubs } = require('./presence');
|
||||
const W = require('./webhooks');
|
||||
const CHAT = require('./chat');
|
||||
|
||||
@@ -14,6 +14,35 @@ const CHAT = require('./chat');
|
||||
// lobbyPending: room code -> Map(peerId -> guest ws) awaiting admission.
|
||||
const roomLobby = new Map();
|
||||
const lobbyPending = new Map();
|
||||
// #7: live stats per room so a finished call can be logged with the MOST people it ever held. Without
|
||||
// this there's no way to tell a plain 1:1 from a call that grew to 3+ once everyone has left.
|
||||
const roomStats = new Map(); // room -> { teamId, startedAt, peak, names:Set, uids:Set }
|
||||
function noteRoomStat(room, ws, size) {
|
||||
let st = roomStats.get(room);
|
||||
if (!st) { st = { teamId: null, startedAt: Date.now(), peak: 0, names: new Set(), uids: new Set() }; roomStats.set(room, st); }
|
||||
if (!st.teamId && ws._meetingTeamId) st.teamId = ws._meetingTeamId;
|
||||
if (size > st.peak) st.peak = size;
|
||||
if (ws._peerName) st.names.add(ws._peerName);
|
||||
if (ws._meetingUserId) st.uids.add(ws._meetingUserId);
|
||||
}
|
||||
// Called as a room is torn down. Scheduled meetings already have their own row, so they're skipped.
|
||||
function persistCallHistory(room) {
|
||||
const st = roomStats.get(room);
|
||||
roomStats.delete(room);
|
||||
if (!st || !st.teamId || st.peak < 1) return;
|
||||
try {
|
||||
if (R.scheduledMeetings.byCode(room)) return; // the scheduled meeting row already represents this
|
||||
const gid = roomToGroupCall.get(room) || null;
|
||||
const isDm = roomToDmCall.has(room);
|
||||
R.callHistory.create({
|
||||
id: A.id(), teamId: st.teamId, room, groupId: gid,
|
||||
kind: isDm ? 'dm' : (gid ? 'group' : 'adhoc'),
|
||||
title: isDm ? 'Direct call' : (gid ? null : 'Meeting'),
|
||||
peak: st.peak, participants: [...st.names], uids: [...st.uids],
|
||||
startedAt: st.startedAt, endedAt: Date.now(),
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
// A room requires host approval for GUESTS when explicitly set (ad-hoc) or by the scheduled meeting's
|
||||
// `lobby` flag. Default: require approval (the "people with the link join directly" concern) unless the
|
||||
// organizer chose "join directly". Logged-in tenant users are never held — only guests.
|
||||
@@ -32,6 +61,7 @@ function finishMeetingJoin(ws, room, peers) {
|
||||
ws.send(JSON.stringify({ type: 'meeting-joined', room, peerId, isHost, peers: [...peers.entries()].map(([id, p]) => ({ peerId: id, name: p.name, avatar: p.avatar || null, uid: p.uid || null })) }));
|
||||
for (const [, p] of peers) { if (p.ws.readyState === 1) p.ws.send(JSON.stringify({ type: 'meeting-peer-joined', peerId, name, avatar, uid: ws._meetingUserId || null })); }
|
||||
peers.set(peerId, { ws, name, avatar, uid: ws._meetingUserId || null });
|
||||
noteRoomStat(room, ws, peers.size); // #7: track the high-water participant count for the call log
|
||||
if (ws._meetingUserId) { try { require('./calls').markDmAnswered(room, ws._meetingUserId); } catch (_) {} CHAT.broadcastPresence(ws._meetingUserId); }
|
||||
const tsubs = transcriptSubs.get(room); if (tsubs && tsubs.size > 0) ws.send(JSON.stringify({ type: 'meeting-transcribe-state', active: true }));
|
||||
if (isHost) { const pend = lobbyPending.get(room); if (pend) for (const [ppid, pws] of pend) { if (pws.readyState === 1) ws.send(JSON.stringify({ type: 'meeting-lobby-request', peerId: ppid, name: pws._peerName || 'Guest' })); } }
|
||||
@@ -121,6 +151,7 @@ function handle(ws, m, req) {
|
||||
if (!mUid && typeof m.guestId === 'string' && /^guest-[a-z0-9]+$/i.test(m.guestId)) mUid = m.guestId.slice(0, 64);
|
||||
ws._meetingUserId = mUid; // for per-user transcript ownership + SFU media mapping
|
||||
ws._meetingAvatar = (ju && ju.avatar_url) ? ju.avatar_url : null; // for participant-tile profile pics
|
||||
if (ju) ws._meetingTeamId = ju.team_id; // #7: which tenant owns this call log
|
||||
// LOBBY (#4): a GUEST (no session) waits for the host to admit them when the room requires approval.
|
||||
// Logged-in tenant members always join directly.
|
||||
if (!ju && meetingRoomRequiresApproval(room)) {
|
||||
@@ -376,6 +407,7 @@ function leaveMeeting(ws) {
|
||||
if (roomToDmCall.has(room)) {
|
||||
const others = [...peers.values()].map((p) => p.ws && p.ws._meetingUserId).filter(Boolean);
|
||||
for (const [, p] of peers) { if (p.ws.readyState === 1) { try { p.ws.send(JSON.stringify({ type: 'meeting-ended' })); } catch (_) {} p.ws._meetingRoom = null; } }
|
||||
persistCallHistory(room); // #7: log the call BEFORE endCallByRoom clears roomToDmCall/roomToGroupCall
|
||||
meetingRooms.delete(room);
|
||||
try { require('./calls').finalizeTranscript(room); } catch (_) {} // any remaining buffers (safety)
|
||||
roomHost.delete(room);
|
||||
@@ -386,6 +418,7 @@ function leaveMeeting(ws) {
|
||||
}
|
||||
for (const [, p] of peers) { if (p.ws.readyState === 1) p.ws.send(JSON.stringify({ type: 'meeting-peer-left', peerId: pid })); }
|
||||
if (peers.size === 0) {
|
||||
persistCallHistory(room); // #7: log the finished call (before the room maps are cleared)
|
||||
meetingRooms.delete(room);
|
||||
lobbyPending.delete(room); roomLobby.delete(room); // room gone → drop its lobby state
|
||||
try { require('./calls').finalizeTranscript(room); } catch (_) {} // before endCallByRoom clears the maps
|
||||
|
||||
Reference in New Issue
Block a user