← back to Butlr
scripts/ventura-hours-sweep.js
87 lines
// Sequential Ventura-Blvd "what are your hours" sweep.
// Places up to 40 calls ONE AT A TIME — each call must reach a terminal
// status (done/failed/canceled) before the next is placed. Live audio
// broadcasts on live.agentabrams.com as each call runs.
//
// Run on the Kamatera butlr host:
// cd /root/public-projects/butlr && nohup node scripts/ventura-hours-sweep.js > /tmp/ventura_sweep.log 2>&1 &
//
// Stop it any time: pkill -f ventura-hours-sweep
require('dotenv').config({ path: '/root/public-projects/butlr/.env' });
const fs = require('fs');
const data = require('../lib/data');
const twilio = require('../lib/twilio');
const USER_ID = 'rcA9SnnO';
const MAX_CALLS = 40;
const PER_CALL_TIMEOUT_MS = 130000; // give a call up to ~2min to finish
const GAP_MS = 4000; // breath between calls
const TERMINAL = new Set(['done', 'failed', 'canceled', 'cancelled']);
const GOAL = 'Ask: what are your hours today? Wait for their answer, then thank them warmly and end the call. Ask nothing else; keep it under two sentences.';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function log(...a) { console.log(new Date().toISOString(), ...a); }
function shortLocality(addr) {
// OSM display_name: "Name, number, street, neighborhood, city, county, state, zip, country"
const parts = String(addr || '').split(',').map((s) => s.trim()).filter(Boolean);
// drop the leading name part, keep number+street + city-ish
const tail = parts.slice(1, 4).join(', ');
return tail.slice(0, 60);
}
async function waitTerminal(id) {
const start = Date.now();
while (Date.now() - start < PER_CALL_TIMEOUT_MS) {
const c = data.getCallUnscoped(id);
if (c && TERMINAL.has(c.status)) return c.status;
await sleep(3000);
}
return 'timeout';
}
(async () => {
let list;
try { list = JSON.parse(fs.readFileSync('/tmp/vall.json', 'utf8')); }
catch (e) { log('FATAL: cannot read /tmp/vall.json —', e.message); process.exit(1); }
list = list.slice(0, MAX_CALLS);
log(`=== Ventura hours sweep: ${list.length} businesses, sequential ===`);
let placed = 0, skipped = 0;
for (let i = 0; i < list.length; i++) {
const b = list[i];
const loc = shortLocality(b.addr);
const row = {
business_name: (b.name + (loc ? ' — ' + loc : '')).slice(0, 120),
business_phone: b.e164,
goal: GOAL,
callback_phone: '+16067130489',
callback_name: 'Steve Adams',
category: 'other',
category_label: 'Ventura hours sweep',
max_hold_minutes: 2,
max_spend_cents: 50,
consent_recording: 'on',
consent_terms: 'on',
notes: 'ventura 40 hours sweep ' + (i + 1) + '/' + list.length,
allow_repeat: true,
};
const r = data.addCall(row, USER_ID);
if (!r.ok) { skipped++; log(`SKIP ${i + 1}/${list.length} ${b.e164} ${b.name} — ${(r.errors || []).join('; ')}`); continue; }
try {
await twilio.dialCall(r.call);
placed++;
log(`PLACED ${i + 1}/${list.length} id=${r.call.id} ${b.e164} | ${b.name}`);
} catch (e) {
log(`DIAL-ERR ${i + 1}/${list.length} id=${r.call.id} — ${e.message}`);
continue;
}
const status = await waitTerminal(r.call.id);
log(` └─ ${r.call.id} ended: ${status}`);
await sleep(GAP_MS);
}
log(`=== sweep done: placed=${placed} skipped=${skipped} ===`);
process.exit(0);
})();