← back to Butlr
live page: completed-calls filter to recordings over 5s + show duration
b82d7e089f74627125f48ec9339cf1fdad62fefa · 2026-05-26 10:24:29 -0700 · SteveStudio2
Files touched
Diff
commit b82d7e089f74627125f48ec9339cf1fdad62fefa
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Tue May 26 10:24:29 2026 -0700
live page: completed-calls filter to recordings over 5s + show duration
---
routes/live.js | 37 ++++++++++++++++++++++++++++++++++---
1 file changed, 34 insertions(+), 3 deletions(-)
diff --git a/routes/live.js b/routes/live.js
index 916d241..2cfdfc3 100644
--- a/routes/live.js
+++ b/routes/live.js
@@ -3,10 +3,32 @@
// AJAX-refreshes the card list every 3s; audio plays via Web Audio API + WS.
const express = require('express');
+const fs = require('fs');
+const path = require('path');
+const { execFileSync } = require('child_process');
const data = require('../lib/data');
const router = express.Router();
+const REC_DIR = path.join(__dirname, '..', 'data', 'recordings');
+const MIN_DONE_SEC = 5; // hide sub-5s completed calls (no-answer / instant hangups)
+
+// Recording duration in seconds, cached on the call row after the first probe
+// so we don't shell out to ffprobe on every /api/done refresh.
+function recordingDurationSec(call) {
+ if (typeof call.recording_duration_sec === 'number') return call.recording_duration_sec;
+ const f = path.join(REC_DIR, call.id + '.mp3');
+ if (!fs.existsSync(f)) return 0;
+ try {
+ const out = execFileSync('ffprobe',
+ ['-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', f],
+ { timeout: 4000 }).toString().trim();
+ const d = Math.round((parseFloat(out) || 0) * 10) / 10;
+ data.patchRow(call.id, { recording_duration_sec: d });
+ return d;
+ } catch { return 0; }
+}
+
const LIVE_STATUSES = new Set(['queued', 'dialing', 'on_hold', 'connected', 'ringing']);
const LIVE_HOST = 'live.agentabrams.com';
@@ -418,11 +440,13 @@ function renderLive(calls) {
var goal = doneEsc((c.goal || '').slice(0, 140));
var ago = doneAgo(c.updated_at || c.created_at);
var id = doneEsc(c.id);
+ var dur = c.recording_duration_sec ? Math.round(c.recording_duration_sec) + 's' : '';
return '<div class="card status-done" style="border-left:4px solid var(--muted);">' +
'<div class="biz">' + biz + '</div>' +
'<div class="meta">' +
'<span class="pill" style="background:#475569;color:#e2e8f0;">DONE</span>' +
(phone ? '<span>' + phone + '</span>' : '') +
+ (dur ? '<span style="color:var(--green);">' + dur + '</span>' : '') +
'<span style="margin-left:auto;">' + ago + '</span>' +
'</div>' +
(goal ? '<div class="goal">' + goal + (c.goal && c.goal.length > 140 ? '\\u2026' : '') + '</div>' : '') +
@@ -459,9 +483,16 @@ router.get('/api/done', async (req, res) => {
const limit = Math.min(parseInt(req.query.limit, 10) || 30, 100);
const all = data.listCallsUnscoped();
const done = all.filter((c) => c.status === 'done')
- .sort((a, b) => new Date(b.updated_at || b.created_at) - new Date(a.updated_at || a.created_at))
- .slice(0, limit);
- res.json({ ok: true, calls: done });
+ .sort((a, b) => new Date(b.updated_at || b.created_at) - new Date(a.updated_at || a.created_at));
+ // Only surface real conversations — recording over MIN_DONE_SEC. Hides
+ // no-answers, instant hangups, and AMD-killed voicemails.
+ const calls = [];
+ for (const c of done) {
+ const dur = recordingDurationSec(c);
+ if (dur > MIN_DONE_SEC) { calls.push({ ...c, recording_duration_sec: dur }); }
+ if (calls.length >= limit) break;
+ }
+ res.json({ ok: true, calls });
} catch (e) {
res.status(500).json({ ok: false, error: e.message });
}
← eeda3f9 live page: fix completed-calls flash-disappear (scope live-g
·
back to Butlr
·
butlr: short punchy call intro (Butler / real local client) 1f52b13 →