← back to Butlr
live page: completed-calls history + reconcile prod drift
56de574e00b208ca74ec5f3f703d4e09b9aa0518 · 2026-05-26 09:51:17 -0700 · SteveStudio2
Adds a 'Completed calls' history section below the live grid on
live.agentabrams.com — fetches /api/done?limit=24 every 30s and renders
each done call with business name, status pill, time-ago, goal snippet,
and an inline <audio> player streaming from /api/calls/:id/recording
(same-origin session-cookie auth, token stays out of the DOM). onerror
gracefully swaps a failed player for a 'no recording archived' note.
Also reconciles 31 lines of prod drift: the Kamatera copy had auto-play-
on-connect + the shared-AudioContext unlock banner that were never
committed back to this repo. This commit pulls prod's full current state
(342 lines) plus the new history section (→408) so Mac2 = prod again.
Files touched
Diff
commit 56de574e00b208ca74ec5f3f703d4e09b9aa0518
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Tue May 26 09:51:17 2026 -0700
live page: completed-calls history + reconcile prod drift
Adds a 'Completed calls' history section below the live grid on
live.agentabrams.com — fetches /api/done?limit=24 every 30s and renders
each done call with business name, status pill, time-ago, goal snippet,
and an inline <audio> player streaming from /api/calls/:id/recording
(same-origin session-cookie auth, token stays out of the DOM). onerror
gracefully swaps a failed player for a 'no recording archived' note.
Also reconciles 31 lines of prod drift: the Kamatera copy had auto-play-
on-connect + the shared-AudioContext unlock banner that were never
committed back to this repo. This commit pulls prod's full current state
(342 lines) plus the new history section (→408) so Mac2 = prod again.
---
routes/live.js | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 99 insertions(+), 2 deletions(-)
diff --git a/routes/live.js b/routes/live.js
index 80f9b9d..38a0de5 100644
--- a/routes/live.js
+++ b/routes/live.js
@@ -112,19 +112,56 @@ function renderLive(calls) {
<span class="count"><span id="liveCount">${live.length}</span> active</span>
<span class="sub" style="margin-left:auto">auto-refresh 3s · click 🔊 Listen to hear live (no page reload)</span>
</header>
+ <div id="audio-unlock-banner" onclick="unlockAudio()" style="background:#22c55e;color:#022;text-align:center;padding:10px;font-weight:700;cursor:pointer;font-size:14px;">🔊 CLICK HERE ONCE TO ENABLE LIVE AUDIO — then calls play automatically as they connect</div>
+ <div style="display:none">
+ </div>
<main id="grid">${cardsHtml}</main>
+ <section id="completed-section" style="max-width:1100px;margin:0 auto;padding:8px 32px 80px;">
+ <h2 style="font-family:'Playfair Display',Georgia,serif;font-weight:500;font-size:20px;color:#f8fafc;border-top:1px solid var(--line);padding-top:24px;margin:24px 0 4px;">Completed calls <span id="doneCount" style="font-size:13px;color:var(--muted);font-weight:400;"></span></h2>
+ <p class="sub" style="margin:0 0 16px;">recent successful calls · play the recording inline</p>
+ <div id="doneGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px;"></div>
+ </section>
+
<script>
(function(){
// Map of callId → { ws, audioCtx, btn, stateEl }
const sessions = new Map();
+ // Shared AudioContext — browsers require a user gesture to start audio.
+ // We unlock ONE context on the first click anywhere, then reuse it for every
+ // call so auto-play-on-connect actually produces sound.
+ let sharedCtx = null;
+ let audioUnlocked = false;
+ function ensureCtx(sampleRate) {
+ if (!sharedCtx) {
+ sharedCtx = new (window.AudioContext || window.webkitAudioContext)();
+ }
+ if (sharedCtx.state === 'suspended') sharedCtx.resume();
+ return sharedCtx;
+ }
+ function unlockAudio() {
+ if (audioUnlocked) return;
+ try {
+ ensureCtx();
+ // play a 1-sample silent buffer to fully unlock on iOS/Safari
+ const b = sharedCtx.createBuffer(1, 1, 22050);
+ const s = sharedCtx.createBufferSource();
+ s.buffer = b; s.connect(sharedCtx.destination); s.start(0);
+ audioUnlocked = true;
+ const banner = document.getElementById('audio-unlock-banner');
+ if (banner) { banner.style.display = 'none'; }
+ } catch (e) {}
+ }
+ document.addEventListener('click', unlockAudio, { once: false });
+ document.addEventListener('keydown', unlockAudio, { once: false });
+
function getScheme() { return location.protocol === 'https:' ? 'wss:' : 'ws:'; }
function startListen(callId, sampleRate, btn, stateEl) {
let audioCtx, ws, nextPlayTime, frameCount = 0;
try {
- audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate });
+ audioCtx = ensureCtx(sampleRate);
nextPlayTime = audioCtx.currentTime + 0.1;
} catch (e) {
stateEl.textContent = 'no audio support';
@@ -179,7 +216,7 @@ function renderLive(calls) {
const s = sessions.get(callId);
if (!s) return;
try { s.ws && s.ws.close(); } catch(e){}
- try { s.audioCtx && s.audioCtx.close(); } catch(e){}
+ /* keep shared ctx alive for reuse */
sessions.delete(callId);
s.btn.textContent = '🔊 Listen';
s.btn.classList.remove('listening');
@@ -258,6 +295,66 @@ function renderLive(calls) {
} catch (e) { /* swallow */ }
}
setInterval(refresh, 3000);
+
+ // ── Completed-calls history ──────────────────────────────────────────
+ // Fetches the last N status=done calls and renders them with an inline
+ // <audio> player streaming from /api/calls/:id/recording (same-origin,
+ // session-cookie auth — token stays out of the DOM). Refreshes every 30s
+ // so a call that just finished slides into the history without a reload.
+ function doneEsc(s) {
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function(c){
+ return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c];
+ });
+ }
+ function doneAgo(ts) {
+ try {
+ var ms = Date.now() - new Date(ts).getTime();
+ if (ms < 60000) return Math.floor(ms/1000) + 's ago';
+ if (ms < 3600000) return Math.floor(ms/60000) + 'm ago';
+ if (ms < 86400000) return Math.floor(ms/3600000) + 'h ago';
+ return Math.floor(ms/86400000) + 'd ago';
+ } catch(e) { return ''; }
+ }
+ async function loadCompleted() {
+ try {
+ var r = await fetch('/api/done?limit=24', { credentials: 'same-origin' });
+ var j = await r.json();
+ var calls = (j.calls || []);
+ var countEl = document.getElementById('doneCount');
+ var grid = document.getElementById('doneGrid');
+ if (!grid) return;
+ if (countEl) countEl.textContent = calls.length ? '· ' + calls.length + ' shown' : '';
+ if (!calls.length) {
+ grid.innerHTML = '<div style="grid-column:1/-1;color:var(--muted);font-size:13px;padding:24px 0;">No completed calls yet.</div>';
+ return;
+ }
+ grid.innerHTML = calls.map(function(c){
+ var biz = doneEsc(c.business_name || 'Unknown');
+ var phone = doneEsc(c.business_phone || '');
+ var goal = doneEsc((c.goal || '').slice(0, 140));
+ var ago = doneAgo(c.updated_at || c.created_at);
+ var id = doneEsc(c.id);
+ 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>' : '') +
+ '<span style="margin-left:auto;">' + ago + '</span>' +
+ '</div>' +
+ (goal ? '<div class="goal">' + goal + (c.goal && c.goal.length > 140 ? '\\u2026' : '') + '</div>' : '') +
+ '<audio controls preload="none" src="/api/calls/' + id + '/recording" ' +
+ 'style="width:100%;height:34px;margin-top:6px;" ' +
+ 'onerror="this.style.display=\\'none\\';this.insertAdjacentHTML(\\'afterend\\',\\'<span style="color:#64748b;font-size:11px">no recording archived</span>\\')"></audio>' +
+ '<div style="margin-top:6px;">' +
+ '<a href="https://butlr.agentabrams.com/calls/' + id + '" target="_blank" rel="noopener" ' +
+ 'style="color:var(--muted);font-size:11px;text-decoration:none;">call detail \\u2192</a>' +
+ '</div>' +
+ '</div>';
+ }).join('');
+ } catch (e) { /* swallow */ }
+ }
+ loadCompleted();
+ setInterval(loadCompleted, 30000);
})();
</script>
</body>
← 1295f61 sync local from prod — reconcile butlr drift
·
back to Butlr
·
live page: chime on every new call (two-tone Web Audio beep, 4ef812b →