[object Object]

← 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 →