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:
2026-07-15 00:42:53 +05:30
parent 860a7bd6cf
commit 6fa261b68f
5 changed files with 163 additions and 21 deletions
+41 -1
View File
@@ -1239,7 +1239,47 @@ route('GET', '/api/meetings', async (req, res) => {
invited: [], status: 'past', inCall: 0, recordings: list.map(recDTO),
};
});
json(res, 200, rows.concat(synth));
// #7: past CALLS from the call log. Rules the user asked for:
// • a plain 1:1 direct call is NOT listed (it's a call, not a meeting) — UNLESS it produced a
// recording/transcript, which the `synth` entries above already cover;
// • a call that ever held MORE THAN 2 people IS listed (e.g. a 1:1 that a third person joined).
// Rooms already represented by a scheduled meeting or a recording entry are skipped, so nothing doubles.
const takenRooms = new Set([...schedByRoom.keys(), ...[...unsched.values()].map((l) => l[0].room).filter(Boolean)]);
const callRows = [];
for (const c of R.callHistory.forTeam(u.team_id)) {
if (c.peak <= 2) continue; // 1:1 (or nobody) → not a meeting
if (c.room && takenRooms.has(c.room)) continue; // already listed above
let uids = []; try { uids = JSON.parse(c.uids || '[]'); } catch (_) {}
const canSee = uids.includes(u.id) || (c.group_id && R.conversations.isMember(c.group_id, u.id));
if (!canSee) continue; // only people who were actually in it
let parts = []; try { parts = JSON.parse(c.participants || '[]'); } catch (_) {}
callRows.push({
id: 'call-' + c.id, roomCode: c.room || '', title: c.title || (c.group_id ? 'Group call' : 'Meeting'),
description: '', scheduledAt: c.started_at, endedAt: c.ended_at, groupId: c.group_id || null,
groupName: c.group_id ? ((R.conversations.byId(c.group_id) || {}).name || 'Group') : null,
createdBy: null, createdByName: '', canManage: false, isHost: false,
invited: parts, participantCount: c.peak,
durationMins: Math.max(1, Math.round((c.ended_at - c.started_at) / 60000)),
status: 'past', inCall: 0, recordings: [],
});
}
// Date filter + pagination apply to PAST only (running/upcoming are small and always returned whole).
const q = new URLSearchParams(req.url.split('?')[1] || '');
const from = Number(q.get('from')) || 0;
const to = Number(q.get('to')) || 0;
const page = Math.max(1, Number(q.get('page')) || 1);
const pageSize = Math.min(50, Math.max(5, Number(q.get('pageSize')) || 10));
const all = rows.concat(synth, callRows);
const live2 = all.filter((m) => m.status !== 'past');
let past = all.filter((m) => m.status === 'past');
if (from) past = past.filter((m) => m.scheduledAt >= from);
if (to) past = past.filter((m) => m.scheduledAt <= to);
past.sort((a, b) => b.scheduledAt - a.scheduledAt); // newest first
const pastTotal = past.length;
const start = (page - 1) * pageSize;
json(res, 200, { list: live2.concat(past.slice(start, start + pageSize)), pastTotal, page, pageSize });
});
// Host uploads an in-browser meeting recording (webm). Stored + indexed so it shows under Past meetings.