Read EVERY file in /home/ccuser/rateright-growth/src/routes/ and provide a detailed audit. For each file report:
1. What endpoints it exposes (method, path, purpose)
2. Auth checks — does it use any middleware for authentication? Are there unprotected routes?
3. Input validation — are request params/body validated? Any injection risks?
4. Error handling — are errors caught? Are internal details leaked?
5. Dead code — unused routes, commented-out code, stale functions
6. Security issues — API key exposure, SQL injection, any dangerous patterns
7. Twilio/VAPI webhook validation — if applicable

The files to read are:
- calls.js, aiAlerts.js, activation.js, dossier.js, dealIntelligence.js, battlegroundV2.js
- frontendTracking.js, achievements.js, battleground.js, bugReport.js, dev.js, coaching.js
- callScoring.js, analytics.js, ai.js, callAnalytics.js, prompts.js, sms.js
- objections.js, notion.js, voicemail.js, health.js, users.js, notifications.js
- scripts.js, intelligence.js, xp.js, manager.js, sync.js, jobs.js
- voice.js, voice.js.patch, callList.js, sequences.js, vapi.js, tags.js, playbook.js
- team.js, competitors.js, export.js, import.js, outboundWebhooks.js, enrichment.js
- leads.js, dashboard.js, webhooks.js

Be thorough and brutal. Report EVERYTHING. Include specific line numbers and code snippets for issues found.


I'll conduct a thorough audit of every file in the routes directory. Let me start by reading all the files systematically.


TodoWrite

Read and audit all route files in /home/ccuser/rateright-growth/src/routes/

Compile detailed findings for each file

Produce final audit report


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable


/home/ccuser/rateright-growth/src/routes/calls.js
/home/ccuser/rateright-growth/src/routes/aiAlerts.js
/home/ccuser/rateright-growth/src/routes/activation.js
/home/ccuser/rateright-growth/src/routes/dossier.js
/home/ccuser/rateright-growth/src/routes/dealIntelligence.js
/home/ccuser/rateright-growth/src/routes/battlegroundV2.js
/home/ccuser/rateright-growth/src/routes/frontendTracking.js
/home/ccuser/rateright-growth/src/routes/achievements.js
/home/ccuser/rateright-growth/src/routes/battleground.js
/home/ccuser/rateright-growth/src/routes/bugReport.js
/home/ccuser/rateright-growth/src/routes/dev.js
/home/ccuser/rateright-growth/src/routes/coaching.js
/home/ccuser/rateright-growth/src/routes/callScoring.js
/home/ccuser/rateright-growth/src/routes/analytics.js
/home/ccuser/rateright-growth/src/routes/ai.js
/home/ccuser/rateright-growth/src/routes/callAnalytics.js
/home/ccuser/rateright-growth/src/routes/prompts.js
/home/ccuser/rateright-growth/src/routes/sms.js
/home/ccuser/rateright-growth/src/routes/objections.js
/home/ccuser/rateright-growth/src/routes/notion.js
/home/ccuser/rateright-growth/src/routes/voicemail.js
/home/ccuser/rateright-growth/src/routes/health.js
/home/ccuser/rateright-growth/src/routes/users.js
/home/ccuser/rateright-growth/src/routes/notifications.js
/home/ccuser/rateright-growth/src/routes/scripts.js
/home/ccuser/rateright-growth/src/routes/intelligence.js
/home/ccuser/rateright-growth/src/routes/xp.js
/home/ccuser/rateright-growth/src/routes/manager.js
/home/ccuser/rateright-growth/src/routes/sync.js
/home/ccuser/rateright-growth/src/routes/jobs.js
/home/ccuser/rateright-growth/src/routes/callList.js
/home/ccuser/rateright-growth/src/routes/voice.js
/home/ccuser/rateright-growth/src/routes/sequences.js
/home/ccuser/rateright-growth/src/routes/vapi.js
/home/ccuser/rateright-growth/src/routes/tags.js
/home/ccuser/rateright-growth/src/routes/playbook.js
/home/ccuser/rateright-growth/src/routes/team.js
/home/ccuser/rateright-growth/src/routes/competitors.js
/home/ccuser/rateright-growth/src/routes/export.js
/home/ccuser/rateright-growth/src/routes/import.js
/home/ccuser/rateright-growth/src/routes/outboundWebhooks.js
/home/ccuser/rateright-growth/src/routes/enrichment.js
/home/ccuser/rateright-growth/src/routes/leads.js
/home/ccuser/rateright-growth/src/routes/dashboard.js
/home/ccuser/rateright-growth/src/routes/webhooks.js


Let me read all the files in parallel batches. Starting with the first batch.


     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→ * AI Alerts API - Proactive AI-generated alerts and insights
     3→ */
     4→
     5→const express = require('express');
     6→const router = express.Router();
     7→const { supabase } = require('../config/database');
     8→
     9→// Alert types
    10→const ALERT_TYPES = {
    11→  HOT_LEAD: 'hot_lead',
    12→  SLIPPING_LEAD: 'slipping_lead',
    13→  FOLLOW_UP_DUE: 'follow_up_due',
    14→  WINNING_PATTERN: 'winning_pattern',
    15→  STREAK_RISK: 'streak_risk',
    16→  MILESTONE: 'milestone',
    17→  COACHING_TIP: 'coaching_tip',
    18→  CONVERSION_OPPORTUNITY: 'conversion_opportunity',
    19→};
    20→
    21→// ============================================
    22→// Get pending alerts for user
    23→// ============================================
    24→
    25→router.get('/', async (req, res) => {
    26→  try {
    27→    const userId = req.user?.id || 'default-user';
    28→    const { limit = 20, includeRead = 'false' } = req.query;
    29→
    30→    let query = supabase
    31→      .from('ai_alerts')
    32→      .select(`
    33→        *,
    34→        leads (
    35→          id,
    36→          first_name,
    37→          last_name,
    38→          company,
    39→          phone,
    40→          type
    41→        )
    42→      `)
    43→      .eq('user_id', userId)
    44→      .order('created_at', { ascending: false })
    45→      .limit(parseInt(limit));
    46→
    47→    // Only show unread/undismissed by default
    48→    if (includeRead !== 'true') {
    49→      query = query.is('dismissed_at', null);
    50→    }
    51→
    52→    const { data, error } = await query;
    53→
    54→    if (error) throw error;
    55→
    56→    // Count unread
    57→    const unreadCount = (data || []).filter(a => !a.read_at).length;
    58→
    59→    res.json({
    60→      success: true,
    61→      alerts: data || [],
    62→      unreadCount,
    63→    });
    64→  } catch (error) {
    65→    console.error('Error fetching alerts:', error);
    66→    res.status(500).json({ success: false, error: error.message });
    67→  }
    68→});
    69→
    70→// ============================================
    71→// Create a new alert
    72→// ============================================
    73→
    74→router.post('/', async (req, res) => {
    75→  try {
    76→    const userId = req.user?.id || 'default-user';
    77→    const { alertType, leadId, title, message, data, priority } = req.body;
    78→
    79→    if (!alertType || !title) {
    80→      return res.status(400).json({
    81→        success: false,
    82→        error: 'alertType and title are required',
    83→      });
    84→    }
    85→
    86→    const { data: alert, error } = await supabase
    87→      .from('ai_alerts')
    88→      .insert({
    89→        user_id: userId,
    90→        alert_type: alertType,
    91→        lead_id: leadId || null,
    92→        title,
    93→        message,
    94→        data: data || {},
    95→        priority: priority || 'medium',
    96→      })
    97→      .select()
    98→      .single();
    99→
   100→    if (error) throw error;
   101→
   102→    res.json({
   103→      success: true,
   104→      alert,
   105→    });
   106→  } catch (error) {
   107→    console.error('Error creating alert:', error);
   108→    res.status(500).json({ success: false, error: error.message });
   109→  }
   110→});
   111→
   112→// ============================================
   113→// Mark alert as read
   114→// ============================================
   115→
   116→router.patch('/:id/read', async (req, res) => {
   117→  try {
   118→    const { id } = req.params;
   119→    const userId = req.user?.id || 'default-user';
   120→
   121→    const { error } = await supabase
   122→      .from('ai_alerts')
   123→      .update({ read_at: new Date().toISOString() })
   124→      .eq('id', id)
   125→      .eq('user_id', userId);
   126→
   127→    if (error) throw error;
   128→
   129→    res.json({ success: true });
   130→  } catch (error) {
   131→    console.error('Error marking alert read:', error);
   132→    res.status(500).json({ success: false, error: error.message });
   133→  }
   134→});
   135→
   136→// ============================================
   137→// Dismiss alert
   138→// ============================================
   139→
   140→router.patch('/:id/dismiss', async (req, res) => {
   141→  try {
   142→    const { id } = req.params;
   143→    const userId = req.user?.id || 'default-user';
   144→
   145→    const { error } = await supabase
   146→      .from('ai_alerts')
   147→      .update({ dismissed_at: new Date().toISOString() })
   148→      .eq('id', id)
   149→      .eq('user_id', userId);
   150→
   151→    if (error) throw error;
   152→
   153→    res.json({ success: true });
   154→  } catch (error) {
   155→    console.error('Error dismissing alert:', error);
   156→    res.status(500).json({ success: false, error: error.message });
   157→  }
   158→});
   159→
   160→// ============================================
   161→// Mark all as read
   162→// ============================================
   163→
   164→router.post('/read-all', async (req, res) => {
   165→  try {
   166→    const userId = req.user?.id || 'default-user';
   167→
   168→    const { error } = await supabase
   169→      .from('ai_alerts')
   170→      .update({ read_at: new Date().toISOString() })
   171→      .eq('user_id', userId)
   172→      .is('read_at', null);
   173→
   174→    if (error) throw error;
   175→
   176→    res.json({ success: true });
   177→  } catch (error) {
   178→    console.error('Error marking all read:', error);
   179→    res.status(500).json({ success: false, error: error.message });
   180→  }
   181→});
   182→
   183→// ============================================
   184→// Generate proactive insights
   185→// ============================================
   186→
   187→router.post('/generate', async (req, res) => {
   188→  try {
   189→    const userId = req.user?.id || 'default-user';
   190→    const alertsGenerated = [];
   191→
   192→    // Check for hot leads (high score, not contacted recently)
   193→    const { data: hotLeads } = await supabase
   194→      .from('leads')
   195→      .select('id, first_name, last_name, company, score, last_contact_at')
   196→      .gte('score', 70)
   197→      .or('last_contact_at.is.null,last_contact_at.lt.' + new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString())
   198→      .not('status', 'in', '("converted","closed_lost")')
   199→      .order('score', { ascending: false })
   200→      .limit(5);
   201→
   202→    for (const lead of (hotLeads || [])) {
   203→      // Check if we already have an alert for this lead
   204→      const { data: existing } = await supabase
   205→        .from('ai_alerts')
   206→        .select('id')
   207→        .eq('user_id', userId)
   208→        .eq('lead_id', lead.id)
   209→        .eq('alert_type', ALERT_TYPES.HOT_LEAD)
   210→        .is('dismissed_at', null)
   211→        .single();
   212→
   213→      if (!existing) {
   214→        const { data: alert } = await supabase
   215→          .from('ai_alerts')
   216→          .insert({
   217→            user_id: userId,
   218→            alert_type: ALERT_TYPES.HOT_LEAD,
   219→            lead_id: lead.id,
   220→            title: `Hot Lead: ${lead.first_name || 'Unknown'}`,
   221→            message: `${lead.first_name || 'This lead'} has a score of ${lead.score} - don't let them slip away!`,
   222→            priority: 'high',
   223→            data: { score: lead.score },
   224→          })
   225→          .select()
   226→          .single();
   227→
   228→        if (alert) alertsGenerated.push(alert);
   229→      }
   230→    }
   231→
   232→    // Check for slipping leads (was engaged, no contact in 7+ days)
   233→    const { data: slippingLeads } = await supabase
   234→      .from('leads')
   235→      .select('id, first_name, last_name, company, status, last_contact_at')
   236→      .in('status', ['engaged', 'qualified'])
   237→      .lt('last_contact_at', new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString())
   238→      .order('last_contact_at', { ascending: true })
   239→      .limit(5);
   240→
   241→    for (const lead of (slippingLeads || [])) {
   242→      const { data: existing } = await supabase
   243→        .from('ai_alerts')
   244→        .select('id')
   245→        .eq('user_id', userId)
   246→        .eq('lead_id', lead.id)
   247→        .eq('alert_type', ALERT_TYPES.SLIPPING_LEAD)
   248→        .is('dismissed_at', null)
   249→        .single();
   250→
   251→      if (!existing) {
   252→        const daysSince = Math.floor((Date.now() - new Date(lead.last_contact_at)) / (24 * 60 * 60 * 1000));
   253→        const { data: alert } = await supabase
   254→          .from('ai_alerts')
   255→          .insert({
   256→            user_id: userId,
   257→            alert_type: ALERT_TYPES.SLIPPING_LEAD,
   258→            lead_id: lead.id,
   259→            title: `${lead.first_name || 'Lead'} is slipping`,
   260→            message: `No contact in ${daysSince} days. They were ${lead.status} - reach out today!`,
   261→            priority: daysSince > 14 ? 'high' : 'medium',
   262→            data: { daysSince, status: lead.status },
   263→          })
   264→          .select()
   265→          .single();
   266→
   267→        if (alert) alertsGenerated.push(alert);
   268→      }
   269→    }
   270→
   271→    // Check for follow-ups due today
   272→    const today = new Date();
   273→    today.setHours(0, 0, 0, 0);
   274→    const tomorrow = new Date(today);
   275→    tomorrow.setDate(tomorrow.getDate() + 1);
   276→
   277→    const { data: followUps } = await supabase
   278→      .from('callbacks')
   279→      .select(`
   280→        id,
   281→        scheduled_at,
   282→        reason,
   283→        leads (
   284→          id,
   285→          first_name,
   286→          last_name,
   287→          company
   288→        )
   289→      `)
   290→      .eq('status', 'pending')
   291→      .gte('scheduled_at', today.toISOString())
   292→      .lt('scheduled_at', tomorrow.toISOString())
   293→      .limit(10);
   294→
   295→    for (const callback of (followUps || [])) {
   296→      if (!callback.leads) continue;
   297→
   298→      const { data: existing } = await supabase
   299→        .from('ai_alerts')
   300→        .select('id')
   301→        .eq('user_id', userId)
   302→        .eq('lead_id', callback.leads.id)
   303→        .eq('alert_type', ALERT_TYPES.FOLLOW_UP_DUE)
   304→        .gte('created_at', today.toISOString())
   305→        .single();
   306→
   307→      if (!existing) {
   308→        const { data: alert } = await supabase
   309→          .from('ai_alerts')
   310→          .insert({
   311→            user_id: userId,
   312→            alert_type: ALERT_TYPES.FOLLOW_UP_DUE,
   313→            lead_id: callback.leads.id,
   314→            title: `Follow-up: ${callback.leads.first_name || 'Lead'}`,
   315→            message: callback.reason || 'Scheduled follow-up is due today',
   316→            priority: 'medium',
   317→            data: { callbackId: callback.id, scheduledAt: callback.scheduled_at },
   318→          })
   319→          .select()
   320→          .single();
   321→
   322→        if (alert) alertsGenerated.push(alert);
   323→      }
   324→    }
   325→
   326→    // Check streak risk
   327→    const { data: xpData } = await supabase
   328→      .from('user_xp')
   329→      .select('current_streak, last_activity_date')
   330→      .eq('user_id', userId)
   331→      .single();
   332→
   333→    if (xpData && xpData.current_streak >= 3) {
   334→      const lastActivity = new Date(xpData.last_activity_date);
   335→      const now = new Date();
   336→      const hoursSinceActivity = (now - lastActivity) / (1000 * 60 * 60);
   337→
   338→      if (hoursSinceActivity >= 20 && hoursSinceActivity < 24) {
   339→        const { data: existing } = await supabase
   340→          .from('ai_alerts')
   341→          .select('id')
   342→          .eq('user_id', userId)
   343→          .eq('alert_type', ALERT_TYPES.STREAK_RISK)
   344→          .gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString())
   345→          .single();
   346→
   347→        if (!existing) {
   348→          const { data: alert } = await supabase
   349→            .from('ai_alerts')
   350→            .insert({
   351→              user_id: userId,
   352→              alert_type: ALERT_TYPES.STREAK_RISK,
   353→              title: `Protect your ${xpData.current_streak}-day streak!`,
   354→              message: `You haven't logged activity today. Make a call to keep your streak alive!`,
   355→              priority: 'high',
   356→              data: { streak: xpData.current_streak },
   357→            })
   358→            .select()
   359→            .single();
   360→
   361→          if (alert) alertsGenerated.push(alert);
   362→        }
   363→      }
   364→    }
   365→
   366→    res.json({
   367→      success: true,
   368→      alertsGenerated: alertsGenerated.length,
   369→      alerts: alertsGenerated,
   370→    });
   371→  } catch (error) {
   372→    console.error('Error generating insights:', error);
   373→    res.status(500).json({ success: false, error: error.message });
   374→  }
   375→});
   376→
   377→// Export alert types for use elsewhere
   378→router.ALERT_TYPES = ALERT_TYPES;
   379→
   380→module.exports = router;
   381→

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