fix(meetings): Past shows only scheduled + instant meetings; ad-hoc calls hidden w/o recording (batch86)

Revised rule per user: a call is not a meeting. Past meetings now = real SCHEDULED meetings +
INSTANT meetings (Start-a-meeting, logged in call_history as adhoc). Ad-hoc CALLS — 1:1 direct,
group calls, and a 1:1 that a 3rd person joined — are shown ONLY if they produced a
recording/transcript. Dropped the earlier ">2 participants shows it" exception.
Also stop rendering a bare "Host: —" on logged call cards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 14:58:48 +05:30
parent f708cc2f12
commit c3dc47a94c
2 changed files with 28 additions and 15 deletions
+2 -2
View File
@@ -1058,7 +1058,7 @@
</head> </head>
<body> <body>
<script src="/icons.js?v=6"></script> <script src="/icons.js?v=6"></script>
<script>window.__BUILD='2026-07-15-batch85';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD); <script>window.__BUILD='2026-07-15-batch86';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
// Emoji are rendered with the OS's own (colour) emoji font — instant, zero network. // Emoji are rendered with the OS's own (colour) emoji font — instant, zero network.
// //
// We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from // We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from
@@ -3706,7 +3706,7 @@ async function loadScheduledMeetings(){
if(m.durationMins) meta.push(m.durationMins+' min'); if(m.durationMins) meta.push(m.durationMins+' min');
if(m.recurrenceLabel) meta.push('🔁 '+pEsc(m.recurrenceLabel)); if(m.recurrenceLabel) meta.push('🔁 '+pEsc(m.recurrenceLabel));
if(m.status==='running') meta.push(m.inCall+' in call'); if(m.status==='running') meta.push(m.inCall+' in call');
meta.push(m.isHost?'You\'re the host':('Host: '+pEsc(m.createdByName||'—'))); if(m.isHost) meta.push('You\'re the host'); else if(m.createdByName) meta.push('Host: '+pEsc(m.createdByName)); // skip "Host: —" for logged calls
if(m.invited&&m.invited.length) meta.push(m.invited.length+' invited'); if(m.invited&&m.invited.length) meta.push(m.invited.length+' invited');
// Cancel only on a FUTURE upcoming meeting (#13: not once the time has passed). // Cancel only on a FUTURE upcoming meeting (#13: not once the time has passed).
const canCancel=m.canManage && m.status==='upcoming' && m.scheduledAt>Date.now(); const canCancel=m.canManage && m.status==='upcoming' && m.scheduledAt>Date.now();
+26 -13
View File
@@ -1244,21 +1244,34 @@ route('GET', '/api/meetings', async (req, res) => {
// • a plain 1:1 direct call is NOT listed (it's a call, not a meeting) — UNLESS it produced a // • 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; // 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). // • a call that ever held MORE THAN 2 people IS listed (e.g. a 1:1 that a third person joined).
// #3/#7: calls.js writes a scheduled_meetings row for EVERY call ("Direct Call" for 1:1, "Group call" // What belongs in Past meetings (the user's rules):
// for group). Those are the 136 "Direct Call" cards. Peak participant count per room comes from the // • real SCHEDULED meetings → always;
// call log. Rule: an auto call-history row is shown in Past ONLY if it produced a recording/transcript // • INSTANT meetings (Start-a-meeting → meeting-create → logged in call_history as 'adhoc') → always;
// OR the call ever held MORE than 2 people. Plain 1:1s with neither are dropped. Real, user-scheduled // • ad-hoc CALLS — a 1:1 "Direct Call", a group call, or a 1:1 that a 3rd person joined — are NOT
// meetings (any other title) are never filtered. // meetings, so they show ONLY if they produced a recording/transcript.
const peakByRoom = {}; // calls.js writes a 'Direct Call'/'Group call' scheduled row per call (that's the 136 cards); those are
for (const c of R.callHistory.forTeam(u.team_id)) { if (c.room) peakByRoom[c.room] = Math.max(peakByRoom[c.room] || 0, c.peak || 0); } // the ad-hoc calls to gate. Instant meetings have no scheduled row and live only in call_history.
const CALL_TITLES = new Set(['Direct Call', 'Group call']); const CALL_TITLES = new Set(['Direct Call', 'Group call']);
const keptRows = rows.filter((m) => { const keptRows = rows.filter((m) => {
if (!CALL_TITLES.has(m.title)) return true; // a real scheduled meeting if (!CALL_TITLES.has(m.title)) return true; // a real, user-scheduled meeting
if (m.recordings && m.recordings.length) return true; // has a recording / transcript return !!(m.recordings && m.recordings.length); // ad-hoc call → only with a recording/transcript
const peak = peakByRoom[m.roomCode] || 0;
if (peak > 2) { m.participantCount = peak; m.invited = []; return true; } // grew past 2 people
return false; // nothing to show → hide (#3)
}); });
// Instant meetings from the call log (deduped against anything already listed via a scheduled row/recording).
const usedRooms = new Set([...schedByRoom.keys(), ...[...unsched.values()].map((l) => l[0].room).filter(Boolean)]);
const instantRows = [];
for (const c of R.callHistory.forTeam(u.team_id)) {
if (c.room && usedRooms.has(c.room)) continue;
let uids = []; try { uids = JSON.parse(c.uids || '[]'); } catch (_) {}
if (!uids.includes(u.id)) continue; // only meetings you were actually in
let parts = []; try { parts = JSON.parse(c.participants || '[]'); } catch (_) {}
instantRows.push({
id: 'call-' + c.id, roomCode: c.room || '', title: c.title || 'Meeting', description: '',
scheduledAt: c.started_at, endedAt: c.ended_at, groupId: null, groupName: null,
createdBy: null, createdByName: '', canManage: false, isHost: false, invited: [],
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). // Date filter + pagination apply to PAST only (running/upcoming are small and always returned whole).
const q = new URLSearchParams(req.url.split('?')[1] || ''); const q = new URLSearchParams(req.url.split('?')[1] || '');
@@ -1266,7 +1279,7 @@ route('GET', '/api/meetings', async (req, res) => {
const to = Number(q.get('to')) || 0; const to = Number(q.get('to')) || 0;
const page = Math.max(1, Number(q.get('page')) || 1); const page = Math.max(1, Number(q.get('page')) || 1);
const pageSize = Math.min(50, Math.max(5, Number(q.get('pageSize')) || 10)); const pageSize = Math.min(50, Math.max(5, Number(q.get('pageSize')) || 10));
const all = keptRows.concat(synth); const all = keptRows.concat(synth, instantRows);
const live2 = all.filter((m) => m.status !== 'past'); const live2 = all.filter((m) => m.status !== 'past');
let past = all.filter((m) => m.status === 'past'); let past = all.filter((m) => m.status === 'past');
if (from) past = past.filter((m) => m.scheduledAt >= from); if (from) past = past.filter((m) => m.scheduledAt >= from);