Find all files related to the calling/voice system in this codebase. Look for:
1. Twilio integration files
2. Call-related components (dialer, call UI, etc.)
3. Call-related API routes
4. Any WebRTC or voice-related code

Map out the call flow: how does a call get initiated, connected, and what components are involved?

Return a summary of:
- Key files involved in calling
- The call initiation flow
- Any recent changes to these files (check git log)


I'll systematically search for all calling/voice system files and map out the call flow.


Bash

find /home/ccuser/rateright-growth -type f -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" | head -50


No files found


Good. Now let me search for more specific calling/UI components and API routes:


find /home/ccuser/rateright-growth/src -type f \( -name "*.tsx" -o -name "*.ts" -o -name "*.jsx" -o -name "*.js" \) | grep -i -E "(call|dial|voice)" | grep -v node_modules


find /home/ccuser/rateright-growth/src/components -type f \( -name "*.tsx" -o -name "*.ts" \) 2>/dev/null | head -30


Found 45 files
src/services/leaderboardService.js
src/services/badgesService.js
src/routes/achievements.js
public/assets/index-9N4YdW1G.js
public/assets/More-DHk9QUsz.js
public/assets/Battleground-eQY-fEJi.js
public/assets/CallList-Dm2OY2oA.js
public/assets/CallPrepPage-DuBRB5Mr.js
public/assets/index-Ck7O2q4X.js
public/assets/index-COQoGuD1.js
public/assets/More-BFfjroI1.js
public/assets/More-CNE3ZFeM.js
public/assets/Battleground-CTfFmTc_.js
public/assets/CallList-BRdbt1Ld.js
public/assets/CallList-D8pvDMoj.js
public/assets/CallPrepPage-BgrBD2VU.js
public/assets/CallPrepPage-Bs2twS5y.js
public/assets/Battleground-BykNQEzp.js
docs/tooltip-system-plan.md
docs/micro-optimisation-plan.md
docs/potential-bugs/03-frontend-components.md
docs/pre-call-briefing-plan.md
docs/gamification-0.1-plan.md
docs/LESSONS.md
docs/PAGE-BY-PAGE-TEST.md
docs/ROADMAP.md
docs/SELLABILITY-ROADMAP.md
docs/SYSTEM-INTEL.md
docs/TESTING-CHECKLIST.md
docs/VALUE-PROPOSITION.md
docs/WEAKNESS-FIX-REPORT.md
docs/0.1-PERCENT-TRANSFORMATION-PLAN.md
docs/BUILD-ELITE-CALL-UI.md
docs/BUILD-SEQUENCE.md
docs/CALLER-GUIDE.md
admin/src/pages/CallList.jsx
admin/src/pages/CallPrepPage.jsx
admin/src/context/PowerDialerContext.jsx
admin/src/components/gamification/UserStatsModal.jsx
admin/src/components/LiveCopilot.jsx
admin/src/components/BottomNav.jsx
admin/src/components/EliteCallUI.jsx
admin/src/components/FeatureEducation.jsx
admin/src/App.jsx
BUILD-GUIDE.md


Let me look at the main calling/voice components and files:


     1→/**
     2→ * Twilio service for SMS and Voice
     3→ */
     4→
     5→const twilio = require('twilio');
     6→const AccessToken = twilio.jwt.AccessToken;
     7→const VoiceGrant = AccessToken.VoiceGrant;
     8→
     9→// Initialize Twilio client (lazy initialization)
    10→let twilioClient = null;
    11→
    12→function getClient() {
    13→  if (!twilioClient) {
    14→    const accountSid = process.env.TWILIO_ACCOUNT_SID;
    15→    const authToken = process.env.TWILIO_AUTH_TOKEN;
    16→
    17→    if (!accountSid || !authToken) {
    18→      console.warn('Twilio not configured - missing ACCOUNT_SID or AUTH_TOKEN');
    19→      return null;
    20→    }
    21→
    22→    twilioClient = twilio(accountSid, authToken);
    23→  }
    24→  return twilioClient;
    25→}
    26→
    27→/**
    28→ * Generate a Voice access token for browser-based calling
    29→ * @param {string} identity - Unique identity for this user/session
    30→ * @returns {{success: boolean, token?: string, identity?: string, error?: string}}
    31→ */
    32→function generateVoiceToken(identity) {
    33→  const accountSid = process.env.TWILIO_ACCOUNT_SID;
    34→  const apiKeySid = process.env.TWILIO_API_KEY_SID;
    35→  const apiKeySecret = process.env.TWILIO_API_KEY_SECRET;
    36→  const twimlAppSid = process.env.TWILIO_TWIML_APP_SID;
    37→
    38→  if (!accountSid || !apiKeySid || !apiKeySecret) {
    39→    return { success: false, error: 'Twilio Voice not configured - missing API keys' };
    40→  }
    41→
    42→  try {
    43→    const token = new AccessToken(accountSid, apiKeySid, apiKeySecret, {
    44→      identity: identity,
    45→      ttl: 3600 // 1 hour
    46→    });
    47→
    48→    const voiceGrant = new VoiceGrant({
    49→      outgoingApplicationSid: twimlAppSid,
    50→      incomingAllow: true // Allow incoming calls to ring in the browser
    51→    });
    52→
    53→    token.addGrant(voiceGrant);
    54→
    55→    return {
    56→      success: true,
    57→      token: token.toJwt(),
    58→      identity: identity
    59→    };
    60→  } catch (error) {
    61→    console.error('Failed to generate Voice token:', error);
    62→    return { success: false, error: error.message };
    63→  }
    64→}
    65→
    66→/**
    67→ * Check if Voice calling is configured
    68→ * @returns {boolean}
    69→ */
    70→function isVoiceConfigured() {
    71→  return !!(process.env.TWILIO_ACCOUNT_SID &&
    72→            process.env.TWILIO_API_KEY_SID &&
    73→            process.env.TWILIO_API_KEY_SECRET &&
    74→            process.env.TWILIO_TWIML_APP_SID);
    75→}
    76→
    77→/**
    78→ * Send an SMS message
    79→ * @param {string} to - Recipient phone number (E.164 format)
    80→ * @param {string} body - Message body
    81→ * @returns {Promise<{success: boolean, messageSid?: string, error?: string}>}
    82→ */
    83→async function sendSMS(to, body) {
    84→  const client = getClient();
    85→  const from = process.env.TWILIO_PHONE_NUMBER;
    86→
    87→  if (!client) {
    88→    return { success: false, error: 'Twilio not configured' };
    89→  }
    90→
    91→  if (!from) {
    92→    return { success: false, error: 'TWILIO_PHONE_NUMBER not set' };
    93→  }
    94→
    95→  try {
    96→    const message = await client.messages.create({
    97→      body,
    98→      to,
    99→      from
   100→    });
   101→
   102→    console.log(`SMS sent to ${to}: ${message.sid}`);
   103→    return { success: true, messageSid: message.sid };
   104→  } catch (error) {
   105→    console.error(`Failed to send SMS to ${to}:`, error.message);
   106→    return { success: false, error: error.message };
   107→  }
   108→}
   109→
   110→/**
   111→ * Send batch SMS messages
   112→ * @param {Array<{to: string, body: string}>} messages - Array of messages
   113→ * @returns {Promise<{sent: number, failed: number, results: Array}>}
   114→ */
   115→async function sendBatchSMS(messages) {
   116→  const results = [];
   117→  let sent = 0;
   118→  let failed = 0;
   119→
   120→  for (const msg of messages) {
   121→    const result = await sendSMS(msg.to, msg.body);
   122→    results.push({ to: msg.to, ...result });
   123→
   124→    if (result.success) {
   125→      sent++;
   126→    } else {
   127→      failed++;
   128→    }
   129→
   130→    // Small delay between messages to avoid rate limiting
   131→    await new Promise(resolve => setTimeout(resolve, 100));
   132→  }
   133→
   134→  return { sent, failed, results };
   135→}
   136→
   137→/**
   138→ * Validate Twilio webhook signature
   139→ * @param {string} signature - X-Twilio-Signature header
   140→ * @param {string} url - Full webhook URL
   141→ * @param {object} params - Request body params
   142→ * @returns {boolean}
   143→ */
   144→function validateWebhookSignature(signature, url, params) {
   145→  const authToken = process.env.TWILIO_AUTH_TOKEN;
   146→
   147→  if (!authToken) {
   148→    console.warn('Cannot validate webhook - AUTH_TOKEN not set');
   149→    return false;
   150→  }
   151→
   152→  return twilio.validateRequest(authToken, signature, url, params);
   153→}
   154→
   155→/**
   156→ * Check if Twilio is configured
   157→ * @returns {boolean}
   158→ */
   159→function isConfigured() {
   160→  return !!(process.env.TWILIO_ACCOUNT_SID &&
   161→            process.env.TWILIO_AUTH_TOKEN &&
   162→            process.env.TWILIO_PHONE_NUMBER);
   163→}
   164→
   165→module.exports = {
   166→  sendSMS,
   167→  sendBatchSMS,
   168→  validateWebhookSignature,
   169→  isConfigured,
   170→  generateVoiceToken,
   171→  isVoiceConfigured
   172→};
   173→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→/**
     2→ * Call Logging API routes
     3→ */
     4→
     5→const express = require('express');
     6→const router = express.Router();
     7→const { supabase, supabaseAdmin } = require('../config/database');
     8→const perplexityService = require('../services/perplexity');
     9→const { awardXPForCall } = require('../services/xpService');
    10→const { checkRecord, RECORD_TYPES, createPulseEvent, getTodaysCallCount } = require('../services/leaderboardService');
    11→const { updateChallengeProgress, updateTeamChallengeProgress } = require('../services/challengesService');
    12→const { scoreCall, saveCallScore } = require('../services/repPerformance');
    13→
    14→/**
    15→ * POST /api/calls/log
    16→ * Log a call outcome
    17→ */
    18→router.post('/log', async (req, res) => {
    19→  try {
    20→    if (!supabase) {
    21→      return res.status(503).json({ error: 'Database not configured' });
    22→    }
    23→
    24→    const {
    25→      leadId,
    26→      outcome,
    27→      durationSeconds,
    28→      notes,
    29→      objections = [],
    30→      followUpAt,
    31→      handledBy,
    32→      // Auto-transcription fields
    33→      transcript,
    34→      aiSummary,
    35→      // Twilio CallSid for linking recording transcript
    36→      callSid,
    37→    } = req.body;
    38→
    39→    if (!leadId) {
    40→      return res.status(400).json({ error: 'leadId is required' });
    41→    }
    42→
    43→    if (!outcome) {
    44→      return res.status(400).json({ error: 'outcome is required' });
    45→    }
    46→
    47→    // Valid outcomes
    48→    const validOutcomes = [
    49→      'connected', 'voicemail', 'no_answer', 'busy', 'wrong_number',
    50→      'callback_scheduled', 'not_interested', 'interested', 'converted'
    51→    ];
    52→
    53→    if (!validOutcomes.includes(outcome)) {
    54→      return res.status(400).json({ error: `Invalid outcome. Must be one of: ${validOutcomes.join(', ')}` });
    55→    }
    56→
    57→    // Get lead (specific columns only for performance)
    58→    const { data: lead, error: leadError } = await supabase
    59→      .from('leads')
    60→      .select('id, assigned_to, phone, score, status, first_name, last_name, company')
    61→      .eq('id', leadId)
    62→      .single();
    63→
    64→    if (leadError || !lead) {
    65→      return res.status(404).json({ error: 'Lead not found' });
    66→    }
    67→
    68→    // Get the authenticated user ID for handled_by (prefer auth, fallback to passed value)
    69→    const callHandledBy = req.user?.id || handledBy || null;
    70→
    71→    // Auto-assign lead if unassigned
    72→    if (!lead.assigned_to && callHandledBy) {
    73→      await supabase
    74→        .from('leads')
    75→        .update({
    76→          assigned_to: callHandledBy,
    77→          assigned_at: new Date().toISOString()
    78→        })
    79→        .eq('id', leadId);
    80→      console.log(`Lead auto-assigned: ${leadId} to ${callHandledBy}`);
    81→    }
    82→
    83→    // Log communication with transcript and AI summary
    84→    const { data: comm, error: commError } = await supabase
    85→      .from('communications')
    86→      .insert({
    87→        lead_id: leadId,
    88→        type: 'call_outbound',
    89→        direction: 'outbound',
    90→        phone: lead.phone,
    91→        content: notes || null,
    92→        handled_by: callHandledBy, // Top-level for easy querying by rep performance
    93→        user_id: callHandledBy, // User who made the call (for assignment tracking)
    94→        // Denormalized columns for fast filtering (also kept in metadata for backwards compat)
    95→        outcome,
    96→        call_duration_seconds: durationSeconds || 0,
    97→        metadata: {
    98→          outcome,
    99→          duration_seconds: durationSeconds || 0,
   100→          objections,
   101→          handled_by: callHandledBy,
   102→          // Auto-transcription data
   103→          transcript: transcript || null,
   104→          ai_summary: aiSummary || null,
   105→          auto_transcribed: !!transcript,
   106→          // CallSid for linking recording transcript later
   107→          call_sid: callSid || null,
   108→        }
   109→      })
   110→      .select()
   111→      .single();
   112→
   113→    if (commError) {
   114→      console.error('Call log error:', commError.message);
   115→      return res.status(500).json({ error: 'Failed to log call' });
   116→    }
   117→
   118→    // Check for orphaned transcript from recording webhook (transcription runs before call is logged)
   119→    if (callSid && !transcript) {
   120→      try {
   121→        const { data: orphan } = await supabase
   122→          .from('copilot_sessions')
   123→          .select('id, transcript, recording_url, duration_seconds')
   124→          .eq('metadata->>call_sid', callSid)
   125→          .eq('metadata->>orphaned', 'true')
   126→          .single();
   127→
   128→        if (orphan && orphan.transcript) {
   129→          console.log(`[Calls] Found orphaned transcript for CallSid ${callSid}, attaching...`);
   130→
   131→          // Update the communication we just created
   132→          const { error: attachError } = await supabase
   133→            .from('communications')
   134→            .update({
   135→              metadata: {
   136→                ...comm.metadata,
   137→                transcript: orphan.transcript,
   138→                recording_url: orphan.recording_url,
   139→                recording_duration: orphan.duration_seconds,
   140→                auto_transcribed: true,
   141→                attached_from_orphan: true
   142→              }
   143→            })
   144→            .eq('id', comm.id);
   145→
   146→          if (!attachError) {
   147→            // Delete the orphan record
   148→            await supabase.from('copilot_sessions').delete().eq('id', orphan.id);
   149→            console.log(`[Calls] Transcript attached and orphan cleaned up`);
   150→
   151→            // Update the comm object so XP/scoring can use the transcript
   152→            comm.metadata = {
   153→              ...comm.metadata,
   154→              transcript: orphan.transcript,
   155→              recording_url: orphan.recording_url,
   156→              auto_transcribed: true
   157→            };
   158→          } else {
   159→            console.error('[Calls] Failed to attach orphaned transcript:', attachError.message);
   160→          }
   161→        }
   162→      } catch (orphanErr) {
   163→        console.warn('[Calls] Orphan transcript check failed:', orphanErr.message);
   164→      }
   165→    }
   166→
   167→    // Update lead status based on outcome
   168→    const leadUpdates = {
   169→      last_contact_at: new Date().toISOString(),
   170→      last_contact_type: 'call_outbound'
   171→    };
   172→
   173→    if (outcome === 'converted') {
   174→      leadUpdates.status = 'converted';
   175→    } else if (outcome === 'not_interested') {
   176→      leadUpdates.status = 'closed_lost';
   177→    } else if (outcome === 'interested') {
   178→      leadUpdates.status = 'engaged';
   179→    }
   180→
   181→    // Apply AI-recommended score adjustment
   182→    if (aiSummary?.score_adjustment) {
   183→      const newScore = Math.max(0, Math.min(100, (lead.score || 50) + aiSummary.score_adjustment));
   184→      leadUpdates.score = newScore;
   185→    }
   186→
   187→    // Apply AI-recommended status if not already set
   188→    if (!leadUpdates.status && aiSummary?.status_recommendation) {
   189→      leadUpdates.status = aiSummary.status_recommendation;
   190→    }
   191→
   192→    await supabase
   193→      .from('leads')
   194→      .update(leadUpdates)
   195→      .eq('id', leadId);
   196→
   197→    // Create follow-up callback if scheduled
   198→    let callbackId = null;
   199→    if (followUpAt) {
   200→      const { data: callback, error: cbError } = await supabase
   201→        .from('callbacks')
   202→        .insert({
   203→          lead_id: leadId,
   204→          phone: lead.phone,
   205→          reason: `Follow-up from ${outcome} call`,
   206→          priority: outcome === 'interested' ? 'high' : 'medium',
   207→          status: 'pending',
   208→          source_type: 'call_outbound',
   209→          source_id: comm.id,
   210→          scheduled_at: followUpAt,
   211→          handled_by: handledBy
   212→        })
   213→        .select('id')
   214→        .single();
   215→
   216→      if (!cbError && callback) {
   217→        callbackId = callback.id;
   218→      }
   219→    }
   220→
   221→    // Mark any existing pending callbacks as completed
   222→    let completeQuery = supabase
   223→      .from('callbacks')
   224→      .update({
   225→        status: 'completed',
   226→        completed_at: new Date().toISOString(),
   227→        handled_by: handledBy
   228→      })
   229→      .eq('lead_id', leadId)
   230→      .eq('status', 'pending');
   231→
   232→    // Only exclude new callback if one was created (fixes race condition)
   233→    if (callbackId) {
   234→      completeQuery = completeQuery.neq('id', callbackId);
   235→    }
   236→
   237→    const { error: completeError } = await completeQuery;
   238→    if (completeError) {
   239→      console.warn('Failed to complete pending callbacks:', completeError.message);
   240→    }
   241→
   242→    // Award XP for the call (fire and forget)
   243→    // Use authenticated user ID from auth middleware
   244→    const userId = req.user?.id || handledBy || 'default-user';
   245→    let xpResult = null;
   246→    try {
   247→      xpResult = await awardXPForCall(userId, outcome, leadId);
   248→      if (xpResult.success) {
   249→        console.log(`XP awarded for call: +${xpResult.totalAwarded} XP (${outcome})`);
   250→      }
   251→    } catch (xpError) {
   252→      console.warn('Failed to award XP:', xpError.message);
   253→    }
   254→
   255→    // Add note if provided (or if auto-transcribed)
   256→    if (notes || transcript) {
   257→      const noteContent = transcript
   258→        ? `[Auto-Transcribed Call - ${outcome}]\n\n${notes || aiSummary?.summary || transcript.substring(0, 500)}`
   259→        : `Call (${outcome}): ${notes}`;
   260→
   261→      await supabase
   262→        .from('lead_notes')
   263→        .insert({
   264→          lead_id: leadId,
   265→          content: noteContent,
   266→          created_by: handledBy || (transcript ? 'auto-transcribe' : 'system')
   267→        });
   268→    }
   269→
   270→    console.log(`Call logged: ${leadId} - ${outcome}${transcript ? ' (auto-transcribed)' : ''}`);
   271→
   272→    // Trigger background auto-research if we have a transcript
   273→    // Fire and forget - don't block the response
   274→    if (transcript && transcript.length >= 50) {
   275→      setImmediate(() => {
   276→        perplexityService.autoResearchFromTranscript(leadId, transcript)
   277→          .then(result => {
   278→            if (result.success) {
   279→              console.log(`[AutoResearch] Background research completed for lead ${leadId}`);
   280→            }
   281→          })
   282→          .catch(err => {
   283→            console.error(`[AutoResearch] Background research failed for lead ${leadId}:`, err.message);
   284→          });
   285→      });
   286→
   287→      // AI call scoring (fire and forget)
   288→      setImmediate(async () => {
   289→        try {
   290→          const callScore = await scoreCall(comm, transcript);
   291→          if (callScore) {
   292→            await saveCallScore(
   293→              comm.id,
   294→              leadId,
   295→              userId,
   296→              callScore,
   297→              outcome,
   298→              durationSeconds || 0
   299→            );
   300→            console.log(`[CallScore] Scored call ${comm.id}: ${callScore.overall_score}/10`);
   301→          }
   302→        } catch (err) {
   303→          console.error(`[CallScore] Failed to score call:`, err.message);
   304→        }
   305→      });
   306→    }
   307→
   308→    // Update lead dossier with call summary (fire and forget)
   309→    setImmediate(async () => {
   310→      try {
   311→        // Get current dossier or create new one
   312→        let { data: dossier } = await supabase
   313→          .from('lead_dossier')
   314→          .select('call_summaries, current_status')
   315→          .eq('lead_id', leadId)
   316→          .single();
   317→
   318→        const currentSummaries = dossier?.call_summaries || [];
   319→
   320→        // Extract key points from AI summary if available
   321→        const keyPoints = [];
   322→        if (aiSummary?.summary) keyPoints.push(aiSummary.summary);
   323→        if (aiSummary?.next_step) keyPoints.push(`Next: ${aiSummary.next_step}`);
   324→        if (aiSummary?.coaching_note) keyPoints.push(`Coaching: ${aiSummary.coaching_note}`);
   325→
   326→        const newSummary = {
   327→          date: new Date().toISOString(),
   328→          duration: durationSeconds || 0,
   329→          outcome,
   330→          key_points: keyPoints,
   331→          next_steps: aiSummary?.next_step || null,
   332→          mood: aiSummary?.sentiment || null,
   333→          notes: notes?.substring(0, 500) || null,
   334→        };
   335→
   336→        // Update current status
   337→        const newStatus = {
   338→          stage: leadUpdates.status || lead.status,
   339→          temperature: outcome === 'interested' ? 'hot' : outcome === 'converted' ? 'closed' : 'warm',
   340→          last_contact: new Date().toISOString(),
   341→          next_action: aiSummary?.next_step || (followUpAt ? 'Follow up scheduled' : null),
   342→          follow_up_date: followUpAt || null,
   343→        };
   344→
   345→        await (supabaseAdmin || supabase)
   346→          .from('lead_dossier')
   347→          .upsert({
   348→            lead_id: leadId,
   349→            call_summaries: [...currentSummaries, newSummary],
   350→            current_status: newStatus,
   351→          }, {
   352→            onConflict: 'lead_id'
   353→          });
   354→
   355→        console.log(`[Dossier] Call summary added for lead ${leadId}`);
   356→      } catch (dossierError) {
   357→        console.error('[Dossier] Failed to update:', dossierError.message);
   358→      }
   359→    });
   360→
   361→    // Auto-enroll in sequence for no-answer calls (fire and forget)
   362→    if (outcome === 'no_answer') {
   363→      setImmediate(async () => {
   364→        try {
   365→          // Check if lead is opted out of sequences
   366→          const { data: leadData } = await supabase
   367→            .from('leads')
   368→            .select('metadata')
   369→            .eq('id', leadId)
   370→            .single();
   371→
   372→          if (leadData?.metadata?.sequence_opted_out) {
   373→            console.log(`[Sequence] Lead ${leadId} opted out, skipping auto-enroll`);
   374→            return;
   375→          }
   376→
   377→          // Check if already enrolled in an active sequence
   378→          const { data: existing } = await supabase
   379→            .from('sequence_enrollments')
   380→            .select('id')
   381→            .eq('lead_id', leadId)
   382→            .eq('status', 'active')
   383→            .limit(1);
   384→
   385→          if (existing?.length > 0) {
   386→            console.log(`[Sequence] Lead ${leadId} already enrolled, skipping`);
   387→            return;
   388→          }
   389→
   390→          // Find appropriate sequence based on lead type (use admin to bypass RLS)
   391→          const db = supabaseAdmin || supabase;
   392→          const leadType = leadData?.metadata?.lead_type || 'worker';
   393→          const { data: sequences, error: seqErr } = await db
   394→            .from('sequences')
   395→            .select('id, name')
   396→            .eq('channel', 'sms')
   397→            .eq('is_active', true)
   398→            .ilike('name', `%${leadType}%`)
   399→            .limit(1);
   400→
   401→          if (seqErr) {
   402→            console.error(`[Sequence] Query error:`, seqErr.message);
   403→          }
   404→
   405→          // Fallback to any active SMS sequence if no match
   406→          let sequenceId = sequences?.[0]?.id;
   407→          let sequenceName = sequences?.[0]?.name;
   408→          if (!sequenceId) {
   409→            const { data: fallbackSeq, error: fallbackErr } = await db
   410→              .from('sequences')
   411→              .select('id, name')
   412→              .eq('channel', 'sms')
   413→              .eq('is_active', true)
   414→              .limit(1);
   415→            if (fallbackErr) {
   416→              console.error(`[Sequence] Fallback query error:`, fallbackErr.message);
   417→            }
   418→            sequenceId = fallbackSeq?.[0]?.id;
   419→            sequenceName = fallbackSeq?.[0]?.name;
   420→          }
   421→
   422→          if (!sequenceId) {
   423→            console.log(`[Sequence] No active sequences found for auto-enroll (leadType: ${leadType})`);
   424→            return;
   425→          }
   426→
   427→          // Enroll the lead (use admin to bypass RLS)
   428→          const { data: enrollment, error: enrollError } = await db
   429→            .from('sequence_enrollments')
   430→            .insert({
   431→              sequence_id: sequenceId,
   432→              lead_id: leadId,
   433→              status: 'active',
   434→              current_step: 0,
   435→              enrolled_at: new Date().toISOString(),
   436→              next_step_at: new Date().toISOString() // Start immediately
   437→            })
   438→            .select()
   439→            .single();
   440→
   441→          if (enrollError) {
   442→            console.error(`[Sequence] Failed to auto-enroll:`, enrollError.message);
   443→          } else {
   444→            console.log(`[Sequence] Auto-enrolled lead ${leadId} in "${sequenceName}" after no-answer`);
   445→          }
   446→        } catch (seqError) {
   447→          console.error('[Sequence] Auto-enroll error:', seqError.message);
   448→        }
   449→      });
   450→    }
   451→
   452→    // Battleground: Update challenges and check records (fire and forget)
   453→    setImmediate(async () => {
   454→      try {
   455→        // Get user display name
   456→        const { data: userXp } = await supabase
   457→          .from('user_xp')
   458→          .select('display_name')
   459→          .eq('user_id', userId)
   460→          .single();
   461→        const userName = userXp?.display_name || 'Someone';
   462→
   463→        // Update challenge progress
   464→        await updateChallengeProgress(userId, 'call', 1);
   465→        await updateTeamChallengeProgress('call', 1);
   466→
   467→        // Check for daily calls record
   468→        const todaysCalls = await getTodaysCallCount(userId);
   469→        await checkRecord(RECORD_TYPES.MOST_CALLS_DAY, userId, userName, todaysCalls);
   470→
   471→        // If conversion, create pulse event and check records
   472→        if (outcome === 'converted') {
   473→          await createPulseEvent({
   474→            event_type: 'conversion',
   475→            user_id: userId,
   476→            user_name: userName,
   477→            title: `${userName} just closed a deal!`,
   478→            subtitle: lead.name || lead.company || 'New customer',
   479→            emoji: '🎉',
   480→            link_type: 'lead',
   481→            link_id: leadId,
   482→          });
   483→
   484→          await updateChallengeProgress(userId, 'conversion', 1);
   485→          await updateTeamChallengeProgress('conversion', 1);
   486→        }
   487→      } catch (bgError) {
   488→        console.error('[Battleground] Background update error:', bgError.message);
   489→      }
   490→    });
   491→
   492→    res.json({
   493→      success: true,
   494→      communicationId: comm.id,
   495→      callbackId,
   496→      outcome,
   497→      leadStatus: leadUpdates.status || lead.status,
   498→      newScore: leadUpdates.score,
   499→      autoTranscribed: !!transcript,
   500→      xp: xpResult?.success ? {
   501→        awarded: xpResult.totalAwarded,
   502→        streakBonus: xpResult.streakBonus,
   503→        newTotal: xpResult.newTotal,
   504→        level: xpResult.level,
   505→        levelName: xpResult.levelName,
   506→        levelUp: xpResult.levelUp,
   507→        currentStreak: xpResult.currentStreak,
   508→      } : null,
   509→    });
   510→
   511→  } catch (error) {
   512→    console.error('Call log error:', error);
   513→    res.status(500).json({ error: 'Failed to log call' });
   514→  }
   515→});
   516→
   517→/**
   518→ * GET /api/calls/outcomes
   519→ * Get list of valid call outcomes
   520→ */
   521→router.get('/outcomes', (req, res) => {
   522→  res.json({
   523→    outcomes: [
   524→      { value: 'connected', label: 'Connected', description: 'Spoke with lead' },
   525→      { value: 'voicemail', label: 'Voicemail', description: 'Left voicemail' },
   526→      { value: 'no_answer', label: 'No Answer', description: 'No answer, no voicemail' },
   527→      { value: 'busy', label: 'Busy', description: 'Line busy' },
   528→      { value: 'wrong_number', label: 'Wrong Number', description: 'Number incorrect' },
   529→      { value: 'callback_scheduled', label: 'Callback Scheduled', description: 'Agreed to call back' },
   530→      { value: 'not_interested', label: 'Not Interested', description: 'Declined' },
   531→      { value: 'interested', label: 'Interested', description: 'Wants to proceed' },
   532→      { value: 'converted', label: 'Converted', description: 'Signed up / converted' }
   533→    ]
   534→  });
   535→});
   536→
   537→/**
   538→ * GET /api/calls/history/:leadId
   539→ * Get call history for a specific lead
   540→ */
   541→router.get('/history/:leadId', async (req, res) => {
   542→  try {
   543→    const { leadId } = req.params;
   544→    const limit = parseInt(req.query.limit) || 5;
   545→
   546→    if (!supabase) {
   547→      return res.status(503).json({ error: 'Database not configured' });
   548→    }
   549→
   550→    const { data: calls, error } = await (supabaseAdmin || supabase)
   551→      .from('communications')
   552→      .select('id, created_at, type, metadata')
   553→      .eq('lead_id', leadId)
   554→      .in('type', ['call_outbound', 'call_inbound', 'call'])
   555→      .order('created_at', { ascending: false })
   556→      .limit(limit);
   557→
   558→    if (error) {
   559→      console.error('Error fetching call history:', error);
   560→      return res.status(500).json({ error: 'Failed to fetch call history' });
   561→    }
   562→
   563→    // Format response
   564→    const history = (calls || []).map(call => ({
   565→      id: call.id,
   566→      date: call.created_at,
   567→      type: call.type,
   568→      outcome: call.metadata?.outcome || call.metadata?.call_outcome || 'Unknown',
   569→      duration: call.metadata?.duration_seconds || call.metadata?.duration || 0,
   570→      notes: call.metadata?.notes || null
   571→    }));
   572→
   573→    res.json({ calls: history });
   574→  } catch (error) {
   575→    console.error('Call history error:', error);
   576→    res.status(500).json({ error: 'Failed to fetch call history' });
   577→  }
   578→});
   579→
   580→/**
   581→ * GET /api/calls/recording/:communicationId
   582→ * Stream call recording audio (proxied through backend for Twilio auth)
   583→ */
   584→router.get('/recording/:communicationId', async (req, res) => {
   585→  try {
   586→    const { communicationId } = req.params;
   587→
   588→    if (!supabase) {
   589→      return res.status(503).json({ error: 'Database not configured' });
   590→    }
   591→
   592→    // Get the communication record
   593→    const { data: comm, error } = await supabase
   594→      .from('communications')
   595→      .select('metadata')
   596→      .eq('id', communicationId)
   597→      .single();
   598→
   599→    if (error || !comm) {
   600→      return res.status(404).json({ error: 'Communication not found' });
   601→    }
   602→
   603→    const recordingUrl = comm.metadata?.recording_url;
   604→    if (!recordingUrl) {
   605→      return res.status(404).json({ error: 'No recording available for this call' });
   606→    }
   607→
   608→    // Twilio recordings require Basic Auth with Account SID and Auth Token
   609→    const twilioAccountSid = process.env.TWILIO_ACCOUNT_SID;
   610→    const twilioAuthToken = process.env.TWILIO_AUTH_TOKEN;
   611→
   612→    if (!twilioAccountSid || !twilioAuthToken) {
   613→      return res.status(503).json({ error: 'Twilio credentials not configured' });
   614→    }
   615→
   616→    // Fetch the recording from Twilio with auth
   617→    const audioUrl = `${recordingUrl}.mp3`;
   618→    const authHeader = 'Basic ' + Buffer.from(`${twilioAccountSid}:${twilioAuthToken}`).toString('base64');
   619→
   620→    const response = await fetch(audioUrl, {
   621→      headers: {
   622→        'Authorization': authHeader
   623→      }
   624→    });
   625→
   626→    if (!response.ok) {
   627→      console.error(`[Recording] Twilio fetch failed: ${response.status}`);
   628→      return res.status(response.status).json({ error: 'Failed to fetch recording from Twilio' });
   629→    }
   630→
   631→    // Stream the audio back to the client
   632→    res.set('Content-Type', 'audio/mpeg');
   633→    res.set('Content-Disposition', `inline; filename="call-${communicationId}.mp3"`);
   634→
   635→    // Handle the response body as a stream
   636→    const arrayBuffer = await response.arrayBuffer();
   637→    res.send(Buffer.from(arrayBuffer));
   638→
   639→  } catch (error) {
   640→    console.error('[Recording] Playback error:', error);
   641→    res.status(500).json({ error: 'Failed to stream recording' });
   642→  }
   643→});
   644→
   645→module.exports = router;
   646→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→/**
     2→ * Voice calling routes using Twilio
     3→ */
     4→
     5→const express = require('express');
     6→const router = express.Router();
     7→const voiceWebhooks = express.Router(); // Public routes for Twilio callbacks
     8→const VoiceResponse = require('twilio').twiml.VoiceResponse;
     9→const { generateVoiceToken, isVoiceConfigured } = require('../services/twilio');
    10→const { supabase } = require('../config/database');
    11→const { extractInboundIntel, generatePostCallSummary } = require('../services/ai');
    12→
    13→// Cache callSid -> leadId for recording transcription (expires after 1 hour)
    14→const callLeadCache = new Map();
    15→const CACHE_TTL = 60 * 60 * 1000; // 1 hour
    16→
    17→// Validate required environment variable
    18→const APP_URL = process.env.APP_URL;
    19→if (!APP_URL) {
    20→  console.error('SECURITY WARNING: APP_URL environment variable not set. Voice webhooks may not work correctly.');
    21→}
    22→
    23→/**
    24→ * GET /api/voice/token
    25→ * Generate a Voice access token for browser-based calling
    26→ */
    27→router.get('/token', (req, res) => {
    28→  // Use fixed identity 'agent' so inbound calls can find the client
    29→  const identity = 'agent';
    30→
    31→  const result = generateVoiceToken(identity);
    32→
    33→  if (!result.success) {
    34→    return res.status(500).json({ error: result.error });
    35→  }
    36→
    37→  res.json({
    38→    token: result.token,
    39→    identity: result.identity
    40→  });
    41→});
    42→
    43→/**
    44→ * GET /api/voice/twiml
    45→ * Test endpoint to verify webhook is accessible
    46→ */
    47→voiceWebhooks.get('/twiml', (req, res) => {
    48→  res.json({
    49→    status: 'ok',
    50→    message: 'TwiML webhook is accessible (use POST for actual calls)',
    51→    method: 'GET'
    52→  });
    53→});
    54→
    55→/**
    56→ * POST /api/voice/twiml
    57→ * TwiML instructions for outbound calls
    58→ * This is called by Twilio when a call is initiated from the browser
    59→ * PUBLIC - no auth required (Twilio calls this directly)
    60→ */
    61→voiceWebhooks.post('/twiml', (req, res) => {
    62→  console.log('[Voice TwiML] Received request:', req.body);
    63→  const { To, leadId, CallSid } = req.body;
    64→  const appUrl = APP_URL || 'https://rateright-growth-production.up.railway.app';
    65→  const wsUrl = appUrl.replace('https://', 'wss://').replace('http://', 'ws://');
    66→
    67→  const twiml = new VoiceResponse();
    68→
    69→  if (To) {
    70→    // Real-time Media Stream disabled - using post-call recording transcription instead
    71→    // Recording quality is better than real-time phone audio (8kHz mulaw)
    72→
    73→    // Cache leadId for this call so we can link the recording later
    74→    if (CallSid && leadId) {
    75→      callLeadCache.set(CallSid, { leadId, timestamp: Date.now() });
    76→      console.log(`[Voice TwiML] Cached leadId ${leadId} for call ${CallSid}`);
    77→    }
    78→
    79→    // Dial the number using our Twilio phone number as caller ID
    80→    const dial = twiml.dial({
    81→      callerId: process.env.TWILIO_PHONE_NUMBER,
    82→      record: 'record-from-answer-dual', // Record both sides
    83→      recordingStatusCallback: `${appUrl}/api/voice/recording-status`,
    84→      recordingStatusCallbackMethod: 'POST'
    85→    });
    86→
    87→    dial.number({
    88→      statusCallback: `${appUrl}/api/voice/call-status`,
    89→      statusCallbackEvent: ['initiated', 'ringing', 'answered', 'completed'],
    90→      statusCallbackMethod: 'POST'
    91→    }, To);
    92→
    93→    console.log('[Voice TwiML] Generated with Media Stream:', twiml.toString().substring(0, 300));
    94→  } else {
    95→    twiml.say('No phone number provided');
    96→  }
    97→
    98→  res.type('text/xml');
    99→  res.send(twiml.toString());
   100→});
   101→
   102→/**
   103→ * POST /api/voice/inbound
   104→ * Handle incoming calls to the Twilio number
   105→ * PUBLIC - no auth required (Twilio calls this directly)
   106→ */
   107→voiceWebhooks.post('/inbound', async (req, res) => {
   108→  const { From, To, CallSid } = req.body;
   109→  console.log(`[Voice Inbound] Call from ${From} to ${To}, CallSid: ${CallSid}`);
   110→
   111→  const { normalizeAustralianPhone, formatPhoneDisplay } = require('../utils/phone');
   112→  const { sendInboundCallAlert } = require('../services/slack');
   113→
   114→  const twiml = new VoiceResponse();
   115→  const normalizedPhone = normalizeAustralianPhone(From);
   116→
   117→  // Look up caller in leads database
   118→  let lead = null;
   119→  let callerName = 'Unknown Caller';
   120→  let isHotLead = false;
   121→  let inboundCallCount = 0;
   122→  let isHighIntent = false;
   123→
   124→  if (supabase && From) {
   125→    if (normalizedPhone) {
   126→      // Try multiple phone formats in a single OR query (fixes N+1 pattern)
   127→      const format1 = normalizedPhone;                        // +61412345678 (E.164)
   128→      const format2 = '0' + normalizedPhone.substring(3);     // 0412345678 (local)
   129→      const format3 = normalizedPhone.substring(3);           // 412345678 (short)
   130→      const format4 = normalizedPhone.substring(1);           // 61412345678 (no + prefix)
   131→
   132→      const { data } = await supabase
   133→        .from('leads')
   134→        .select('id, first_name, last_name, company, phone, score, metadata')
   135→        .or(`phone.eq.${format1},phone.eq.${format2},phone.eq.${format3},phone.eq.${format4}`)
   136→        .limit(1)
   137→        .maybeSingle();
   138→
   139→      if (data) {
   140→        lead = data;
   141→        callerName = `${lead.first_name || ''} ${lead.last_name || ''}`.trim() || 'Unknown';
   142→        if (lead.company) callerName += ` from ${lead.company}`;
   143→
   144→        // INBOUND CALL SCORING
   145→        // Lead calling YOU = high intent = +20 points
   146→        const currentScore = lead.score || 0;
   147→        const newScore = Math.min(100, currentScore + 20); // Cap at 100
   148→
   149→        // Track inbound call count
   150→        inboundCallCount = (lead.metadata?.inbound_call_count || 0) + 1;
   151→        isHighIntent = inboundCallCount >= 3; // 3+ calls = High Intent
   152→
   153→        // Update the lead with new score and call count
   154→        const now = new Date().toISOString();
   155→        const { error: updateError } = await supabase
   156→          .from('leads')
   157→          .update({
   158→            score: newScore,
   159→            // Smart ranking fields
   160→            last_inbound_at: now,
   161→            last_inbound_type: 'call_inbound',
   162→            is_high_intent: isHighIntent,
   163→            metadata: {
   164→              ...lead.metadata,
   165→              inbound_call_count: inboundCallCount,
   166→              is_high_intent: isHighIntent,
   167→              last_inbound_call_at: now,
   168→              score_boost_reason: inboundCallCount === 1
   169→                ? 'Called you back (+20 points)'
   170→                : `Called you ${inboundCallCount} times (+20 points each)`
   171→            }
   172→          })
   173→          .eq('id', lead.id);
   174→
   175→        if (updateError) {
   176→          console.error('[Voice Inbound] Failed to update lead score:', updateError.message);
   177→        } else {
   178→          console.log(`[Voice Inbound] Lead ${lead.id} score: ${currentScore} → ${newScore} (call #${inboundCallCount})`);
   179→          lead.score = newScore; // Update local reference
   180→
   181→          // Refresh call list priority for this lead (async, non-blocking)
   182→          const { refreshLeadPriority } = require('../services/callListRanker');
   183→          refreshLeadPriority(lead.id).catch(err => {
   184→            console.warn('[Voice Inbound] Failed to refresh lead priority:', err.message);
   185→          });
   186→        }
   187→
   188→        isHotLead = newScore >= 80;
   189→      }
   190→    }
   191→  }
   192→
   193→  // Log to communications table
   194→  if (supabase) {
   195→    const highIntentNote = isHighIntent ? ` [HIGH INTENT - Call #${inboundCallCount}]` : '';
   196→    const { error: commError } = await supabase.from('communications').insert({
   197→      lead_id: lead?.id || null,
   198→      type: 'call_inbound',
   199→      direction: 'inbound',
   200→      phone: normalizedPhone || From,
   201→      content: `Incoming call from ${callerName}${highIntentNote}`,
   202→      external_id: CallSid,
   203→      metadata: {
   204→        twilio_from: From,
   205→        twilio_to: To,
   206→        caller_name: callerName,
   207→        call_sid: CallSid,
   208→        is_hot_lead: isHotLead,
   209→        is_high_intent: isHighIntent,
   210→        inbound_call_count: inboundCallCount,
   211→        lead_score: lead?.score || null,
   212→        score_boosted: lead ? true : false
   213→      }
   214→    });
   215→
   216→    if (commError) {
   217→      console.error('[Voice Inbound] Failed to log communication:', commError.message);
   218→    }
   219→  }
   220→
   221→  // Send Slack notification (with hot lead and high intent indicator)
   222→  try {
   223→    await sendInboundCallAlert(lead, formatPhoneDisplay(normalizedPhone || From), callerName, isHotLead, isHighIntent, inboundCallCount);
   224→  } catch (err) {
   225→    console.error('[Voice Inbound] Failed to send Slack alert:', err.message);
   226→  }
   227→
   228→  // Ring the browser client
   229→  // HOT LEADS get 60 seconds ring time, others get 30
   230→  const ringTimeout = isHotLead ? 60 : 30;
   231→
   232→  // Cache leadId for this call so recording can be linked later
   233→  if (CallSid && lead?.id) {
   234→    callLeadCache.set(CallSid, { leadId: lead.id, timestamp: Date.now() });
   235→    console.log(`[Voice Inbound] Cached leadId ${lead.id} for call ${CallSid}`);
   236→  }
   237→
   238→  const appUrl = APP_URL || 'https://rateright-growth-production.up.railway.app';
   239→  const dial = twiml.dial({
   240→    callerId: From,
   241→    timeout: ringTimeout,
   242→    action: `${appUrl}/api/voice/inbound-status?leadId=${lead?.id || ''}&isHot=${isHotLead}&phone=${encodeURIComponent(normalizedPhone || From)}`,
   243→    method: 'POST',
   244→    record: 'record-from-answer-dual',
   245→    recordingStatusCallback: `${appUrl}/api/voice/recording-status`,
   246→    recordingStatusCallbackMethod: 'POST'
   247→  });
   248→
   249→  // Dial the Twilio Client (browser) - use 'agent' as the identity
   250→  dial.client('agent');
   251→
   252→  console.log(`[Voice Inbound] Ringing browser client for: ${callerName} (hot: ${isHotLead}, timeout: ${ringTimeout}s)`);
   253→
   254→  res.type('text/xml');
   255→  res.send(twiml.toString());
   256→});
   257→
   258→/**
   259→ * POST /api/voice/inbound-status
   260→ * Handle what happens after inbound call attempt (e.g., voicemail)
   261→ * Smart missed call handling for hot leads
   262→ * PUBLIC - no auth required (Twilio calls this directly)
   263→ */
   264→voiceWebhooks.post('/inbound-status', async (req, res) => {
   265→  const { DialCallStatus, CallSid, From } = req.body;
   266→  const leadId = req.query.leadId || null;
   267→  const isHot = req.query.isHot === 'true';
   268→  const phone = req.query.phone ? decodeURIComponent(req.query.phone) : From;
   269→
   270→  console.log(`[Voice Inbound Status] Call ${CallSid}: ${DialCallStatus} (leadId: ${leadId}, hot: ${isHot})`);
   271→
   272→  const twiml = new VoiceResponse();
   273→
   274→  if (DialCallStatus !== 'completed' && DialCallStatus !== 'answered') {
   275→    // MISSED CALL - Smart handling
   276→
   277→    // 1. Create callback (HIGH priority for hot leads)
   278→    if (supabase) {
   279→      const { sendMissedCallAlert } = require('../services/slack');
   280→      const { sendSMS } = require('../services/twilio');
   281→
   282→      // Create callback
   283→      const { error: cbError } = await supabase.from('callbacks').insert({
   284→        lead_id: leadId || null,
   285→        phone: phone,
   286→        reason: isHot ? '🔥 HOT LEAD MISSED CALL - Call back IMMEDIATELY!' : 'Missed inbound call',
   287→        priority: isHot ? 'urgent' : 'high',
   288→        status: 'pending',
   289→        source_type: 'missed_call',
   290→        notes: `Caller hung up or went to voicemail. ${isHot ? 'This is a hot lead - prioritize!' : ''}`
   291→      });
   292→
   293→      if (cbError) {
   294→        console.error('[Voice Inbound Status] Failed to create callback:', cbError.message);
   295→      } else {
   296→        console.log(`[Voice Inbound Status] Callback created (priority: ${isHot ? 'urgent' : 'high'})`);
   297→      }
   298→
   299→      // 2. For HOT LEADS: Auto-send SMS
   300→      if (isHot && phone) {
   301→        const smsResult = await sendSMS(phone,
   302→          "Sorry I missed your call! I'm wrapping up something now and will call you back in 5 minutes. - RateRight"
   303→        );
   304→
   305→        if (smsResult.success) {
   306→          console.log('[Voice Inbound Status] Auto-SMS sent to hot lead');
   307→
   308→          // Log the SMS
   309→          await supabase.from('communications').insert({
   310→            lead_id: leadId || null,
   311→            type: 'sms_outbound',
   312→            direction: 'outbound',
   313→            phone: phone,
   314→            content: "Sorry I missed your call! I'm wrapping up something now and will call you back in 5 minutes. - RateRight",
   315→            external_id: smsResult.messageSid,
   316→            metadata: {
   317→              auto_reply: true,
   318→              trigger: 'missed_hot_lead_call'
   319→            }
   320→          });
   321→        }
   322→      }
   323→
   324→      // 3. Send Slack alert
   325→      try {
   326→        await sendMissedCallAlert(leadId, phone, isHot);
   327→      } catch (err) {
   328→        console.error('[Voice Inbound Status] Failed to send Slack alert:', err.message);
   329→      }
   330→    }
   331→
   332→    // Voicemail prompt - professional branded greeting
   333→    twiml.say({ voice: 'Polly.Nicole', language: 'en-AU' },
   334→      "Hi, you've reached the RateRight team. We're currently helping other customers, but your call is important to us. Please leave a message with your name and number after the beep, and we'll get back to you shortly. Or feel free to send us a text. Thanks for calling."
   335→    );
   336→    twiml.record({
   337→      maxLength: 120,
   338→      transcribe: true,
   339→      transcribeCallback: `${APP_URL || 'https://rateright-growth-production.up.railway.app'}/api/voice/voicemail?leadId=${leadId || ''}`,
   340→      action: `${APP_URL || 'https://rateright-growth-production.up.railway.app'}/api/voice/voicemail-complete?leadId=${leadId || ''}&phone=${encodeURIComponent(phone)}`,
   341→      method: 'POST'
   342→    });
   343→  }
   344→
   345→  res.type('text/xml');
   346→  res.send(twiml.toString());
   347→});
   348→
   349→/**
   350→ * POST /api/voice/voicemail
   351→ * Handle voicemail transcription (comes AFTER recording is complete)
   352→ * Updates the existing voicemail record with transcription
   353→ * PUBLIC - no auth required (Twilio calls this directly)
   354→ */
   355→voiceWebhooks.post('/voicemail', async (req, res) => {
   356→  const { TranscriptionText, RecordingUrl, From, CallSid } = req.body;
   357→  const leadId = req.query.leadId || null;
   358→
   359→  console.log(`[Voicemail Transcription] CallSid ${CallSid}: "${TranscriptionText?.substring(0, 50)}..."`);
   360→
   361→  // Update the existing voicemail record with transcription
   362→  if (supabase && CallSid && TranscriptionText) {
   363→    const { error } = await supabase
   364→      .from('communications')
   365→      .update({
   366→        content: TranscriptionText,
   367→        metadata: supabase.rpc ? undefined : {
   368→          transcription: TranscriptionText,
   369→          transcription_completed: true
   370→        }
   371→      })
   372→      .eq('external_id', CallSid)
   373→      .eq('type', 'voicemail');
   374→
   375→    // If update didn't match, try updating by pattern match on external_id
   376→    if (error) {
   377→      console.error('[Voicemail Transcription] Failed to update:', error.message);
   378→    } else {
   379→      console.log('[Voicemail Transcription] Record updated with transcription');
   380→
   381→      // ASYNC: Extract intel from voicemail transcription (fire and forget)
   382→      if (leadId && TranscriptionText) {
   383→        extractInboundIntel(leadId, TranscriptionText, 'voicemail').catch(err => {
   384→          console.error('[InboundIntel] Voicemail extraction failed:', err.message);
   385→        });
   386→      }
   387→    }
   388→  }
   389→
   390→  res.sendStatus(200);
   391→});
   392→
   393→/**
   394→ * POST /api/voice/voicemail-complete
   395→ * Handle voicemail recording complete - SAVE IMMEDIATELY
   396→ * PUBLIC - no auth required (Twilio calls this directly)
   397→ */
   398→voiceWebhooks.post('/voicemail-complete', async (req, res) => {
   399→  const { RecordingUrl, RecordingDuration, RecordingSid, CallSid, From } = req.body;
   400→  const leadId = req.query.leadId || null;
   401→  const phone = req.query.phone ? decodeURIComponent(req.query.phone) : From;
   402→
   403→  console.log(`[Voicemail Complete] Recording ${RecordingSid}, Duration: ${RecordingDuration}s, URL: ${RecordingUrl}`);
   404→
   405→  const { normalizeAustralianPhone } = require('../utils/phone');
   406→  const { sendVoicemailAlert } = require('../services/slack');
   407→  const normalizedPhone = normalizeAustralianPhone(phone) || phone;
   408→
   409→  // SAVE VOICEMAIL IMMEDIATELY (don't wait for transcription)
   410→  if (supabase && RecordingUrl) {
   411→    // Look up lead name if we have leadId
   412→    let callerName = 'Unknown Caller';
   413→    if (leadId) {
   414→      const { data: lead } = await supabase
   415→        .from('leads')
   416→        .select('first_name, last_name')
   417→        .eq('id', leadId)
   418→        .single();
   419→
   420→      if (lead) {
   421→        callerName = `${lead.first_name || ''} ${lead.last_name || ''}`.trim() || 'Unknown';
   422→      }
   423→    }
   424→
   425→    const { error: commError } = await supabase.from('communications').insert({
   426→      lead_id: leadId || null,
   427→      type: 'voicemail',
   428→      direction: 'inbound',
   429→      phone: normalizedPhone,
   430→      content: '[Voicemail - transcription pending...]',
   431→      external_id: CallSid,
   432→      metadata: {
   433→        recording_url: RecordingUrl,
   434→        recording_sid: RecordingSid,
   435→        recording_duration: parseInt(RecordingDuration) || 0,
   436→        transcription_pending: true,
   437→        caller_name: callerName
   438→      }
   439→    });
   440→
   441→    if (commError) {
   442→      console.error('[Voicemail Complete] Failed to save:', commError.message);
   443→    } else {
   444→      console.log('[Voicemail Complete] Voicemail saved to database');
   445→    }
   446→
   447→    // Send Slack notification
   448→    try {
   449→      await sendVoicemailAlert(leadId, normalizedPhone, parseInt(RecordingDuration) || 0, callerName);
   450→    } catch (err) {
   451→      console.error('[Voicemail Complete] Failed to send Slack alert:', err.message);
   452→    }
   453→  }
   454→
   455→  const twiml = new VoiceResponse();
   456→  twiml.say({ voice: 'Polly.Nicole', language: 'en-AU' }, "Thanks for your message. Someone from our team will be in touch shortly. Have a great day!");
   457→  twiml.hangup();
   458→
   459→  res.type('text/xml');
   460→  res.send(twiml.toString());
   461→});
   462→
   463→/**
   464→ * POST /api/voice/call-status
   465→ * Webhook for call status updates
   466→ * PUBLIC - no auth required (Twilio calls this directly)
   467→ */
   468→voiceWebhooks.post('/call-status', async (req, res) => {
   469→  const { CallSid, CallStatus, To, From, CallDuration, Timestamp } = req.body;
   470→
   471→  console.log(`Call ${CallSid} status: ${CallStatus}, duration: ${CallDuration || 0}s`);
   472→
   473→  // Could save call status to database here
   474→  // For now just acknowledge
   475→  res.sendStatus(200);
   476→});
   477→
   478→/**
   479→ * POST /api/voice/recording-status
   480→ * Webhook for recording completion - triggers transcription
   481→ * PUBLIC - no auth required (Twilio calls this directly)
   482→ */
   483→voiceWebhooks.post('/recording-status', async (req, res) => {
   484→  const { CallSid, RecordingSid, RecordingUrl, RecordingDuration, RecordingStatus } = req.body;
   485→
   486→  console.log(`Recording ${RecordingSid} for call ${CallSid}: ${RecordingStatus}`);
   487→
   488→  if (RecordingStatus === 'completed' && RecordingUrl) {
   489→    console.log(`Recording available at: ${RecordingUrl}`);
   490→
   491→    // Transcribe recording asynchronously (don't block webhook response)
   492→    transcribeRecording(CallSid, RecordingUrl, parseInt(RecordingDuration) || 0).catch(err => {
   493→      console.error('[Recording] Transcription error:', err.message);
   494→    });
   495→  }
   496→
   497→  res.sendStatus(200);
   498→});
   499→
   500→/**
   501→ * Transcribe a Twilio recording using Deepgram
   502→ * Updates the communications record if it exists, otherwise creates copilot_session
   503→ */
   504→async function transcribeRecording(callSid, recordingUrl, duration) {
   505→  console.log(`[Recording] Starting transcription for call ${callSid}`);
   506→
   507→  // Get leadId from cache
   508→  let leadId = null;
   509→  const cached = callLeadCache.get(callSid);
   510→  if (cached) {
   511→    leadId = cached.leadId;
   512→    console.log(`[Recording] Found cached leadId ${leadId} for call ${callSid}`);
   513→    // Clean up cache
   514→    callLeadCache.delete(callSid);
   515→  } else {
   516→    console.log(`[Recording] No cached leadId for call ${callSid}`);
   517→  }
   518→
   519→  // Twilio recordings need auth - add .mp3 for the audio file
   520→  const audioUrl = `${recordingUrl}.mp3`;
   521→
   522→  try {
   523→    // First, fetch the audio from Twilio with Basic Auth
   524→    const twilioAuth = Buffer.from(`${process.env.TWILIO_ACCOUNT_SID}:${process.env.TWILIO_AUTH_TOKEN}`).toString('base64');
   525→
   526→    console.log(`[Recording] Fetching audio from Twilio: ${audioUrl}`);
   527→    const audioResponse = await fetch(audioUrl, {
   528→      headers: {
   529→        'Authorization': `Basic ${twilioAuth}`
   530→      }
   531→    });
   532→
   533→    if (!audioResponse.ok) {
   534→      console.error(`[Recording] Failed to fetch from Twilio: ${audioResponse.status}`);
   535→      return;
   536→    }
   537→
   538→    const audioBuffer = await audioResponse.arrayBuffer();
   539→    console.log(`[Recording] Fetched ${audioBuffer.byteLength} bytes from Twilio`);
   540→
   541→    // Send binary audio to Deepgram
   542→    // Note: Removed diarize=true as it can cause issues with phone audio quality
   543→    const response = await fetch('https://api.deepgram.com/v1/listen?model=nova-2&language=en-AU&smart_format=true&punctuate=true&utterances=true', {
   544→      method: 'POST',
   545→      headers: {
   546→        'Authorization': `Token ${process.env.DEEPGRAM_API_KEY}`,
   547→        'Content-Type': 'audio/mpeg',
   548→      },
   549→      body: Buffer.from(audioBuffer),
   550→    });
   551→
   552→    if (!response.ok) {
   553→      const errText = await response.text();
   554→      console.error('[Recording] Deepgram error:', response.status, errText);
   555→      return;
   556→    }
   557→
   558→    const result = await response.json();
   559→    const transcript = result.results?.channels?.[0]?.alternatives?.[0]?.transcript || '';
   560→    const confidence = result.results?.channels?.[0]?.alternatives?.[0]?.confidence || 0;
   561→
   562→    console.log(`[Recording] Transcribed ${transcript.length} chars for call ${callSid} (confidence: ${confidence})`);
   563→
   564→    if (transcript.length < 10) {
   565→      console.log(`[Recording] Transcript too short, not saving. Content: "${transcript}"`);
   566→      console.log(`[Recording] Deepgram response:`, JSON.stringify(result.metadata || {}));
   567→      return;
   568→    }
   569→
   570→    if (!supabase) {
   571→      console.log('[Recording] No database connection');
   572→      return;
   573→    }
   574→
   575→    // Try multiple methods to find the communication record
   576→    let comm = null;
   577→
   578→    // Method 1: Try metadata.call_sid (outbound calls logged via /calls/log)
   579→    const { data: comm1 } = await supabase
   580→      .from('communications')
   581→      .select('id, metadata, lead_id')
   582→      .eq('metadata->>call_sid', callSid)
   583→      .single();
   584→
   585→    if (comm1) {
   586→      comm = comm1;
   587→      console.log(`[Recording] Found communication by metadata.call_sid: ${comm.id}`);
   588→    }
   589→
   590→    // Method 2: Try external_id (voicemails and inbound calls)
   591→    if (!comm) {
   592→      const { data: comm2 } = await supabase
   593→        .from('communications')
   594→        .select('id, metadata, lead_id')
   595→        .eq('external_id', callSid)
   596→        .single();
   597→
   598→      if (comm2) {
   599→        comm = comm2;
   600→        console.log(`[Recording] Found communication by external_id: ${comm.id}`);
   601→      }
   602→    }
   603→
   604→    // Method 3: If we have leadId from cache, find recent call for that lead
   605→    if (!comm && leadId) {
   606→      const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
   607→      const { data: comm3 } = await supabase
   608→        .from('communications')
   609→        .select('id, metadata, lead_id')
   610→        .eq('lead_id', leadId)
   611→        .in('type', ['call_outbound', 'call_inbound'])
   612→        .gte('created_at', fiveMinutesAgo)
   613→        .order('created_at', { ascending: false })
   614→        .limit(1)
   615→        .single();
   616→
   617→      if (comm3) {
   618→        comm = comm3;
   619→        console.log(`[Recording] Found communication by recent call for lead: ${comm.id}`);
   620→      }
   621→    }
   622→
   623→    if (comm) {
   624→      // Update existing communication record with transcript and recording URL
   625→      const updatedMetadata = {
   626→        ...comm.metadata,
   627→        transcript: transcript,
   628→        recording_url: recordingUrl,
   629→        recording_duration: duration,
   630→        auto_transcribed: true,
   631→      };
   632→
   633→      const { error: updateError } = await supabase
   634→        .from('communications')
   635→        .update({ metadata: updatedMetadata })
   636→        .eq('id', comm.id);
   637→
   638→      if (updateError) {
   639→        console.error('[Recording] Failed to update communication:', updateError.message);
   640→      } else {
   641→        console.log(`[Recording] Transcript added to communication ${comm.id}`);
   642→
   643→        // ASYNC: Full intel extraction from call transcript (fire and forget)
   644→        const extractLeadId = leadId || comm.lead_id || comm.metadata?.lead_id;
   645→        if (extractLeadId && transcript && transcript.length > 50) {
   646→          extractFullCallIntel(extractLeadId, transcript, duration).catch(err => {
   647→            console.error('[Recording] Full intel extraction failed:', err.message);
   648→          });
   649→        }
   650→      }
   651→    } else if (leadId) {
   652→      // No matching communication yet - save to copilot_sessions as backup
   653→      console.log('[Recording] No matching communication found, saving to copilot_sessions');
   654→      const { error } = await supabase
   655→        .from('copilot_sessions')
   656→        .insert({
   657→          lead_id: leadId,
   658→          transcript: transcript,
   659→          duration_seconds: duration,
   660→          metadata: {
   661→            recording_url: recordingUrl,
   662→            call_sid: callSid,
   663→            auto_transcribed: true,
   664→            transcribed_at: new Date().toISOString()
   665→          }
   666→        });
   667→
   668→      if (error) {
   669→        console.error('[Recording] Failed to save transcript:', error.message);
   670→      } else {
   671→        console.log('[Recording] Transcript saved to copilot_sessions (backup)');
   672→
   673→        // Still extract intel even without communication match
   674→        extractFullCallIntel(leadId, transcript, duration).catch(err => {
   675→          console.error('[Recording] Intel extraction failed:', err.message);
   676→        });
   677→      }
   678→    } else {
   679→      // Save orphaned transcript for later attachment when call is logged
   680→      console.log(`[Recording] No match found - saving orphaned transcript for CallSid ${callSid}`);
   681→
   682→      const { error: orphanError } = await supabase
   683→        .from('copilot_sessions')
   684→        .insert({
   685→          lead_id: leadId || null,
   686→          transcript: transcript,
   687→          duration_seconds: duration,
   688→          metadata: {
   689→            recording_url: recordingUrl,
   690→            orphaned: true,
   691→            call_sid: callSid,
   692→            awaiting_communication: true,
   693→            transcribed_at: new Date().toISOString()
   694→          }
   695→        });
   696→
   697→      if (orphanError) {
   698→        console.error('[Recording] Failed to save orphaned transcript:', orphanError.message);
   699→      } else {
   700→        console.log(`[Recording] Orphaned transcript saved for CallSid ${callSid}`);
   701→      }
   702→    }
   703→  } catch (err) {
   704→    console.error('[Recording] Transcription failed:', err.message);
   705→  }
   706→}
   707→
   708→/**
   709→ * Extract full intel from call transcript including buying signals
   710→ * Runs the same AI analysis as copilot session save
   711→ */
   712→async function extractFullCallIntel(leadId, transcript, duration) {
   713→  console.log(`[Recording] Extracting full intel for lead ${leadId} (${transcript.length} chars)`);
   714→
   715→  try {
   716→    // Get lead data for context
   717→    const { data: lead, error: leadError } = await supabase
   718→      .from('leads')
   719→      .select('*')
   720→      .eq('id', leadId)
   721→      .single();
   722→
   723→    if (leadError || !lead) {
   724→      console.log(`[Recording] Could not fetch lead ${leadId}:`, leadError?.message);
   725→      return;
   726→    }
   727→
   728→    // Generate AI summary with buying signals
   729→    const summaryResult = await generatePostCallSummary(lead, transcript, duration || 0);
   730→
   731→    if (!summaryResult.success || !summaryResult.summary) {
   732→      console.log(`[Recording] AI summary generation failed:`, summaryResult.error);
   733→      return;
   734→    }
   735→
   736→    const aiSummary = summaryResult.summary;
   737→    console.log(`[Recording] AI summary generated for lead ${leadId}`);
   738→
   739→    // Build lead updates
   740→    const updates = {
   741→      updated_at: new Date().toISOString(),
   742→    };
   743→
   744→    // Apply score adjustment if recommended
   745→    if (aiSummary.score_adjustment) {
   746→      updates.score = Math.max(0, Math.min(100, (lead.score || 0) + aiSummary.score_adjustment));
   747→      console.log(`[Recording] Score adjusted: ${lead.score} → ${updates.score}`);
   748→    }
   749→
   750→    // Apply status recommendation if different
   751→    if (aiSummary.status_recommendation && aiSummary.status_recommendation !== lead.status) {
   752→      updates.status = aiSummary.status_recommendation;
   753→      console.log(`[Recording] Status updated: ${lead.status} → ${updates.status}`);
   754→    }
   755→
   756→    // Extract and save buying signals
   757→    if (aiSummary.buying_signals && aiSummary.buying_signals.length > 0) {
   758→      const existingSignals = lead.metadata?.buying_signals || [];
   759→      const newSignals = aiSummary.buying_signals.map(s => ({
   760→        type: s.signal_type || 'medium',
   761→        score: s.signal_type === 'high' ? 30 : s.signal_type === 'medium' ? 15 : 5,
   762→        source: 'call',
   763→        text: s.phrase,
   764→        timestamp_approx: s.timestamp_approx,
   765→        detected_at: new Date().toISOString(),
   766→      }));
   767→      const allSignals = [...existingSignals, ...newSignals].slice(-10);
   768→      updates.metadata = { ...lead.metadata, buying_signals: allSignals };
   769→      console.log(`[Recording] Added ${newSignals.length} buying signals`);
   770→    }
   771→
   772→    // Save updates to lead
   773→    const { error: updateError } = await supabase
   774→      .from('leads')
   775→      .update(updates)
   776→      .eq('id', leadId);
   777→
   778→    if (updateError) {
   779→      console.error(`[Recording] Failed to update lead:`, updateError.message);
   780→    } else {
   781→      console.log(`[Recording] Lead ${leadId} updated with call intel`);
   782→    }
   783→
   784→    // Also run the existing extractInboundIntel for additional processing
   785→    extractInboundIntel(leadId, transcript, 'inbound_call').catch(err => {
   786→      console.error('[Recording] extractInboundIntel failed:', err.message);
   787→    });
   788→
   789→  } catch (err) {
   790→    console.error(`[Recording] extractFullCallIntel error:`, err.message);
   791→  }
   792→}
   793→
   794→/**
   795→ * POST /api/voice/transcribe-recording
   796→ * Manually transcribe an existing Twilio recording
   797→ * Useful for retroactively transcribing recordings that failed
   798→ * PUBLIC - admin utility endpoint
   799→ */
   800→voiceWebhooks.post('/transcribe-recording', async (req, res) => {
   801→  const { recordingSid, leadId, callSid } = req.body;
   802→
   803→  if (!recordingSid) {
   804→    return res.status(400).json({ error: 'recordingSid is required' });
   805→  }
   806→
   807→  try {
   808→    // Construct the recording URL from the SID
   809→    const recordingUrl = `https://api.twilio.com/2010-04-01/Accounts/${process.env.TWILIO_ACCOUNT_SID}/Recordings/${recordingSid}`;
   810→
   811→    console.log(`[ManualTranscribe] Starting transcription for ${recordingSid}`);
   812→
   813→    // If leadId provided, cache it for the transcription function
   814→    if (callSid && leadId) {
   815→      callLeadCache.set(callSid, { leadId, timestamp: Date.now() });
   816→    }
   817→
   818→    // Run transcription
   819→    await transcribeRecording(callSid || recordingSid, recordingUrl, 0);
   820→
   821→    res.json({ success: true, message: 'Transcription started' });
   822→  } catch (err) {
   823→    console.error('[ManualTranscribe] Error:', err.message);
   824→    res.status(500).json({ error: err.message });
   825→  }
   826→});
   827→
   828→/**
   829→ * GET /api/voice/lookup-caller
   830→ * Look up lead info for an incoming caller
   831→ */
   832→router.get('/lookup-caller', async (req, res) => {
   833→  const { phone } = req.query;
   834→
   835→  if (!phone) {
   836→    return res.status(400).json({ error: 'Phone number required' });
   837→  }
   838→
   839→  if (!supabase) {
   840→    return res.json({ lead: null });
   841→  }
   842→
   843→  try {
   844→    const { normalizeAustralianPhone } = require('../utils/phone');
   845→    const normalizedPhone = normalizeAustralianPhone(phone);
   846→
   847→    if (!normalizedPhone) {
   848→      return res.json({ lead: null });
   849→    }
   850→
   851→    // Try multiple formats
   852→    const formats = [
   853→      normalizedPhone,
   854→      '0' + normalizedPhone.substring(3),
   855→      normalizedPhone.substring(3),
   856→    ];
   857→
   858→    for (const format of formats) {
   859→      const { data: lead } = await supabase
   860→        .from('leads')
   861→        .select('id, first_name, last_name, company, phone, score, status, last_contact_at, last_contact_type, metadata')
   862→        .eq('phone', format)
   863→        .maybeSingle();
   864→
   865→      if (lead) {
   866→        // Add high intent info to response
   867→        const inboundCallCount = (lead.metadata?.inbound_call_count || 0);
   868→        const isHighIntent = lead.metadata?.is_high_intent || inboundCallCount >= 3;
   869→
   870→        return res.json({
   871→          lead: {
   872→            ...lead,
   873→            inbound_call_count: inboundCallCount,
   874→            is_high_intent: isHighIntent,
   875→            // Don't expose full metadata to client
   876→            metadata: undefined
   877→          }
   878→        });
   879→      }
   880→    }
   881→
   882→    res.json({ lead: null });
   883→  } catch (err) {
   884→    console.error('[Voice Lookup] Error:', err);
   885→    res.json({ lead: null });
   886→  }
   887→});
   888→
   889→/**
   890→ * GET /api/voice/status
   891→ * Check Voice configuration status
   892→ */
   893→router.get('/status', (req, res) => {
   894→  res.json({
   895→    configured: isVoiceConfigured(),
   896→    hasAccountSid: !!process.env.TWILIO_ACCOUNT_SID,
   897→    hasApiKey: !!process.env.TWILIO_API_KEY_SID,
   898→    hasApiSecret: !!process.env.TWILIO_API_KEY_SECRET,
   899→    hasTwimlApp: !!process.env.TWILIO_TWIML_APP_SID,
   900→    hasPhoneNumber: !!process.env.TWILIO_PHONE_NUMBER
   901→  });
   902→});
   903→
   904→// Debug endpoint removed for security - exposed Twilio credentials
   905→
   906→/**
   907→ * POST /api/voice/backfill-transcripts
   908→ * Backfill transcripts from Twilio recordings
   909→ * Finds recordings without transcripts and processes them
   910→ * PUBLIC - uses X-API-Key auth check inline
   911→ */
   912→voiceWebhooks.post('/backfill-transcripts', async (req, res) => {
   913→  // Simple API key check for admin endpoints
   914→  const apiKey = req.headers['x-api-key'];
   915→  const expectedKey = process.env.CLAWDBOT_API_KEY;
   916→  if (!apiKey || apiKey !== expectedKey) {
   917→    return res.status(401).json({ error: 'Invalid API key' });
   918→  }
   919→
   920→  const { days = 30, dryRun = false } = req.body;
   921→  const twilio = require('twilio');
   922→
   923→  console.log(`[Backfill] Starting transcript backfill (days: ${days}, dryRun: ${dryRun})`);
   924→
   925→  try {
   926→    const twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
   927→
   928→    // Fetch recordings from Twilio
   929→    const dateFrom = new Date();
   930→    dateFrom.setDate(dateFrom.getDate() - days);
   931→
   932→    const recordings = await twilioClient.recordings.list({
   933→      dateCreatedAfter: dateFrom,
   934→      limit: 500
   935→    });
   936→
   937→    console.log(`[Backfill] Found ${recordings.length} recordings`);
   938→
   939→    // Get already-transcribed call SIDs
   940→    const { data: commsWithTranscripts } = await supabase
   941→      .from('communications')
   942→      .select('external_id, metadata')
   943→      .not('metadata->transcript', 'is', null);
   944→
   945→    const transcribedCallSids = new Set();
   946→    (commsWithTranscripts || []).forEach(c => {
   947→      if (c.external_id) transcribedCallSids.add(c.external_id);
   948→      if (c.metadata?.call_sid) transcribedCallSids.add(c.metadata.call_sid);
   949→    });
   950→
   951→    // Also check orphaned transcripts
   952→    const { data: orphaned } = await supabase
   953→      .from('copilot_sessions')
   954→      .select('metadata')
   955→      .eq('metadata->orphaned', true);
   956→
   957→    (orphaned || []).forEach(o => {
   958→      if (o.metadata?.call_sid) transcribedCallSids.add(o.metadata.call_sid);
   959→    });
   960→
   961→    // Filter to those needing transcription
   962→    const needsTranscription = recordings.filter(r =>
   963→      !transcribedCallSids.has(r.callSid) && r.duration >= 5
   964→    );
   965→
   966→    console.log(`[Backfill] ${needsTranscription.length} recordings need transcription`);
   967→
   968→    if (dryRun) {
   969→      return res.json({
   970→        success: true,
   971→        dryRun: true,
   972→        totalRecordings: recordings.length,
   973→        alreadyTranscribed: transcribedCallSids.size,
   974→        needsTranscription: needsTranscription.length,
   975→        recordings: needsTranscription.map(r => ({
   976→          callSid: r.callSid,
   977→          duration: r.duration,
   978→          dateCreated: r.dateCreated
   979→        }))
   980→      });
   981→    }
   982→
   983→    // Process in background
   984→    res.json({
   985→      success: true,
   986→      message: `Processing ${needsTranscription.length} recordings in background`,
   987→      totalRecordings: recordings.length,
   988→      alreadyTranscribed: transcribedCallSids.size,
   989→      needsTranscription: needsTranscription.length
   990→    });
   991→
   992→    // Process each recording (async, after response sent)
   993→    let success = 0, failed = 0, skipped = 0;
   994→
   995→    for (const recording of needsTranscription) {
   996→      try {
   997→        const recordingUrl = `https://api.twilio.com/2010-04-01/Accounts/${process.env.TWILIO_ACCOUNT_SID}/Recordings/${recording.sid}`;
   998→
   999→        // Use existing transcription function
  1000→        await transcribeRecording(recording.callSid, recordingUrl, recording.duration);
  1001→        success++;
  1002→        console.log(`[Backfill] ✅ Transcribed ${recording.callSid}`);
  1003→
  1004→        // Rate limit
  1005→        await new Promise(resolve => setTimeout(resolve, 500));
  1006→      } catch (err) {
  1007→        console.error(`[Backfill] ❌ Failed ${recording.callSid}:`, err.message);
  1008→        failed++;
  1009→      }
  1010→    }
  1011→
  1012→    console.log(`[Backfill] Complete: ${success} success, ${failed} failed, ${skipped} skipped`);
  1013→
  1014→  } catch (err) {
  1015→    console.error('[Backfill] Error:', err);
  1016→    if (!res.headersSent) {
  1017→      res.status(500).json({ error: err.message });
  1018→    }
  1019→  }
  1020→});
  1021→
  1022→module.exports = router;
  1023→module.exports.voiceWebhooks = voiceWebhooks;
  1024→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


1→import { useState, useEffect } from 'react';
     2→import { motion, AnimatePresence } from 'framer-motion';
     3→import {
     4→  Minus,
     5→  Copy,
     6→  Check,
     7→  ChevronRight,
     8→  X,
     9→  Mic,
    10→  MicOff,
    11→  Flame,
    12→  PhoneOff,
    13→  Building2,
    14→  Lightbulb,
    15→  MessageCircle,
    16→  AlertTriangle,
    17→  Loader2
    18→} from 'lucide-react';
    19→
    20→// Format duration as M:SS
    21→function formatDuration(seconds) {
    22→  const mins = Math.floor(seconds / 60);
    23→  const secs = seconds % 60;
    24→  return `${mins}:${secs.toString().padStart(2, '0')}`;
    25→}
    26→
    27→// Script stage display names
    28→const STAGE_NAMES = {
    29→  opener: 'Opener',
    30→  discovery: 'Discovery',
    31→  value_prop: 'Value Props',
    32→  objections: 'Objections',
    33→  close: 'Close'
    34→};
    35→
    36→const STAGE_ORDER = ['opener', 'discovery', 'value_prop', 'objections', 'close'];
    37→
    38→// CallHeader - Fixed top bar
    39→function CallHeader({ duration, leadName, isListening, onMinimize }) {
    40→  return (
    41→

    42→      {/* Left: Recording indicator + timer */}
    43→

    44→        <div className={`w-2.5 h-2.5 rounded-full ${isListening ? 'bg-red-500 animate-pulse' : 'bg-slate-500'}`} />
    45→
    46→          {formatDuration(duration)}
    47→
    48→

    49→
    50→      {/* Center: Lead name */}
    51→
    52→        {leadName || 'Unknown'}
    53→
    54→
    55→      {/* Right: Minimize */}
    56→      <button onClick={onMinimize} className="p-2 -mr-2 text-slate-400 hover:text-white">
    57→        <Minus className="w-5 h-5" />
    58→      </button>
    59→

    60→  );
    61→}
    62→
    63→// ConversationStarters - Top of funnel rapport builders
    64→function ConversationStarters({ intel }) {
    65→  // Check multiple possible paths for conversation starters
    66→  const conversationStarters =
    67→    intel?.personal_intel?.conversation_starters ||
    68→    intel?.person?.conversation_starters ||
    69→    [];
    70→  const interestNews =
    71→    intel?.personal_intel?.interest_news ||
    72→    intel?.interest_news;
    73→
    74→  // Combine news talking points with conversation starters
    75→  const allStarters = [];
    76→
    77→  // Add news-based starters first (most current)
    78→  if (interestNews?.items?.length > 0) {
    79→    interestNews.items.slice(0, 2).forEach(item => {
    80→      allStarters.push({
    81→        type: 'news',
    82→        text: item.talking_point || item.headline,
    83→        source: item.topic || item.source,
    84→        emoji: item.topic?.toLowerCase().includes('liverpool') ? '⚽' :
    85→               item.topic?.toLowerCase().includes('f1') ? '🏎' :
    86→               item.topic?.toLowerCase().includes('gaa') ? '🏐' : '📰',
    87→      });
    88→    });
    89→  }
    90→
    91→  // Add general conversation starters
    92→  conversationStarters.slice(0, 3 - allStarters.length).forEach(starter => {
    93→    allStarters.push({
    94→      type: 'starter',
    95→      text: typeof starter === 'string' ? starter : starter.text,
    96→      source: typeof starter === 'string' ? null : starter.topic,
    97→      emoji: '💬',
    98→    });
    99→  });
   100→
   101→  if (allStarters.length === 0) return null;
   102→
   103→  return (
   104→

   105→

   106→        🎯
   107→        Ice Breakers
   108→

   109→

   110→        {allStarters.map((starter, i) => (
   111→

   112→            {starter.emoji}
   113→

   114→
"{starter.text}"

   115→              {starter.source && (
   116→
{starter.source}

   117→              )}
   118→

   119→

   120→        ))}
   121→

   122→

   123→  );
   124→}
   125→
   126→// ScriptStep - Shows ONE script step at a time
   127→function ScriptStep({ stage, script, openingLine }) {
   128→  const [copied, setCopied] = useState(false);
   129→  const stageIndex = STAGE_ORDER.indexOf(stage);
   130→
   131→  // Get content for current stage
   132→  const getStageContent = () => {
   133→    if (!script) {
   134→      return openingLine || "Hey, it's [your name] from RateRight. How's it going?";
   135→    }
   136→
   137→    switch (stage) {
   138→      case 'opener':
   139→        return script.opening || openingLine || "Hey, it's [your name] from RateRight. How's it going?";
   140→      case 'discovery':
   141→        return script.discovery_questions?.[0]?.question || "What's your biggest challenge finding workers right now?";
   142→      case 'value_prop':
   143→        return script.value_props?.[0]?.prop || "We connect you with pre-vetted workers at just 9.9% - no lock-in contracts.";
   144→      case 'objections':
   145→        return "Listen for concerns and address them directly.";
   146→      case 'close':
   147→        return script.closing_techniques?.[0]?.script || "Let me get you set up - it takes about 2 minutes. What's your email?";
   148→      default:
   149→        return script.opening || "Start the conversation...";
   150→    }
   151→  };
   152→
   153→  const content = getStageContent();
   154→
   155→  const handleCopy = async () => {
   156→    await navigator.clipboard.writeText(content);
   157→    setCopied(true);
   158→    setTimeout(() => setCopied(false), 500);
   159→  };
   160→
   161→  return (
   162→

   163→      {/* Stage indicator */}
   164→

   165→
   166→          {STAGE_NAMES[stage] || 'Script'}
   167→
   168→

   169→        {stageIndex + 1}/{STAGE_ORDER.length}
   170→

   171→
   172→      {/* Script content */}
   173→

   174→        "{content}"
   175→

   176→
   177→      {/* Copy button */}
   178→      <button
   179→        onClick={handleCopy}
   180→        className="absolute top-4 right-4 p-2 text-slate-400 hover:text-white transition-colors"
   181→      >
   182→        {copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" />}
   183→      </button>
   184→

   185→  );
   186→}
   187→
   188→// QuickIntel - Collapsed one-liner
   189→function QuickIntel({ company, person, onExpand }) {
   190→  if (!company && !person) {
   191→    return null;
   192→  }
   193→
   194→  const parts = [];
   195→  if (company?.name) parts.push(company.name);
   196→  if (company?.industry) parts.push(company.industry);
   197→  if (company?.size) parts.push(company.size);
   198→  else if (person?.job_title) parts.push(person.job_title);
   199→
   200→  if (parts.length === 0) return null;
   201→
   202→  return (
   203→    <button
   204→      onClick={onExpand}
   205→      className="bg-slate-800/30 border border-slate-700/50 rounded-xl px-4 py-2.5 flex items-center gap-3 w-full text-left"
   206→    >
   207→      <Building2 className="w-4 h-4 text-blue-400 flex-shrink-0" />
   208→
   209→        {parts.join(' • ')}
   210→
   211→      <ChevronRight className="w-4 h-4 text-slate-500 flex-shrink-0" />
   212→    </button>
   213→  );
   214→}
   215→
   216→// ContextCard - AI suggestion that slides in
   217→function ContextCard({ type, trigger, suggestion, onDismiss }) {
   218→  const isBuyingSignal = type === 'buying_signal';
   219→
   220→  return (
   221→    <motion.div
   222→      initial={{ y: 100, opacity: 0, scale: 0.95 }}
   223→      animate={{ y: 0, opacity: 1, scale: 1 }}
   224→      exit={{ y: 100, opacity: 0, scale: 0.95 }}
   225→      transition={{ type: "spring", damping: 25, stiffness: 300 }}
   226→      className={`w-full rounded-2xl overflow-hidden relative ${
   227→        isBuyingSignal
   228→          ? 'bg-gradient-to-br from-emerald-500/20 to-green-500/10 border border-emerald-500/50'
   229→          : 'bg-gradient-to-br from-amber-500/20 to-orange-500/10 border border-amber-500/50'
   230→      }`}
   231→    >
   232→      {/* Header */}
   233→

   236→        {isBuyingSignal ? '🔥' : '⚠'}
   237→
   240→          {isBuyingSignal ? 'BUYING SIGNAL' : (
   241→            suggestion?.objection_type === 'price' ? 'PRICE CONCERN' :
   242→            suggestion?.objection_type === 'timing' ? 'TIMING ISSUE' :
   243→            suggestion?.objection_type === 'trust' ? 'TRUST CONCERN' :
   244→            'OBJECTION'
   245→          )}
   246→
   247→

   248→
   249→      {/* What they said */}
   250→

   251→
"{trigger}"

   252→

   253→
   254→      {/* Your response */}
   255→

   256→

   257→          "{suggestion?.response || 'Handle this naturally...'}"
   258→

   259→
Adapt in your own voice

   260→

   261→
   262→      {/* Key point */}
   263→      {suggestion?.key_point && (
   264→

   265→

   270→            <Lightbulb className={`w-3 h-3 ${isBuyingSignal ? 'text-emerald-400' : 'text-amber-400'}`} />
   271→            <span className={`text-xs font-medium ${isBuyingSignal ? 'text-emerald-300' : 'text-amber-300'}`}>
   272→              {suggestion.key_point}
   273→
   274→

   275→

   276→      )}
   277→
   278→      {/* Follow up */}
   279→      {suggestion?.follow_up_question && (
   280→

   281→          <MessageCircle className={`w-4 h-4 mt-0.5 flex-shrink-0 ${
   282→            isBuyingSignal ? 'text-emerald-400' : 'text-amber-400'
   283→          }`} />
   284→

   285→            <span className={`text-xs font-bold ${isBuyingSignal ? 'text-emerald-400' : 'text-amber-400'}`}>
   286→              THEN ASK:
   287→
   288→            "{suggestion.follow_up_question}"
   289→

   290→

   291→      )}
   292→
   293→      {/* Dismiss button */}
   294→      <button
   295→        onClick={onDismiss}
   296→        className="absolute top-2 right-2 p-2 text-slate-400 hover:text-white"
   297→      >
   298→        <X className="w-4 h-4" />
   299→      </button>
   300→    </motion.div>
   301→  );
   302→}
   303→
   304→// CallActionBar - Fixed bottom actions
   305→function CallActionBar({ isMuted, onMute, onMarkHot, onEndCall, markedHot }) {
   306→  return (
   307→

   308→

   309→        {/* Mute */}
   310→        <button
   311→          onClick={onMute}
   312→          className={`w-14 h-14 rounded-full flex items-center justify-center transition-colors ${
   313→            isMuted ? 'bg-red-500 text-white' : 'bg-slate-700 text-slate-300'
   314→          }`}
   315→        >
   316→          {isMuted ? <MicOff className="w-6 h-6" /> : <Mic className="w-6 h-6" />}
   317→        </button>
   318→
   319→        {/* Mark Hot */}
   320→        <button
   321→          onClick={onMarkHot}
   322→          disabled={markedHot}
   323→          className={`w-14 h-14 rounded-full flex items-center justify-center transition-colors ${
   324→            markedHot
   325→              ? 'bg-amber-500 text-white'
   326→              : 'bg-amber-500/20 border border-amber-500/50 text-amber-400'
   327→          }`}
   328→        >
   329→          <Flame className="w-6 h-6" />
   330→        </button>
   331→
   332→        {/* End Call */}
   333→        <button
   334→          onClick={onEndCall}
   335→          className="px-8 py-4 rounded-full bg-red-500 text-white font-bold flex items-center gap-2 active:scale-95 transition-transform"
   336→        >
   337→          <PhoneOff className="w-5 h-5" />
   338→          End
   339→        </button>
   340→

   341→

   342→  );
   343→}
   344→
   345→// Intel Modal - Expanded intel view
   346→function IntelModal({ intel, leadName, onClose }) {
   347→  if (!intel) return null;
   348→
   349→  return (
   350→    <motion.div
   351→      initial={{ opacity: 0 }}
   352→      animate={{ opacity: 1 }}
   353→      exit={{ opacity: 0 }}
   354→      className="fixed inset-0 z-[60] bg-black/70 flex items-end"
   355→      onClick={onClose}
   356→    >
   357→      <motion.div
   358→        initial={{ y: '100%' }}
   359→        animate={{ y: 0 }}
   360→        exit={{ y: '100%' }}
   361→        transition={{ type: "spring", damping: 25, stiffness: 300 }}
   362→        className="bg-slate-800 rounded-t-3xl w-full max-h-[70vh] overflow-y-auto"
   363→        onClick={e => e.stopPropagation()}
   364→      >
   365→

   366→          {/* Handle */}
   367→

   368→
   369→
Intel: {leadName}

   370→
   371→          {/* Company */}
   372→          {intel.company && (
   373→

   374→

   375→                <Building2 className="w-4 h-4 text-blue-400" />
   376→                {intel.company.name}
   377→

   378→

   379→                {intel.company.industry && (
   380→
   381→                    {intel.company.industry}
   382→
   383→                )}
   384→                {intel.company.size && (
   385→
   386→                    {intel.company.size}
   387→
   388→                )}
   389→                {intel.company.location && (
   390→
   391→                    {intel.company.location}
   392→
   393→                )}
   394→

   395→              {intel.company.sales_angle && (
   396→

   397→                  SALES ANGLE:
   398→                  {intel.company.sales_angle}
   399→

   400→              )}
   401→

   402→          )}
   403→
   404→          {/* Person */}
   405→          {intel.person?.job_title && (
   406→

   407→

   408→                👤
   409→                {leadName}
   410→

   411→
{intel.person.job_title}

   412→              {intel.person.years_at_company && (
   413→
{intel.person.years_at_company} years at company

   414→              )}
   415→

   416→          )}
   417→
   418→          {/* Similar Wins */}
   419→          {intel.similar_wins?.length > 0 && (
   420→

   421→

   422→                🏆
   423→                Similar Wins
   424→

   425→

   426→                {intel.similar_wins.slice(0, 3).map((win, i) => (
   427→

   428→                    {win.name}
   429→                    {win.key_factor && (
   430→                       — "{win.key_factor}"
   431→                    )}
   432→

   433→                ))}
   434→

   435→

   436→          )}
   437→

   438→      </motion.div>
   439→    </motion.div>
   440→  );
   441→}
   442→
   443→// Main EliteCallUI Component
   444→export default function EliteCallUI({
   445→  lead,
   446→  duration,
   447→  isListening,
   448→  isMuted,
   449→  script,
   450→  scriptStage,
   451→  intel,
   452→  latestSuggestion,
   453→  openingLine,
   454→  error,
   455→  onMinimize,
   456→  onMute,
   457→  onMarkHot,
   458→  onEndCall,
   459→  onDismissSuggestion,
   460→}) {
   461→  const [showIntelModal, setShowIntelModal] = useState(false);
   462→  const [markedHot, setMarkedHot] = useState(false);
   463→
   464→  const handleMarkHot = () => {
   465→    setMarkedHot(true);
   466→    onMarkHot?.();
   467→  };
   468→
   469→  return (
   470→

   471→      {/* 1. HEADER */}
   472→      <CallHeader
   473→        duration={duration}
   474→        leadName={lead?.first_name}
   475→        isListening={isListening}
   476→        onMinimize={onMinimize}
   477→      />
   478→
   479→      {/* 2. MAIN CONTENT */}
   480→

   481→        {/* ICE BREAKERS - Top of funnel conversation starters */}
   482→        <ConversationStarters intel={intel} />
   483→
   484→        {/* Current Script Step */}
   485→


Now let me check for Twilio media setup and any other related files: