     1→/**
     2→ * Activation Command Center API Routes
     3→ * Endpoints for funnel visualization, stuck alerts, and bulk actions
     4→ */
     5→
     6→const express = require('express');
     7→const router = express.Router();
     8→const { supabase } = require('../config/database');
     9→const logger = require('../utils/logger');
    10→const {
    11→  WORKER_STAGES,
    12→  CONTRACTOR_STAGES,
    13→  getStageLabel,
    14→  calculateDaysInStage,
    15→  isStuck,
    16→  diagnoseStuckReason,
    17→} = require('../services/stageCalculator');
    18→const {
    19→  analyzeBlockers,
    20→  generateNudge,
    21→  generateBulkNudges,
    22→  suggestAction,
    23→  analyzeStageDropoff,
    24→} = require('../services/activationAI');
    25→const stageDiagnosis = require('../services/stageDiagnosis');
    26→
    27→// Configuration
    28→const AVG_REVENUE_WORKER = parseInt(process.env.ACTIVATION_AVG_REVENUE_WORKER || '500', 10);
    29→const AVG_REVENUE_CONTRACTOR = parseInt(process.env.ACTIVATION_AVG_REVENUE_CONTRACTOR || '3000', 10);
    30→const STUCK_THRESHOLD_DAYS = parseInt(process.env.ACTIVATION_STUCK_THRESHOLD_DAYS || '7', 10);
    31→
    32→/**
    33→ * GET /api/activation/leads-at-stage
    34→ * Returns individual leads at a specific stage
    35→ */
    36→router.get('/leads-at-stage', async (req, res) => {
    37→  try {
    38→    if (!supabase) {
    39→      return res.status(503).json({ error: 'Database not configured' });
    40→    }
    41→
    42→    const { stage, type, limit = 100 } = req.query;
    43→
    44→    if (!stage) {
    45→      return res.status(400).json({ error: 'Stage is required' });
    46→    }
    47→
    48→    let query = supabase
    49→      .from('leads')
    50→      .select('id, first_name, last_name, email, phone, company, platform_stage, platform_data, platform_health_score, metadata, created_at, stage_changed_at')
    51→      .is('deleted_at', null)
    52→      .eq('platform_stage', stage)
    53→      .order('stage_changed_at', { ascending: true })
    54→      .limit(parseInt(limit, 10));
    55→
    56→    if (type && type !== 'all') {
    57→      query = query.eq('metadata->>lead_type', type);
    58→    }
    59→
    60→    const { data: leads, error } = await query;
    61→
    62→    if (error) {
    63→      throw error;
    64→    }
    65→
    66→    const now = new Date();
    67→    const leadsWithDetails = (leads || []).map(lead => {
    68→      const platformData = lead.platform_data || {};
    69→      const daysInStage = calculateDaysInStage(lead.stage_changed_at, lead.created_at);
    70→
    71→      // Calculate days since login
    72→      let daysSinceLogin = null;
    73→      if (platformData.last_login) {
    74→        const lastLogin = new Date(platformData.last_login);
    75→        daysSinceLogin = Math.floor((now - lastLogin) / (1000 * 60 * 60 * 24));
    76→      }
    77→
    78→      const diagnosis = isStuck(daysInStage, STUCK_THRESHOLD_DAYS)
    79→        ? diagnoseStuckReason(platformData, daysInStage)
    80→        : null;
    81→
    82→      return {
    83→        id: lead.id,
    84→        name: [lead.first_name, lead.last_name].filter(Boolean).join(' ') || 'Unknown',
    85→        firstName: lead.first_name,
    86→        lastName: lead.last_name,
    87→        email: lead.email,
    88→        phone: lead.phone,
    89→        company: lead.company,
    90→        type: lead.metadata?.lead_type || 'unknown',
    91→        stage: lead.platform_stage,
    92→        stageLabel: getStageLabel(lead.platform_stage),
    93→        daysInStage,
    94→        lastLogin: platformData.last_login,
    95→        daysSinceLogin,
    96→        healthScore: lead.platform_health_score,
    97→        diagnosis,
    98→        isStuck: isStuck(daysInStage, STUCK_THRESHOLD_DAYS),
    99→      };
   100→    });
   101→
   102→    res.json({
   103→      leads: leadsWithDetails,
   104→      stage,
   105→      stageLabel: getStageLabel(stage),
   106→      total: leadsWithDetails.length,
   107→      stuckCount: leadsWithDetails.filter(l => l.isStuck).length,
   108→    });
   109→  } catch (error) {
   110→    logger.error('Failed to get leads at stage', { error: error.message });
   111→    res.status(500).json({ error: 'Internal server error' });
   112→  }
   113→});
   114→
   115→/**
   116→ * GET /api/activation/funnel
   117→ * Returns stage counts for workers and contractors with conversion percentages
   118→ */
   119→router.get('/funnel', async (req, res) => {
   120→  try {
   121→    if (!supabase) {
   122→      return res.status(503).json({ error: 'Database not configured' });
   123→    }
   124→
   125→    // Get all leads with platform data
   126→    const { data: leads, error } = await supabase
   127→      .from('leads')
   128→      .select('id, platform_stage, platform_data, metadata, created_at, stage_changed_at')
   129→      .is('deleted_at', null)
   130→      .not('platform_stage', 'is', null);
   131→
   132→    if (error) {
   133→      throw error;
   134→    }
   135→
   136→    // Separate by lead type
   137→    const workers = leads.filter(l => l.metadata?.lead_type === 'worker');
   138→    const contractors = leads.filter(l => l.metadata?.lead_type === 'contractor');
   139→
   140→    // Calculate funnel for workers
   141→    const workerFunnel = calculateFunnel(workers, WORKER_STAGES);
   142→
   143→    // Calculate funnel for contractors
   144→    const contractorFunnel = calculateFunnel(contractors, CONTRACTOR_STAGES);
   145→
   146→    res.json({
   147→      worker: workerFunnel,
   148→      contractor: contractorFunnel,
   149→      totals: {
   150→        workers: workers.length,
   151→        contractors: contractors.length,
   152→        total: leads.length,
   153→      },
   154→    });
   155→  } catch (error) {
   156→    logger.error('Failed to get activation funnel', { error: error.message });
   157→    res.status(500).json({ error: 'Internal server error' });
   158→  }
   159→});
   160→
   161→/**
   162→ * GET /api/activation/stuck
   163→ * Returns users stuck at a stage for 7+ days
   164→ */
   165→router.get('/stuck', async (req, res) => {
   166→  try {
   167→    if (!supabase) {
   168→      return res.status(503).json({ error: 'Database not configured' });
   169→    }
   170→
   171→    const { stage, type } = req.query;
   172→
   173→    // Get leads with platform data
   174→    let query = supabase
   175→      .from('leads')
   176→      .select('id, first_name, last_name, email, phone, company, platform_stage, platform_data, platform_health_score, metadata, created_at, stage_changed_at')
   177→      .is('deleted_at', null)
   178→      .not('platform_stage', 'is', null);
   179→
   180→    // Filter by stage if specified
   181→    if (stage) {
   182→      query = query.eq('platform_stage', stage);
   183→    }
   184→
   185→    // Filter by type if specified
   186→    if (type) {
   187→      query = query.eq('metadata->>lead_type', type);
   188→    }
   189→
   190→    const { data: leads, error } = await query;
   191→
   192→    if (error) {
   193→      throw error;
   194→    }
   195→
   196→    // Find stuck leads (7+ days in current stage without reaching final stage)
   197→    const now = new Date();
   198→    const stuckLeads = [];
   199→    const stageDistribution = {};
   200→
   201→    for (const lead of leads) {
   202→      const leadType = lead.metadata?.lead_type || 'unknown';
   203→      const finalStages = leadType === 'worker' ? ['proven'] : ['active'];
   204→
   205→      // Skip if already at final stage
   206→      if (finalStages.includes(lead.platform_stage)) {
   207→        continue;
   208→      }
   209→
   210→      const daysInStage = calculateDaysInStage(lead.stage_changed_at, lead.created_at);
   211→
   212→      if (isStuck(daysInStage, STUCK_THRESHOLD_DAYS)) {
   213→        const platformData = lead.platform_data || {};
   214→        const diagnosis = diagnoseStuckReason(platformData, daysInStage);
   215→
   216→        // Calculate days since login
   217→        let daysSinceLogin = null;
   218→        if (platformData.last_login) {
   219→          const lastLogin = new Date(platformData.last_login);
   220→          daysSinceLogin = Math.floor((now - lastLogin) / (1000 * 60 * 60 * 24));
   221→        }
   222→
   223→        stuckLeads.push({
   224→          id: lead.id,
   225→          name: [lead.first_name, lead.last_name].filter(Boolean).join(' ') || 'Unknown',
   226→          email: lead.email,
   227→          phone: lead.phone,
   228→          company: lead.company,
   229→          type: leadType,
   230→          stage: lead.platform_stage,
   231→          stageLabel: getStageLabel(lead.platform_stage),
   232→          daysStuck: daysInStage,
   233→          lastLogin: platformData.last_login,
   234→          daysSinceLogin,
   235→          healthScore: lead.platform_health_score,
   236→          diagnosis,
   237→          suggestedAction: diagnosis === 'forgot' ? 'sms' : 'call',
   238→        });
   239→
   240→        // Track distribution
   241→        const key = `${leadType}:${lead.platform_stage}`;
   242→        stageDistribution[key] = (stageDistribution[key] || 0) + 1;
   243→      }
   244→    }
   245→
   246→    // Sort by days stuck (most stuck first)
   247→    stuckLeads.sort((a, b) => b.daysStuck - a.daysStuck);
   248→
   249→    // Calculate revenue at risk
   250→    let revenueAtRisk = 0;
   251→    for (const lead of stuckLeads) {
   252→      revenueAtRisk += lead.type === 'contractor' ? AVG_REVENUE_CONTRACTOR : AVG_REVENUE_WORKER;
   253→    }
   254→
   255→    // Find top blocker
   256→    let topBlocker = null;
   257→    let maxCount = 0;
   258→    for (const [key, count] of Object.entries(stageDistribution)) {
   259→      if (count > maxCount) {
   260→        maxCount = count;
   261→        topBlocker = key.split(':')[1];
   262→      }
   263→    }
   264→
   265→    res.json({
   266→      stuck: stuckLeads,
   267→      summary: {
   268→        totalStuck: stuckLeads.length,
   269→        revenueAtRisk,
   270→        topBlocker,
   271→        stuckThresholdDays: STUCK_THRESHOLD_DAYS,
   272→        distribution: stageDistribution,
   273→      },
   274→    });
   275→  } catch (error) {
   276→    logger.error('Failed to get stuck leads', { error: error.message });
   277→    res.status(500).json({ error: 'Internal server error' });
   278→  }
   279→});
   280→
   281→/**
   282→ * GET /api/activation/revenue-at-risk
   283→ * Calculate blocked revenue by stage
   284→ */
   285→router.get('/revenue-at-risk', async (req, res) => {
   286→  try {
   287→    if (!supabase) {
   288→      return res.status(503).json({ error: 'Database not configured' });
   289→    }
   290→
   291→    // Get all leads with platform data
   292→    const { data: leads, error } = await supabase
   293→      .from('leads')
   294→      .select('id, platform_stage, platform_data, metadata, created_at, stage_changed_at')
   295→      .is('deleted_at', null)
   296→      .not('platform_stage', 'is', null);
   297→
   298→    if (error) {
   299→      throw error;
   300→    }
   301→
   302→    const now = new Date();
   303→    const byStage = {};
   304→
   305→    for (const lead of leads) {
   306→      const leadType = lead.metadata?.lead_type || 'unknown';
   307→      const finalStages = leadType === 'worker' ? ['proven'] : ['active'];
   308→
   309→      // Skip if already at final stage
   310→      if (finalStages.includes(lead.platform_stage)) {
   311→        continue;
   312→      }
   313→
   314→      const daysInStage = calculateDaysInStage(lead.stage_changed_at, lead.created_at);
   315→
   316→      if (isStuck(daysInStage, STUCK_THRESHOLD_DAYS)) {
   317→        const avgRevenue = leadType === 'contractor' ? AVG_REVENUE_CONTRACTOR : AVG_REVENUE_WORKER;
   318→        const key = lead.platform_stage;
   319→
   320→        if (!byStage[key]) {
   321→          byStage[key] = {
   322→            stage: key,
   323→            stageLabel: getStageLabel(key),
   324→            count: 0,
   325→            workers: 0,
   326→            contractors: 0,
   327→            revenue: 0,
   328→          };
   329→        }
   330→
   331→        byStage[key].count++;
   332→        byStage[key].revenue += avgRevenue;
   333→        if (leadType === 'worker') byStage[key].workers++;
   334→        if (leadType === 'contractor') byStage[key].contractors++;
   335→      }
   336→    }
   337→
   338→    // Sort by revenue descending
   339→    const stages = Object.values(byStage).sort((a, b) => b.revenue - a.revenue);
   340→
   341→    // Calculate totals
   342→    const totalRevenue = stages.reduce((sum, s) => sum + s.revenue, 0);
   343→    const totalStuck = stages.reduce((sum, s) => sum + s.count, 0);
   344→
   345→    res.json({
   346→      byStage: stages,
   347→      totals: {
   348→        revenue: totalRevenue,
   349→        stuck: totalStuck,
   350→      },
   351→      config: {
   352→        avgRevenueWorker: AVG_REVENUE_WORKER,
   353→        avgRevenueContractor: AVG_REVENUE_CONTRACTOR,
   354→        stuckThresholdDays: STUCK_THRESHOLD_DAYS,
   355→      },
   356→    });
   357→  } catch (error) {
   358→    logger.error('Failed to calculate revenue at risk', { error: error.message });
   359→    res.status(500).json({ error: 'Internal server error' });
   360→  }
   361→});
   362→
   363→/**
   364→ * GET /api/activation/metrics
   365→ * Get historical activation metrics
   366→ */
   367→router.get('/metrics', async (req, res) => {
   368→  try {
   369→    if (!supabase) {
   370→      return res.status(503).json({ error: 'Database not configured' });
   371→    }
   372→
   373→    const { days = 30 } = req.query;
   374→    const sinceDate = new Date();
   375→    sinceDate.setDate(sinceDate.getDate() - parseInt(days, 10));
   376→
   377→    const { data: metrics, error } = await supabase
   378→      .from('activation_metrics')
   379→      .select('*')
   380→      .gte('date', sinceDate.toISOString().split('T')[0])
   381→      .order('date', { ascending: false });
   382→
   383→    if (error) {
   384→      // Table might not exist yet
   385→      if (error.code === '42P01') {
   386→        return res.json({ metrics: [], message: 'Metrics table not yet created' });
   387→      }
   388→      throw error;
   389→    }
   390→
   391→    res.json({ metrics: metrics || [] });
   392→  } catch (error) {
   393→    logger.error('Failed to get activation metrics', { error: error.message });
   394→    res.status(500).json({ error: 'Internal server error' });
   395→  }
   396→});
   397→
   398→/**
   399→ * GET /api/activation/stage-transitions
   400→ * Get recent stage transitions for analysis
   401→ */
   402→router.get('/stage-transitions', async (req, res) => {
   403→  try {
   404→    if (!supabase) {
   405→      return res.status(503).json({ error: 'Database not configured' });
   406→    }
   407→
   408→    const { limit = 50, leadId } = req.query;
   409→
   410→    let query = supabase
   411→      .from('stage_transitions')
   412→      .select(`
   413→        id,
   414→        lead_id,
   415→        from_stage,
   416→        to_stage,
   417→        triggered_by,
   418→        metadata,
   419→        created_at,
   420→        leads!inner(first_name, last_name, email, company, metadata)
   421→      `)
   422→      .order('created_at', { ascending: false })
   423→      .limit(parseInt(limit, 10));
   424→
   425→    if (leadId) {
   426→      query = query.eq('lead_id', leadId);
   427→    }
   428→
   429→    const { data: transitions, error } = await query;
   430→
   431→    if (error) {
   432→      // Table might not exist yet
   433→      if (error.code === '42P01') {
   434→        return res.json({ transitions: [], message: 'Transitions table not yet created' });
   435→      }
   436→      throw error;
   437→    }
   438→
   439→    res.json({
   440→      transitions: (transitions || []).map(t => ({
   441→        id: t.id,
   442→        leadId: t.lead_id,
   443→        leadName: [t.leads?.first_name, t.leads?.last_name].filter(Boolean).join(' ') || 'Unknown',
   444→        leadEmail: t.leads?.email,
   445→        leadType: t.leads?.metadata?.lead_type,
   446→        fromStage: t.from_stage,
   447→        fromStageLabel: getStageLabel(t.from_stage),
   448→        toStage: t.to_stage,
   449→        toStageLabel: getStageLabel(t.to_stage),
   450→        triggeredBy: t.triggered_by,
   451→        metadata: t.metadata,
   452→        createdAt: t.created_at,
   453→      })),
   454→    });
   455→  } catch (error) {
   456→    logger.error('Failed to get stage transitions', { error: error.message });
   457→    res.status(500).json({ error: 'Internal server error' });
   458→  }
   459→});
   460→
   461→/**
   462→ * POST /api/activation/bulk-nudge
   463→ * Generate AI-powered nudge messages for stuck users at a stage
   464→ */
   465→router.post('/bulk-nudge', async (req, res) => {
   466→  try {
   467→    const { stage, type, preview = true, useAI = true } = req.body;
   468→
   469→    if (!stage) {
   470→      return res.status(400).json({ error: 'Stage is required' });
   471→    }
   472→
   473→    // Get stuck leads at this stage directly from database
   474→    let query = supabase
   475→      .from('leads')
   476→      .select('id, first_name, last_name, email, phone, company, platform_stage, platform_data, platform_health_score, metadata, created_at, stage_changed_at')
   477→      .is('deleted_at', null)
   478→      .eq('platform_stage', stage);
   479→
   480→    if (type) {
   481→      query = query.eq('metadata->>lead_type', type);
   482→    }
   483→
   484→    const { data: leads, error } = await query;
   485→
   486→    if (error) {
   487→      throw error;
   488→    }
   489→
   490→    // Filter to stuck leads only (with valid phone numbers)
   491→    const stuckLeads = [];
   492→    for (const lead of leads || []) {
   493→      const daysInStage = calculateDaysInStage(lead.stage_changed_at, lead.created_at);
   494→      if (isStuck(daysInStage, STUCK_THRESHOLD_DAYS) && lead.phone) {
   495→        const platformData = lead.platform_data || {};
   496→        const diagnosis = diagnoseStuckReason(platformData, daysInStage);
   497→
   498→        stuckLeads.push({
   499→          id: lead.id,
   500→          name: [lead.first_name, lead.last_name].filter(Boolean).join(' ') || 'Unknown',
   501→          first_name: lead.first_name,
   502→          email: lead.email,
   503→          phone: lead.phone,
   504→          company: lead.company,
   505→          type: lead.metadata?.lead_type || 'unknown',
   506→          stage: lead.platform_stage,
   507→          daysStuck: daysInStage,
   508→          diagnosis,
   509→          healthScore: lead.platform_health_score,
   510→        });
   511→      }
   512→    }
   513→
   514→    if (stuckLeads.length === 0) {
   515→      return res.json({
   516→        success: true,
   517→        nudges: [],
   518→        message: 'No stuck leads found at this stage',
   519→      });
   520→    }
   521→
   522→    // Generate nudges - use AI if enabled
   523→    let nudges;
   524→    if (useAI) {
   525→      nudges = await generateBulkNudges(stuckLeads, stage);
   526→    } else {
   527→      nudges = stuckLeads.map(lead => ({
   528→        leadId: lead.id,
   529→        name: [lead.first_name, lead.last_name].filter(Boolean).join(' ') || 'Unknown',
   530→        email: lead.email,
   531→        phone: lead.phone,
   532→        stage: lead.stage,
   533→        daysStuck: lead.daysStuck,
   534→        diagnosis: lead.diagnosis,
   535→        nudge: { message: generateBasicNudge(lead) },
   536→        aiGenerated: false,
   537→      }));
   538→    }
   539→
   540→    if (preview) {
   541→      return res.json({
   542→        success: true,
   543→        preview: true,
   544→        nudges,
   545→        count: nudges.length,
   546→        aiEnabled: useAI,
   547→      });
   548→    }
   549→
   550→    // Actually send the SMS messages
   551→    const { sendSMS } = require('../services/twilio');
   552→    const results = {
   553→      sent: 0,
   554→      failed: 0,
   555→      details: [],
   556→    };
   557→
   558→    for (const nudge of nudges) {
   559→      if (!nudge.phone) {
   560→        results.failed++;
   561→        results.details.push({ leadId: nudge.leadId, success: false, error: 'No phone number' });
   562→        continue;
   563→      }
   564→
   565→      const message = nudge.nudge?.message || nudge.nudgeMessage;
   566→      if (!message) {
   567→        results.failed++;
   568→        results.details.push({ leadId: nudge.leadId, success: false, error: 'No message generated' });
   569→        continue;
   570→      }
   571→
   572→      try {
   573→        const smsResult = await sendSMS(nudge.phone, message);
   574→
   575→        if (smsResult.success) {
   576→          results.sent++;
   577→
   578→          // Log communication
   579→          await supabase
   580→            .from('communications')
   581→            .insert({
   582→              lead_id: nudge.leadId,
   583→              type: 'sms_outbound',
   584→              direction: 'outbound',
   585→              phone: nudge.phone,
   586→              content: message,
   587→              external_id: smsResult.messageSid,
   588→              metadata: {
   589→                activation_nudge: true,
   590→                stage: nudge.stage,
   591→                ai_generated: nudge.aiGenerated,
   592→              },
   593→            });
   594→
   595→          // Update lead
   596→          await supabase
   597→            .from('leads')
   598→            .update({
   599→              last_contact_at: new Date().toISOString(),
   600→              last_contact_type: 'sms_outbound',
   601→            })
   602→            .eq('id', nudge.leadId);
   603→
   604→          results.details.push({ leadId: nudge.leadId, success: true, messageSid: smsResult.messageSid });
   605→        } else {
   606→          results.failed++;
   607→          results.details.push({ leadId: nudge.leadId, success: false, error: smsResult.error });
   608→        }
   609→
   610→        // Small delay between messages
   611→        await new Promise(resolve => setTimeout(resolve, 100));
   612→      } catch (err) {
   613→        results.failed++;
   614→        results.details.push({ leadId: nudge.leadId, success: false, error: err.message });
   615→      }
   616→    }
   617→
   618→    logger.info('Bulk nudge completed', { stage, sent: results.sent, failed: results.failed });
   619→
   620→    res.json({
   621→      success: true,
   622→      preview: false,
   623→      sent: results.sent,
   624→      failed: results.failed,
   625→      total: nudges.length,
   626→      aiEnabled: useAI,
   627→      details: results.details,
   628→    });
   629→  } catch (error) {
   630→    logger.error('Failed to generate bulk nudges', { error: error.message });
   631→    res.status(500).json({ error: 'Internal server error' });
   632→  }
   633→});
   634→
   635→/**
   636→ * POST /api/activation/add-to-call-list
   637→ * Add stuck users at a stage to the call list by boosting their priority
   638→ */
   639→router.post('/add-to-call-list', async (req, res) => {
   640→  try {
   641→    const { stage, type } = req.body;
   642→
   643→    if (!stage) {
   644→      return res.status(400).json({ error: 'Stage is required' });
   645→    }
   646→
   647→    // Get stuck leads at this stage
   648→    let query = supabase
   649→      .from('leads')
   650→      .select('id, first_name, last_name, phone, platform_stage, metadata, created_at, stage_changed_at')
   651→      .is('deleted_at', null)
   652→      .eq('platform_stage', stage)
   653→      .not('phone', 'is', null);
   654→
   655→    if (type && type !== 'all') {
   656→      query = query.eq('metadata->>lead_type', type);
   657→    }
   658→
   659→    const { data: leads, error } = await query;
   660→
   661→    if (error) {
   662→      throw error;
   663→    }
   664→
   665→    // Filter to stuck leads only
   666→    const stuckLeadIds = [];
   667→    for (const lead of leads || []) {
   668→      const daysInStage = calculateDaysInStage(lead.stage_changed_at, lead.created_at);
   669→      if (isStuck(daysInStage, STUCK_THRESHOLD_DAYS)) {
   670→        stuckLeadIds.push(lead.id);
   671→      }
   672→    }
   673→
   674→    if (stuckLeadIds.length === 0) {
   675→      return res.json({
   676→        success: true,
   677→        added: 0,
   678→        message: 'No stuck leads found at this stage',
   679→      });
   680→    }
   681→
   682→    // Boost these leads in the call list by updating their call_list_score and reason
   683→    const boostReason = `Activation: Stuck at ${getStageLabel(stage)} for ${STUCK_THRESHOLD_DAYS}+ days`;
   684→    const { error: updateError } = await supabase
   685→      .from('leads')
   686→      .update({
   687→        call_list_score: 95, // High priority
   688→        call_list_reason: boostReason,
   689→        call_list_reason_type: 'activation_stuck',
   690→        call_list_updated_at: new Date().toISOString(),
   691→      })
   692→      .in('id', stuckLeadIds);
   693→
   694→    if (updateError) {
   695→      throw updateError;
   696→    }
   697→
   698→    logger.info('Added stuck leads to call list', { stage, count: stuckLeadIds.length });
   699→
   700→    res.json({
   701→      success: true,
   702→      added: stuckLeadIds.length,
   703→      stage,
   704→      reason: boostReason,
   705→    });
   706→  } catch (error) {
   707→    logger.error('Failed to add to call list', { error: error.message });
   708→    res.status(500).json({ error: 'Internal server error' });
   709→  }
   710→});
   711→
   712→/**
   713→ * GET /api/activation/analyze
   714→ * AI analysis of funnel blockers
   715→ */
   716→router.get('/analyze', async (req, res) => {
   717→  try {
   718→    if (!supabase) {
   719→      return res.status(503).json({ error: 'Database not configured' });
   720→    }
   721→
   722→    // Get funnel data first
   723→    const { data: leads, error } = await supabase
   724→      .from('leads')
   725→      .select('id, platform_stage, platform_data, metadata, created_at, stage_changed_at')
   726→      .is('deleted_at', null)
   727→      .not('platform_stage', 'is', null);
   728→
   729→    if (error) {
   730→      throw error;
   731→    }
   732→
   733→    // Build funnel data
   734→    const workers = leads.filter(l => l.metadata?.lead_type === 'worker');
   735→    const contractors = leads.filter(l => l.metadata?.lead_type === 'contractor');
   736→
   737→    const funnelData = {
   738→      worker: calculateFunnel(workers, WORKER_STAGES),
   739→      contractor: calculateFunnel(contractors, CONTRACTOR_STAGES),
   740→    };
   741→
   742→    // Get AI analysis
   743→    const analysis = await analyzeBlockers(funnelData);
   744→
   745→    res.json({
   746→      success: true,
   747→      funnel: funnelData,
   748→      analysis: analysis.success ? analysis.analysis : analysis.fallback,
   749→      aiPowered: analysis.success,
   750→    });
   751→  } catch (error) {
   752→    logger.error('Failed to analyze funnel', { error: error.message });
   753→    res.status(500).json({ error: 'Internal server error' });
   754→  }
   755→});
   756→
   757→/**
   758→ * POST /api/activation/nudge
   759→ * Generate AI nudge for a single lead
   760→ */
   761→router.post('/nudge', async (req, res) => {
   762→  try {
   763→    const { leadId } = req.body;
   764→
   765→    if (!leadId) {
   766→      return res.status(400).json({ error: 'leadId is required' });
   767→    }
   768→
   769→    // Get lead data
   770→    const { data: lead, error } = await supabase
   771→      .from('leads')
   772→      .select('id, first_name, last_name, email, phone, company, platform_stage, platform_data, platform_health_score, metadata, created_at, stage_changed_at')
   773→      .eq('id', leadId)
   774→      .single();
   775→
   776→    if (error || !lead) {
   777→      return res.status(404).json({ error: 'Lead not found' });
   778→    }
   779→
   780→    const daysInStage = calculateDaysInStage(lead.stage_changed_at, lead.created_at);
   781→    const diagnosis = diagnoseStuckReason(lead.platform_data || {}, daysInStage);
   782→
   783→    const leadData = {
   784→      id: lead.id,
   785→      name: [lead.first_name, lead.last_name].filter(Boolean).join(' ') || 'Unknown',
   786→      first_name: lead.first_name,
   787→      email: lead.email,
   788→      phone: lead.phone,
   789→      company: lead.company,
   790→      type: lead.metadata?.lead_type || 'unknown',
   791→      metadata: lead.metadata,
   792→    };
   793→
   794→    const result = await generateNudge(
   795→      leadData,
   796→      lead.platform_stage,
   797→      daysInStage,
   798→      diagnosis
   799→    );
   800→
   801→    res.json({
   802→      success: true,
   803→      leadId,
   804→      stage: lead.platform_stage,
   805→      daysInStage,
   806→      diagnosis,
   807→      nudge: result.success ? result.nudge : result.fallback,
   808→      aiPowered: result.success,
   809→    });
   810→  } catch (error) {
   811→    logger.error('Failed to generate nudge', { error: error.message });
   812→    res.status(500).json({ error: 'Internal server error' });
   813→  }
   814→});
   815→
   816→/**
   817→ * GET /api/activation/action-script
   818→ * Get AI-generated call script for a stage
   819→ */
   820→router.get('/action-script', async (req, res) => {
   821→  try {
   822→    const { stage, type = 'contractor' } = req.query;
   823→
   824→    if (!stage) {
   825→      return res.status(400).json({ error: 'Stage is required' });
   826→    }
   827→
   828→    const result = await suggestAction(stage, type);
   829→
   830→    res.json({
   831→      success: true,
   832→      stage,
   833→      type,
   834→      script: result.success ? result.script : result.fallback,
   835→      aiPowered: result.success,
   836→    });
   837→  } catch (error) {
   838→    logger.error('Failed to generate action script', { error: error.message });
   839→    res.status(500).json({ error: 'Internal server error' });
   840→  }
   841→});
   842→
   843→/**
   844→ * GET /api/activation/dropoff-analysis
   845→ * Analyze why users drop off at a specific stage
   846→ */
   847→router.get('/dropoff-analysis', async (req, res) => {
   848→  try {
   849→    const { stage, type = 'contractor' } = req.query;
   850→
   851→    if (!stage) {
   852→      return res.status(400).json({ error: 'Stage is required' });
   853→    }
   854→
   855→    // Get sample stuck leads at this stage
   856→    const { data: leads, error } = await supabase
   857→      .from('leads')
   858→      .select('id, platform_stage, platform_data, platform_health_score, metadata, created_at, stage_changed_at')
   859→      .is('deleted_at', null)
   860→      .eq('platform_stage', stage)
   861→      .eq('metadata->>lead_type', type)
   862→      .limit(20);
   863→
   864→    if (error) {
   865→      throw error;
   866→    }
   867→
   868→    const sampleLeads = (leads || []).map(lead => {
   869→      const daysInStage = calculateDaysInStage(lead.stage_changed_at, lead.created_at);
   870→      const platformData = lead.platform_data || {};
   871→      let daysSinceLogin = null;
   872→      if (platformData.last_login) {
   873→        const lastLogin = new Date(platformData.last_login);
   874→        daysSinceLogin = Math.floor((Date.now() - lastLogin) / (1000 * 60 * 60 * 24));
   875→      }
   876→
   877→      return {
   878→        daysStuck: daysInStage,
   879→        daysSinceLogin,
   880→        healthScore: lead.platform_health_score,
   881→        diagnosis: diagnoseStuckReason(platformData, daysInStage),
   882→      };
   883→    }).filter(l => l.daysStuck >= STUCK_THRESHOLD_DAYS);
   884→
   885→    const result = await analyzeStageDropoff(stage, type, sampleLeads);
   886→
   887→    res.json({
   888→      success: true,
   889→      stage,
   890→      type,
   891→      sampleSize: sampleLeads.length,
   892→      analysis: result.success ? result.analysis : null,
   893→      aiPowered: result.success,
   894→      error: result.error,
   895→    });
   896→  } catch (error) {
   897→    logger.error('Failed to analyze dropoff', { error: error.message });
   898→    res.status(500).json({ error: 'Internal server error' });
   899→  }
   900→});
   901→
   902→/**
   903→ * GET /api/activation/journey/:leadId
   904→ * Get full stage journey with activities for timeline visualization
   905→ */
   906→router.get('/journey/:leadId', async (req, res) => {
   907→  try {
   908→    if (!supabase) {
   909→      return res.status(503).json({ error: 'Database not configured' });
   910→    }
   911→
   912→    const { leadId } = req.params;
   913→
   914→    // Get lead data
   915→    const { data: lead, error: leadError } = await supabase
   916→      .from('leads')
   917→      .select('id, first_name, last_name, platform_stage, platform_data, metadata, created_at, stage_changed_at')
   918→      .eq('id', leadId)
   919→      .single();
   920→
   921→    if (leadError || !lead) {
   922→      return res.status(404).json({ error: 'Lead not found' });
   923→    }
   924→
   925→    const leadType = lead.metadata?.lead_type || 'contractor';
   926→    const stages = stageDiagnosis.getStagesForType(leadType);
   927→
   928→    // Get stage transitions
   929→    const { data: transitions, error: transError } = await supabase
   930→      .from('stage_transitions')
   931→      .select('id, from_stage, to_stage, triggered_by, metadata, created_at')
   932→      .eq('lead_id', leadId)
   933→      .order('created_at', { ascending: true });
   934→
   935→    // Get communications
   936→    const { data: comms, error: commsError } = await supabase
   937→      .from('communications')
   938→      .select('id, type, direction, content, duration, outcome, created_at')
   939→      .eq('lead_id', leadId)
   940→      .order('created_at', { ascending: true });
   941→
   942→    // Build stage history from transitions
   943→    const stageHistory = [];
   944→    const currentStageIndex = stages.indexOf(lead.platform_stage);
   945→
   946→    // Add stages up to current
   947→    for (let i = 0; i <= currentStageIndex && i < stages.length; i++) {
   948→      const stage = stages[i];
   949→      const transition = (transitions || []).find(t => t.to_stage === stage);
   950→
   951→      stageHistory.push({
   952→        stage,
   953→        label: stageDiagnosis.STAGE_LABELS[stage] || stage,
   954→        reachedAt: transition?.created_at || (i === 0 ? lead.created_at : null),
   955→        status: i < currentStageIndex ? 'completed' : i === currentStageIndex ? 'current' : 'pending',
   956→      });
   957→    }
   958→
   959→    // Add future stages
   960→    for (let i = currentStageIndex + 1; i < stages.length; i++) {
   961→      stageHistory.push({
   962→        stage: stages[i],
   963→        label: stageDiagnosis.STAGE_LABELS[stages[i]] || stages[i],
   964→        reachedAt: null,
   965→        status: 'pending',
   966→      });
   967→    }
   968→
   969→    // Build activities timeline
   970→    const activities = [];
   971→
   972→    // Add stage transitions as activities
   973→    for (const transition of transitions || []) {
   974→      activities.push({
   975→        id: transition.id,
   976→        type: 'stage_change',
   977→        date: transition.created_at,
   978→        description: `Moved to ${stageDiagnosis.STAGE_LABELS[transition.to_stage] || transition.to_stage}`,
   979→        fromStage: transition.from_stage,
   980→        toStage: transition.to_stage,
   981→        triggeredBy: transition.triggered_by,
   982→      });
   983→    }
   984→
   985→    // Add communications as activities
   986→    for (const comm of comms || []) {
   987→      let description = '';
   988→      if (comm.type === 'call_outbound' || comm.type === 'call') {
   989→        description = comm.duration
   990→          ? `You called (${Math.round(comm.duration / 60)} min)`
   991→          : 'You called';
   992→        if (comm.outcome) description += ` - ${comm.outcome}`;
   993→      } else if (comm.type === 'sms_outbound') {
   994→        description = 'You sent SMS';
   995→      } else if (comm.type === 'sms_inbound') {
   996→        description = 'They replied via SMS';
   997→      } else if (comm.type === 'email') {
   998→        description = comm.direction === 'outbound' ? 'You emailed' : 'They emailed';
   999→      } else {
  1000→        description = comm.type;
  1001→      }
  1002→
  1003→      activities.push({
  1004→        id: comm.id,
  1005→        type: comm.type,
  1006→        date: comm.created_at,
  1007→        description,
  1008→        direction: comm.direction,
  1009→        content: comm.content?.substring(0, 100),
  1010→      });
  1011→    }
  1012→
  1013→    // Add platform login events from platform_data
  1014→    const platformData = lead.platform_data || {};
  1015→    if (platformData.last_login) {
  1016→      activities.push({
  1017→        id: 'last-login',
  1018→        type: 'platform_login',
  1019→        date: platformData.last_login,
  1020→        description: 'Logged in to platform',
  1021→      });
  1022→    }
  1023→
  1024→    // Sort activities by date
  1025→    activities.sort((a, b) => new Date(a.date) - new Date(b.date));
  1026→
  1027→    // Calculate days in current stage
  1028→    const daysInStage = calculateDaysInStage(lead.stage_changed_at, lead.created_at);
  1029→
  1030→    // Detect activity gaps (7+ days of no activity)
  1031→    const gaps = [];
  1032→    for (let i = 1; i < activities.length; i++) {
  1033→      const prevDate = new Date(activities[i - 1].date);
  1034→      const currDate = new Date(activities[i].date);
  1035→      const daysBetween = Math.floor((currDate - prevDate) / (1000 * 60 * 60 * 24));
  1036→
  1037→      if (daysBetween >= 7) {
  1038→        gaps.push({
  1039→          startDate: activities[i - 1].date,
  1040→          endDate: activities[i].date,
  1041→          days: daysBetween,
  1042→        });
  1043→      }
  1044→    }
  1045→
  1046→    // Check for current gap (no activity in 7+ days)
  1047→    if (activities.length > 0) {
  1048→      const lastActivity = new Date(activities[activities.length - 1].date);
  1049→      const daysSinceLast = Math.floor((Date.now() - lastActivity) / (1000 * 60 * 60 * 24));
  1050→      if (daysSinceLast >= 7) {
  1051→        gaps.push({
  1052→          startDate: activities[activities.length - 1].date,
  1053→          endDate: new Date().toISOString(),
  1054→          days: daysSinceLast,
  1055→          isCurrent: true,
  1056→        });
  1057→      }
  1058→    }
  1059→
  1060→    res.json({
  1061→      leadId,
  1062→      leadType,
  1063→      currentStage: lead.platform_stage,
  1064→      currentStageLabel: stageDiagnosis.STAGE_LABELS[lead.platform_stage],
  1065→      daysInStage,
  1066→      stages: stageHistory,
  1067→      activities,
  1068→      gaps,
  1069→      createdAt: lead.created_at,
  1070→    });
  1071→  } catch (error) {
  1072→    logger.error('Failed to get journey', { error: error.message, leadId: req.params.leadId });
  1073→    res.status(500).json({ error: 'Internal server error' });
  1074→  }
  1075→});
  1076→
  1077→/**
  1078→ * GET /api/activation/diagnose/:leadId
  1079→ * AI diagnosis with pattern, blocker, script
  1080→ */
  1081→router.get('/diagnose/:leadId', async (req, res) => {
  1082→  try {
  1083→    if (!supabase) {
  1084→      return res.status(503).json({ error: 'Database not configured' });
  1085→    }
  1086→
  1087→    const { leadId } = req.params;
  1088→
  1089→    // Get lead data
  1090→    const { data: lead, error: leadError } = await supabase
  1091→      .from('leads')
  1092→      .select('id, first_name, last_name, email, phone, company, platform_stage, platform_data, metadata, created_at, stage_changed_at')
  1093→      .eq('id', leadId)
  1094→      .single();
  1095→
  1096→    if (leadError || !lead) {
  1097→      return res.status(404).json({ error: 'Lead not found' });
  1098→    }
  1099→
  1100→    // Get recent communications for context
  1101→    const { data: comms } = await supabase
  1102→      .from('communications')
  1103→      .select('id, type, direction, content, outcome, created_at')
  1104→      .eq('lead_id', leadId)
  1105→      .order('created_at', { ascending: false })
  1106→      .limit(10);
  1107→
  1108→    const daysInStage = calculateDaysInStage(lead.stage_changed_at, lead.created_at);
  1109→
  1110→    // Run diagnosis
  1111→    const diagnosis = await stageDiagnosis.diagnose(
  1112→      { ...lead, daysInStage },
  1113→      lead.platform_data || {},
  1114→      comms || []
  1115→    );
  1116→
  1117→    res.json({
  1118→      leadId,
  1119→      name: [lead.first_name, lead.last_name].filter(Boolean).join(' '),
  1120→      stage: lead.platform_stage,
  1121→      stageLabel: stageDiagnosis.STAGE_LABELS[lead.platform_stage],
  1122→      daysInStage,
  1123→      ...diagnosis,
  1124→    });
  1125→  } catch (error) {
  1126→    logger.error('Failed to diagnose lead', { error: error.message, leadId: req.params.leadId });
  1127→    res.status(500).json({ error: 'Internal server error' });
  1128→  }
  1129→});
  1130→
  1131→/**
  1132→ * GET /api/activation/similar-wins/:leadId
  1133→ * Get leads who progressed from same stage (success stories)
  1134→ */
  1135→router.get('/similar-wins/:leadId', async (req, res) => {
  1136→  try {
  1137→    if (!supabase) {
  1138→      return res.status(503).json({ error: 'Database not configured' });
  1139→    }
  1140→
  1141→    const { leadId } = req.params;
  1142→    const { limit = 5 } = req.query;
  1143→
  1144→    // Get current lead's stage
  1145→    const { data: lead, error: leadError } = await supabase
  1146→      .from('leads')
  1147→      .select('id, platform_stage, metadata')
  1148→      .eq('id', leadId)
  1149→      .single();
  1150→
  1151→    if (leadError || !lead) {
  1152→      return res.status(404).json({ error: 'Lead not found' });
  1153→    }
  1154→
  1155→    const currentStage = lead.platform_stage;
  1156→    const leadType = lead.metadata?.lead_type || 'contractor';
  1157→    const stages = stageDiagnosis.getStagesForType(leadType);
  1158→    const currentIndex = stages.indexOf(currentStage);
  1159→
  1160→    if (currentIndex === -1 || currentIndex === stages.length - 1) {
  1161→      // Already at final stage or unknown stage
  1162→      return res.json({
  1163→        wins: [],
  1164→        pattern: null,
  1165→        message: 'Lead is at final stage or stage not found',
  1166→      });
  1167→    }
  1168→
  1169→    const nextStage = stages[currentIndex + 1];
  1170→
  1171→    // Find leads who successfully moved from currentStage to nextStage
  1172→    const { data: transitions, error: transError } = await supabase
  1173→      .from('stage_transitions')
  1174→      .select(`
  1175→        id,
  1176→        lead_id,
  1177→        from_stage,
  1178→        to_stage,
  1179→        metadata,
  1180→        created_at,
  1181→        leads!inner(id, first_name, last_name, company, metadata, platform_data)
  1182→      `)
  1183→      .eq('from_stage', currentStage)
  1184→      .eq('to_stage', nextStage)
  1185→      .not('lead_id', 'eq', leadId)
  1186→      .order('created_at', { ascending: false })
  1187→      .limit(parseInt(limit, 10) * 2); // Get extra in case we need to filter
  1188→
  1189→    if (transError) {
  1190→      throw transError;
  1191→    }
  1192→
  1193→    // Process wins
  1194→    const wins = [];
  1195→    const actionCounts = { call: 0, sms: 0, other: 0 };
  1196→
  1197→    for (const trans of transitions || []) {
  1198→      if (wins.length >= parseInt(limit, 10)) break;
  1199→
  1200→      // Get communications that happened just before the transition
  1201→      const { data: priorComms } = await supabase
  1202→        .from('communications')
  1203→        .select('type, outcome, created_at')
  1204→        .eq('lead_id', trans.lead_id)
  1205→        .lte('created_at', trans.created_at)
  1206→        .order('created_at', { ascending: false })
  1207→        .limit(5);
  1208→
  1209→      // Determine what action triggered progress
  1210→      let progressedVia = 'unknown';
  1211→      let keyFactor = null;
  1212→      if (priorComms && priorComms.length > 0) {
  1213→        const lastComm = priorComms[0];
  1214→        if (lastComm.type?.includes('call')) {
  1215→          progressedVia = 'call';
  1216→          keyFactor = lastComm.outcome || 'Phone call';
  1217→          actionCounts.call++;
  1218→        } else if (lastComm.type?.includes('sms')) {
  1219→          progressedVia = 'sms';
  1220→          keyFactor = 'SMS follow-up';
  1221→          actionCounts.sms++;
  1222→        } else {
  1223→          actionCounts.other++;
  1224→        }
  1225→      }
  1226→
  1227→      // Calculate days they were stuck before progressing
  1228→      const transitionData = trans.metadata || {};
  1229→      const daysStuck = transitionData.days_in_previous_stage || null;
  1230→
  1231→      // Calculate revenue (placeholder - could be from actual job data)
  1232→      const platformData = trans.leads?.platform_data || {};
  1233→      const revenue = platformData.total_spend || platformData.total_earned || null;
  1234→
  1235→      wins.push({
  1236→        id: trans.id,
  1237→        leadId: trans.lead_id,
  1238→        name: [trans.leads?.first_name, trans.leads?.last_name].filter(Boolean).join(' ') || 'Unknown',
  1239→        company: trans.leads?.company,
  1240→        stuckStage: currentStage,
  1241→        stuckStageLabel: stageDiagnosis.STAGE_LABELS[currentStage],
  1242→        progressedTo: nextStage,
  1243→        progressedToLabel: stageDiagnosis.STAGE_LABELS[nextStage],
  1244→        daysStuck,
  1245→        progressedVia,
  1246→        keyFactor,
  1247→        revenue,
  1248→        progressedAt: trans.created_at,
  1249→      });
  1250→    }
  1251→
  1252→    // Calculate pattern insight
  1253→    let pattern = null;
  1254→    const totalActions = actionCounts.call + actionCounts.sms + actionCounts.other;
  1255→    if (totalActions > 0) {
  1256→      const callPct = Math.round((actionCounts.call / totalActions) * 100);
  1257→      const smsPct = Math.round((actionCounts.sms / totalActions) * 100);
  1258→
  1259→      if (callPct >= 60) {
  1260→        pattern = {
  1261→          insight: `${callPct}% progressed after a CALL (not SMS)`,
  1262→          recommendedAction: 'call',
  1263→        };
  1264→      } else if (smsPct >= 60) {
  1265→        pattern = {
  1266→          insight: `${smsPct}% progressed after an SMS`,
  1267→          recommendedAction: 'sms',
  1268→        };
  1269→      } else {
  1270→        pattern = {
  1271→          insight: 'Mixed success with calls and SMS',
  1272→          recommendedAction: 'call',
  1273→        };
  1274→      }
  1275→    }
  1276→
  1277→    res.json({
  1278→      leadId,
  1279→      currentStage,
  1280→      currentStageLabel: stageDiagnosis.STAGE_LABELS[currentStage],
  1281→      nextStage,
  1282→      nextStageLabel: stageDiagnosis.STAGE_LABELS[nextStage],
  1283→      wins,
  1284→      pattern,
  1285→      total: wins.length,
  1286→    });
  1287→  } catch (error) {
  1288→    logger.error('Failed to get similar wins', { error: error.message, leadId: req.params.leadId });
  1289→    res.status(500).json({ error: 'Internal server error' });
  1290→  }
  1291→});
  1292→
  1293→/**
  1294→ * Helper: Calculate funnel from leads
  1295→ */
  1296→function calculateFunnel(leads, stages) {
  1297→  const stageCounts = {};
  1298→
  1299→  // Initialize all stages
  1300→  for (const stage of stages) {
  1301→    stageCounts[stage] = 0;
  1302→  }
  1303→
  1304→  // Count leads at each stage
  1305→  for (const lead of leads) {
  1306→    if (lead.platform_stage && stageCounts.hasOwnProperty(lead.platform_stage)) {
  1307→      stageCounts[lead.platform_stage]++;
  1308→    }
  1309→  }
  1310→
  1311→  // Calculate percentages and find biggest dropoff
  1312→  const total = leads.length || 1;
  1313→  let previousCount = total;
  1314→  let biggestDropoff = null;
  1315→  let maxDropPct = 0;
  1316→
  1317→  const stagesData = stages.map((stage, index) => {
  1318→    const count = stageCounts[stage];
  1319→    const pct = Math.round((count / total) * 100);
  1320→
  1321→    // Calculate dropoff from previous stage
  1322→    if (index > 0 && previousCount > 0) {
  1323→      const dropPct = Math.round(((previousCount - count) / previousCount) * 100);
  1324→      if (dropPct > maxDropPct) {
  1325→        maxDropPct = dropPct;
  1326→        biggestDropoff = {
  1327→          from: stages[index - 1],
  1328→          fromLabel: getStageLabel(stages[index - 1]),
  1329→          to: stage,
  1330→          toLabel: getStageLabel(stage),
  1331→          dropPct,
  1332→        };
  1333→      }
  1334→    }
  1335→
  1336→    previousCount = count;
  1337→
  1338→    return {
  1339→      stage,
  1340→      label: getStageLabel(stage),
  1341→      count,
  1342→      pct,
  1343→    };
  1344→  });
  1345→
  1346→  return {
  1347→    stages: stagesData,
  1348→    biggestDropoff,
  1349→    total: leads.length,
  1350→  };
  1351→}
  1352→
  1353→/**
  1354→ * Helper: Generate basic nudge message (placeholder for AI)
  1355→ */
  1356→function generateBasicNudge(lead) {
  1357→  const firstName = lead.first_name || 'there';
  1358→  const stage = lead.stage;
  1359→
  1360→  const nudgeTemplates = {
  1361→    signed_up: `Hi ${firstName}! Saw you signed up recently. Need any help getting started?`,
  1362→    profile_complete: `Hey ${firstName}, your profile's looking good! Just a few more steps to get fully set up.`,
  1363→    job_ready: `Hi ${firstName}! You're all set to start. Ready to find your first opportunity?`,
  1364→    stripe_ready: `Hey ${firstName}, payments are all set up! Time to post your first job?`,
  1365→    job_posted: `Hi ${firstName}, your job is live! Let us know if you need help finding the right worker.`,
  1366→    applied: `Hey ${firstName}! Great work applying. We're here if you need anything.`,
  1367→    working: `Hi ${firstName}, hope the job's going well! Let us know if you need any support.`,
  1368→    hired: `Hey ${firstName}, congrats on the hire! Let us know how it's going.`,
  1369→  };
  1370→
  1371→  return nudgeTemplates[stage] || `Hi ${firstName}! Just checking in - how can we help you today?`;
  1372→}
  1373→
  1374→/**
  1375→ * GET /api/activation/nudge-templates
  1376→ * Get all nudge templates
  1377→ */
  1378→router.get('/nudge-templates', async (req, res) => {
  1379→  try {
  1380→    if (!supabase) {
  1381→      return res.status(503).json({ error: 'Database not configured' });
  1382→    }
  1383→
  1384→    const { stage, type } = req.query;
  1385→
  1386→    let query = supabase
  1387→      .from('nudge_templates')
  1388→      .select('*')
  1389→      .eq('active', true)
  1390→      .order('stage', { ascending: true });
  1391→
  1392→    if (stage) {
  1393→      query = query.eq('stage', stage);
  1394→    }
  1395→
  1396→    if (type) {
  1397→      query = query.or(`lead_type.eq.${type},lead_type.eq.all`);
  1398→    }
  1399→
  1400→    const { data: templates, error } = await query;
  1401→
  1402→    if (error) {
  1403→      // Table might not exist yet - return default templates
  1404→      if (error.code === '42P01') {
  1405→        return res.json({
  1406→          templates: getDefaultTemplates(),
  1407→          fromDefaults: true,
  1408→        });
  1409→      }
  1410→      throw error;
  1411→    }
  1412→
  1413→    // If no templates exist, return defaults
  1414→    if (!templates || templates.length === 0) {
  1415→      return res.json({
  1416→        templates: getDefaultTemplates(),
  1417→        fromDefaults: true,
  1418→      });
  1419→    }
  1420→
  1421→    res.json({ templates, fromDefaults: false });
  1422→  } catch (error) {
  1423→    logger.error('Failed to get nudge templates', { error: error.message });
  1424→    res.status(500).json({ error: 'Internal server error' });
  1425→  }
  1426→});
  1427→
  1428→/**
  1429→ * POST /api/activation/nudge-templates
  1430→ * Create a new nudge template
  1431→ */
  1432→router.post('/nudge-templates', async (req, res) => {
  1433→  try {
  1434→    if (!supabase) {
  1435→      return res.status(503).json({ error: 'Database not configured' });
  1436→    }
  1437→
  1438→    const { name, stage, lead_type, message, variables, active = true } = req.body;
  1439→
  1440→    if (!name || !stage || !message) {
  1441→      return res.status(400).json({ error: 'name, stage, and message are required' });
  1442→    }
  1443→
  1444→    const { data: template, error } = await supabase
  1445→      .from('nudge_templates')
  1446→      .insert({
  1447→        name,
  1448→        stage,
  1449→        lead_type: lead_type || 'all',
  1450→        message,
  1451→        variables: variables || [],
  1452→        active,
  1453→      })
  1454→      .select()
  1455→      .single();
  1456→
  1457→    if (error) {
  1458→      throw error;
  1459→    }
  1460→
  1461→    res.json({ template });
  1462→  } catch (error) {
  1463→    logger.error('Failed to create nudge template', { error: error.message });
  1464→    res.status(500).json({ error: 'Internal server error' });
  1465→  }
  1466→});
  1467→
  1468→/**
  1469→ * PATCH /api/activation/nudge-templates/:id
  1470→ * Update a nudge template
  1471→ */
  1472→router.patch('/nudge-templates/:id', async (req, res) => {
  1473→  try {
  1474→    if (!supabase) {
  1475→      return res.status(503).json({ error: 'Database not configured' });
  1476→    }
  1477→
  1478→    const { id } = req.params;
  1479→    const updates = req.body;
  1480→
  1481→    const { data: template, error } = await supabase
  1482→      .from('nudge_templates')
  1483→      .update(updates)
  1484→      .eq('id', id)
  1485→      .select()
  1486→      .single();
  1487→
  1488→    if (error) {
  1489→      throw error;
  1490→    }
  1491→
  1492→    res.json({ template });
  1493→  } catch (error) {
  1494→    logger.error('Failed to update nudge template', { error: error.message });
  1495→    res.status(500).json({ error: 'Internal server error' });
  1496→  }
  1497→});
  1498→
  1499→/**
  1500→ * DELETE /api/activation/nudge-templates/:id
  1501→ * Delete (deactivate) a nudge template
  1502→ */
  1503→router.delete('/nudge-templates/:id', async (req, res) => {
  1504→  try {
  1505→    if (!supabase) {
  1506→      return res.status(503).json({ error: 'Database not configured' });
  1507→    }
  1508→
  1509→    const { id } = req.params;
  1510→
  1511→    // Soft delete by marking as inactive
  1512→    const { error } = await supabase
  1513→      .from('nudge_templates')
  1514→      .update({ active: false })
  1515→      .eq('id', id);
  1516→
  1517→    if (error) {
  1518→      throw error;
  1519→    }
  1520→
  1521→    res.json({ success: true });
  1522→  } catch (error) {
  1523→    logger.error('Failed to delete nudge template', { error: error.message });
  1524→    res.status(500).json({ error: 'Internal server error' });
  1525→  }
  1526→});
  1527→
  1528→/**
  1529→ * Helper: Get default nudge templates
  1530→ */
  1531→function getDefaultTemplates() {
  1532→  return [
  1533→    {
  1534→      id: 'default-signed-up',
  1535→      name: 'Welcome - Signed Up',
  1536→      stage: 'signed_up',
  1537→      lead_type: 'all',
  1538→      message: 'Hi {first_name}! Saw you signed up recently. Need any help getting started?',
  1539→      variables: ['first_name'],
  1540→      active: true,
  1541→      isDefault: true,
  1542→    },
  1543→    {
  1544→      id: 'default-profile-complete',
  1545→      name: 'Profile Complete',
  1546→      stage: 'profile_complete',
  1547→      lead_type: 'all',
  1548→      message: "Hey {first_name}, your profile's looking good! Just a few more steps to get fully set up.",
  1549→      variables: ['first_name'],
  1550→      active: true,
  1551→      isDefault: true,
  1552→    },
  1553→    {
  1554→      id: 'default-job-ready',
  1555→      name: 'Job Ready',
  1556→      stage: 'job_ready',
  1557→      lead_type: 'worker',
  1558→      message: "Hi {first_name}! You're all set to start. Ready to find your first opportunity?",
  1559→      variables: ['first_name'],
  1560→      active: true,
  1561→      isDefault: true,
  1562→    },
  1563→    {
  1564→      id: 'default-stripe-ready',
  1565→      name: 'Stripe Ready',
  1566→      stage: 'stripe_ready',
  1567→      lead_type: 'contractor',
  1568→      message: 'Hey {first_name}, payments are all set up! Time to post your first job?',
  1569→      variables: ['first_name'],
  1570→      active: true,
  1571→      isDefault: true,
  1572→    },
  1573→    {
  1574→      id: 'default-job-posted',
  1575→      name: 'Job Posted',
  1576→      stage: 'job_posted',
  1577→      lead_type: 'contractor',
  1578→      message: "Hi {first_name}, your job is live! Let us know if you need help finding the right worker.",
  1579→      variables: ['first_name'],
  1580→      active: true,
  1581→      isDefault: true,
  1582→    },
  1583→    {
  1584→      id: 'default-applied',
  1585→      name: 'Applied',
  1586→      stage: 'applied',
  1587→      lead_type: 'worker',
  1588→      message: "Hey {first_name}! Great work applying. We're here if you need anything.",
  1589→      variables: ['first_name'],
  1590→      active: true,
  1591→      isDefault: true,
  1592→    },
  1593→    {
  1594→      id: 'default-working',
  1595→      name: 'Working',
  1596→      stage: 'working',
  1597→      lead_type: 'worker',
  1598→      message: "Hi {first_name}, hope the job's going well! Let us know if you need any support.",
  1599→      variables: ['first_name'],
  1600→      active: true,
  1601→      isDefault: true,
  1602→    },
  1603→    {
  1604→      id: 'default-hired',
  1605→      name: 'Hired',
  1606→      stage: 'hired',
  1607→      lead_type: 'contractor',
  1608→      message: "Hey {first_name}, congrats on the hire! Let us know how it's going.",
  1609→      variables: ['first_name'],
  1610→      active: true,
  1611→      isDefault: true,
  1612→    },
  1613→    {
  1614→      id: 'default-reminder',
  1615→      name: 'General Reminder',
  1616→      stage: 'all',
  1617→      lead_type: 'all',
  1618→      message: "Hi {first_name}! Just checking in - how can we help you today?",
  1619→      variables: ['first_name'],
  1620→      active: true,
  1621→      isDefault: true,
  1622→    },
  1623→  ];
  1624→}
  1625→
  1626→module.exports = router;
  1627→

<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→ * Lead Dossier API routes
     3→ * Structured living document per lead that accumulates all intel
     4→ */
     5→
     6→const express = require('express');
     7→const router = express.Router();
     8→const { supabase, supabaseAdmin } = require('../config/database');
     9→
    10→/**
    11→ * GET /api/dossier/:leadId
    12→ * Get the dossier for a lead (creates empty one if doesn't exist)
    13→ */
    14→router.get('/:leadId', async (req, res) => {
    15→  try {
    16→    if (!supabase) {
    17→      return res.status(503).json({ error: 'Database not configured' });
    18→    }
    19→
    20→    const { leadId } = req.params;
    21→    const db = supabaseAdmin || supabase;
    22→
    23→    // Try to get existing dossier (use admin to bypass RLS)
    24→    let { data: dossier, error } = await db
    25→      .from('lead_dossier')
    26→      .select('*')
    27→      .eq('lead_id', leadId)
    28→      .single();
    29→
    30→    if (error && error.code === 'PGRST116') {
    31→      // No dossier exists, create empty one
    32→      const { data: newDossier, error: createError } = await db
    33→        .from('lead_dossier')
    34→        .insert({ lead_id: leadId })
    35→        .select()
    36→        .single();
    37→
    38→      if (createError) {
    39→        // Handle duplicate key error - dossier was created between SELECT and INSERT
    40→        if (createError.code === '23505') {
    41→          const { data: existingDossier } = await db
    42→            .from('lead_dossier')
    43→            .select('*')
    44→            .eq('lead_id', leadId)
    45→            .single();
    46→          dossier = existingDossier;
    47→        } else {
    48→          console.error('Failed to create dossier:', createError.message);
    49→          return res.status(500).json({ error: 'Failed to create dossier' });
    50→        }
    51→      } else {
    52→        dossier = newDossier;
    53→      }
    54→    } else if (error) {
    55→      console.error('Failed to get dossier:', error.message);
    56→      return res.status(500).json({ error: 'Failed to get dossier' });
    57→    }
    58→
    59→    res.json(dossier);
    60→  } catch (err) {
    61→    console.error('Dossier get error:', err);
    62→    res.status(500).json({ error: 'Failed to get dossier' });
    63→  }
    64→});
    65→
    66→/**
    67→ * PATCH /api/dossier/:leadId
    68→ * Update specific sections of the dossier
    69→ */
    70→router.patch('/:leadId', async (req, res) => {
    71→  try {
    72→    if (!supabase) {
    73→      return res.status(503).json({ error: 'Database not configured' });
    74→    }
    75→
    76→    const { leadId } = req.params;
    77→    const updates = req.body;
    78→
    79→    // Only allow updating specific fields
    80→    const allowedFields = [
    81→      'key_facts', 'business_intel', 'personal_intel',
    82→      'call_summaries', 'objections', 'what_works',
    83→      'current_status', 'ai_summary', 'ai_summary_updated_at'
    84→    ];
    85→
    86→    const filteredUpdates = {};
    87→    for (const field of allowedFields) {
    88→      if (updates[field] !== undefined) {
    89→        filteredUpdates[field] = updates[field];
    90→      }
    91→    }
    92→
    93→    if (Object.keys(filteredUpdates).length === 0) {
    94→      return res.status(400).json({ error: 'No valid fields to update' });
    95→    }
    96→
    97→    // Upsert: update if exists, create if not
    98→    const { data: dossier, error } = await supabase
    99→      .from('lead_dossier')
   100→      .upsert({
   101→        lead_id: leadId,
   102→        ...filteredUpdates
   103→      }, {
   104→        onConflict: 'lead_id'
   105→      })
   106→      .select()
   107→      .single();
   108→
   109→    if (error) {
   110→      console.error('Failed to update dossier:', error.message);
   111→      return res.status(500).json({ error: 'Failed to update dossier' });
   112→    }
   113→
   114→    res.json(dossier);
   115→  } catch (err) {
   116→    console.error('Dossier update error:', err);
   117→    res.status(500).json({ error: 'Failed to update dossier' });
   118→  }
   119→});
   120→
   121→/**
   122→ * POST /api/dossier/:leadId/add-fact
   123→ * Add a new key fact to the dossier
   124→ */
   125→router.post('/:leadId/add-fact', async (req, res) => {
   126→  try {
   127→    if (!supabase) {
   128→      return res.status(503).json({ error: 'Database not configured' });
   129→    }
   130→
   131→    const { leadId } = req.params;
   132→    const { fact, source = 'manual' } = req.body;
   133→
   134→    if (!fact) {
   135→      return res.status(400).json({ error: 'fact is required' });
   136→    }
   137→
   138→    // Get current dossier
   139→    let { data: dossier } = await supabase
   140→      .from('lead_dossier')
   141→      .select('key_facts')
   142→      .eq('lead_id', leadId)
   143→      .single();
   144→
   145→    const currentFacts = dossier?.key_facts || [];
   146→    const newFact = {
   147→      fact,
   148→      source,
   149→      learned_at: new Date().toISOString()
   150→    };
   151→
   152→    // Upsert with new fact
   153→    const { data: updated, error } = await (supabaseAdmin || supabase)
   154→      .from('lead_dossier')
   155→      .upsert({
   156→        lead_id: leadId,
   157→        key_facts: [...currentFacts, newFact]
   158→      }, {
   159→        onConflict: 'lead_id'
   160→      })
   161→      .select()
   162→      .single();
   163→
   164→    if (error) {
   165→      console.error('Failed to add fact:', error.message);
   166→      return res.status(500).json({ error: 'Failed to add fact' });
   167→    }
   168→
   169→    res.json(updated);
   170→  } catch (err) {
   171→    console.error('Add fact error:', err);
   172→    res.status(500).json({ error: 'Failed to add fact' });
   173→  }
   174→});
   175→
   176→/**
   177→ * POST /api/dossier/:leadId/add-call-summary
   178→ * Add a call summary to the dossier
   179→ */
   180→router.post('/:leadId/add-call-summary', async (req, res) => {
   181→  try {
   182→    if (!supabase) {
   183→      return res.status(503).json({ error: 'Database not configured' });
   184→    }
   185→
   186→    const { leadId } = req.params;
   187→    const { duration, outcome, key_points = [], next_steps, mood } = req.body;
   188→
   189→    // Get current dossier
   190→    let { data: dossier } = await supabase
   191→      .from('lead_dossier')
   192→      .select('call_summaries')
   193→      .eq('lead_id', leadId)
   194→      .single();
   195→
   196→    const currentSummaries = dossier?.call_summaries || [];
   197→    const newSummary = {
   198→      date: new Date().toISOString(),
   199→      duration,
   200→      outcome,
   201→      key_points,
   202→      next_steps,
   203→      mood
   204→    };
   205→
   206→    // Upsert with new summary
   207→    const { data: updated, error } = await (supabaseAdmin || supabase)
   208→      .from('lead_dossier')
   209→      .upsert({
   210→        lead_id: leadId,
   211→        call_summaries: [...currentSummaries, newSummary]
   212→      }, {
   213→        onConflict: 'lead_id'
   214→      })
   215→      .select()
   216→      .single();
   217→
   218→    if (error) {
   219→      console.error('Failed to add call summary:', error.message);
   220→      return res.status(500).json({ error: 'Failed to add call summary' });
   221→    }
   222→
   223→    res.json(updated);
   224→  } catch (err) {
   225→    console.error('Add call summary error:', err);
   226→    res.status(500).json({ error: 'Failed to add call summary' });
   227→  }
   228→});
   229→
   230→/**
   231→ * POST /api/dossier/:leadId/add-objection
   232→ * Add an objection to the dossier
   233→ */
   234→router.post('/:leadId/add-objection', async (req, res) => {
   235→  try {
   236→    if (!supabase) {
   237→      return res.status(503).json({ error: 'Database not configured' });
   238→    }
   239→
   240→    const { leadId } = req.params;
   241→    const { objection, response_used, effective } = req.body;
   242→
   243→    if (!objection) {
   244→      return res.status(400).json({ error: 'objection is required' });
   245→    }
   246→
   247→    // Get current dossier
   248→    let { data: dossier } = await supabase
   249→      .from('lead_dossier')
   250→      .select('objections')
   251→      .eq('lead_id', leadId)
   252→      .single();
   253→
   254→    const currentObjections = dossier?.objections || [];
   255→    const newObjection = {
   256→      objection,
   257→      response_used,
   258→      effective,
   259→      date: new Date().toISOString()
   260→    };
   261→
   262→    // Upsert with new objection
   263→    const { data: updated, error } = await (supabaseAdmin || supabase)
   264→      .from('lead_dossier')
   265→      .upsert({
   266→        lead_id: leadId,
   267→        objections: [...currentObjections, newObjection]
   268→      }, {
   269→        onConflict: 'lead_id'
   270→      })
   271→      .select()
   272→      .single();
   273→
   274→    if (error) {
   275→      console.error('Failed to add objection:', error.message);
   276→      return res.status(500).json({ error: 'Failed to add objection' });
   277→    }
   278→
   279→    res.json(updated);
   280→  } catch (err) {
   281→    console.error('Add objection error:', err);
   282→    res.status(500).json({ error: 'Failed to add objection' });
   283→  }
   284→});
   285→
   286→module.exports = router;
   287→

<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→ * Deal Intelligence API Routes
     3→ * Objection analysis, deal velocity, stall prediction
     4→ */
     5→
     6→const express = require('express');
     7→const router = express.Router();
     8→const dealIntelligence = require('../services/dealIntelligence');
     9→
    10→/**
    11→ * GET /api/deals/intelligence/objections
    12→ * Analyze objection impact across all deals
    13→ */
    14→router.get('/intelligence/objections', async (req, res) => {
    15→  try {
    16→    const result = await dealIntelligence.analyzeObjectionImpact();
    17→
    18→    if (!result.success) {
    19→      return res.status(500).json({ error: result.error });
    20→    }
    21→
    22→    res.json(result.objections);
    23→  } catch (error) {
    24→    console.error('Error in GET /intelligence/objections:', error);
    25→    res.status(500).json({ error: 'Failed to analyze objections' });
    26→  }
    27→});
    28→
    29→/**
    30→ * GET /api/deals/intelligence/responses/:objectionType
    31→ * Get winning responses for an objection type
    32→ */
    33→router.get('/intelligence/responses/:objectionType', async (req, res) => {
    34→  try {
    35→    const { objectionType } = req.params;
    36→    const result = await dealIntelligence.findWinningResponses(objectionType);
    37→
    38→    if (!result.success) {
    39→      return res.status(500).json({ error: result.error });
    40→    }
    41→
    42→    res.json(result.responses);
    43→  } catch (error) {
    44→    console.error('Error in GET /intelligence/responses:', error);
    45→    res.status(500).json({ error: 'Failed to get winning responses' });
    46→  }
    47→});
    48→
    49→/**
    50→ * GET /api/deals/intelligence/velocity
    51→ * Get deal velocity statistics
    52→ */
    53→router.get('/intelligence/velocity', async (req, res) => {
    54→  try {
    55→    const { startDate, endDate, userId } = req.query;
    56→    const result = await dealIntelligence.getDealVelocity({
    57→      startDate,
    58→      endDate,
    59→      userId
    60→    });
    61→
    62→    if (!result.success) {
    63→      return res.status(500).json({ error: result.error });
    64→    }
    65→
    66→    res.json(result.velocity);
    67→  } catch (error) {
    68→    console.error('Error in GET /intelligence/velocity:', error);
    69→    res.status(500).json({ error: 'Failed to get deal velocity' });
    70→  }
    71→});
    72→
    73→/**
    74→ * GET /api/deals/intelligence/stall/:leadId
    75→ * Predict stall probability for a lead
    76→ */
    77→router.get('/intelligence/stall/:leadId', async (req, res) => {
    78→  try {
    79→    const { leadId } = req.params;
    80→    const result = await dealIntelligence.predictStall(leadId);
    81→
    82→    if (!result.success) {
    83→      return res.status(404).json({ error: result.error });
    84→    }
    85→
    86→    res.json(result.prediction);
    87→  } catch (error) {
    88→    console.error('Error in GET /intelligence/stall:', error);
    89→    res.status(500).json({ error: 'Failed to predict stall' });
    90→  }
    91→});
    92→
    93→/**
    94→ * GET /api/deals/intelligence/at-risk
    95→ * Get deals at risk of stalling
    96→ */
    97→router.get('/intelligence/at-risk', async (req, res) => {
    98→  try {
    99→    const limit = parseInt(req.query.limit) || 10;
   100→    const result = await dealIntelligence.getDealsAtRisk(limit);
   101→
   102→    if (!result.success) {
   103→      return res.status(500).json({ error: result.error });
   104→    }
   105→
   106→    res.json(result.dealsAtRisk);
   107→  } catch (error) {
   108→    console.error('Error in GET /intelligence/at-risk:', error);
   109→    res.status(500).json({ error: 'Failed to get deals at risk' });
   110→  }
   111→});
   112→
   113→/**
   114→ * GET /api/deals/outcomes
   115→ * Get deal outcomes summary
   116→ */
   117→router.get('/outcomes', async (req, res) => {
   118→  try {
   119→    const { startDate, endDate } = req.query;
   120→
   121→    // Get wins
   122→    const velocityResult = await dealIntelligence.getDealVelocity({ startDate, endDate });
   123→
   124→    // Get losses
   125→    const lossResult = await dealIntelligence.getLossReasons({ startDate, endDate });
   126→
   127→    res.json({
   128→      wins: velocityResult.success ? velocityResult.velocity : null,
   129→      losses: lossResult.success ? { total: lossResult.total, reasons: lossResult.reasons } : null
   130→    });
   131→  } catch (error) {
   132→    console.error('Error in GET /outcomes:', error);
   133→    res.status(500).json({ error: 'Failed to get outcomes' });
   134→  }
   135→});
   136→
   137→/**
   138→ * POST /api/deals/outcomes
   139→ * Record a deal outcome (win/loss)
   140→ */
   141→router.post('/outcomes', async (req, res) => {
   142→  try {
   143→    const {
   144→      leadId,
   145→      outcome,
   146→      primaryReason,
   147→      secondaryReasons,
   148→      competitorLostTo,
   149→      dealValue,
   150→      winningResponse,
   151→      repNotes
   152→    } = req.body;
   153→
   154→    if (!leadId || !outcome) {
   155→      return res.status(400).json({ error: 'leadId and outcome are required' });
   156→    }
   157→
   158→    if (!['won', 'lost', 'stalled'].includes(outcome)) {
   159→      return res.status(400).json({ error: 'outcome must be won, lost, or stalled' });
   160→    }
   161→
   162→    const result = await dealIntelligence.recordDealOutcome({
   163→      leadId,
   164→      outcome,
   165→      primaryReason,
   166→      secondaryReasons,
   167→      competitorLostTo,
   168→      dealValue,
   169→      winningResponse,
   170→      repNotes,
   171→      recordedBy: req.user?.id
   172→    });
   173→
   174→    if (!result.success) {
   175→      return res.status(500).json({ error: result.error });
   176→    }
   177→
   178→    res.json(result.outcome);
   179→  } catch (error) {
   180→    console.error('Error in POST /outcomes:', error);
   181→    res.status(500).json({ error: 'Failed to record outcome' });
   182→  }
   183→});
   184→
   185→/**
   186→ * GET /api/deals/outcomes/reasons
   187→ * Get loss reason breakdown
   188→ */
   189→router.get('/outcomes/reasons', async (req, res) => {
   190→  try {
   191→    const { startDate, endDate } = req.query;
   192→    const result = await dealIntelligence.getLossReasons({ startDate, endDate });
   193→
   194→    if (!result.success) {
   195→      return res.status(500).json({ error: result.error });
   196→    }
   197→
   198→    res.json(result);
   199→  } catch (error) {
   200→    console.error('Error in GET /outcomes/reasons:', error);
   201→    res.status(500).json({ error: 'Failed to get loss reasons' });
   202→  }
   203→});
   204→
   205→/**
   206→ * GET /api/deals/outcomes/reason-options
   207→ * Get configured loss reason options for UI
   208→ */
   209→router.get('/outcomes/reason-options', async (req, res) => {
   210→  try {
   211→    const result = await dealIntelligence.getLossReasonOptions();
   212→
   213→    if (!result.success) {
   214→      return res.status(500).json({ error: result.error });
   215→    }
   216→
   217→    res.json(result.reasons);
   218→  } catch (error) {
   219→    console.error('Error in GET /outcomes/reason-options:', error);
   220→    res.status(500).json({ error: 'Failed to get reason options' });
   221→  }
   222→});
   223→
   224→/**
   225→ * POST /api/deals/stages
   226→ * Record a stage transition
   227→ */
   228→router.post('/stages', async (req, res) => {
   229→  try {
   230→    const { leadId, stage, notes, metadata } = req.body;
   231→
   232→    if (!leadId || !stage) {
   233→      return res.status(400).json({ error: 'leadId and stage are required' });
   234→    }
   235→
   236→    const validStages = ['new', 'contacted', 'discovery', 'proposal', 'negotiation', 'won', 'lost'];
   237→    if (!validStages.includes(stage)) {
   238→      return res.status(400).json({ error: `stage must be one of: ${validStages.join(', ')}` });
   239→    }
   240→
   241→    const result = await dealIntelligence.recordStageTransition(leadId, stage, notes, metadata);
   242→
   243→    if (!result.success) {
   244→      return res.status(500).json({ error: result.error });
   245→    }
   246→
   247→    res.json(result.stage);
   248→  } catch (error) {
   249→    console.error('Error in POST /stages:', error);
   250→    res.status(500).json({ error: 'Failed to record stage' });
   251→  }
   252→});
   253→
   254→module.exports = router;
   255→

<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→ * Battleground V2 API Routes
     3→ * XP Duels + Manager Challenges
     4→ */
     5→
     6→const express = require('express');
     7→const router = express.Router();
     8→const duelsService = require('../services/duelsService');
     9→const challengesService = require('../services/challengesServiceV2');
    10→
    11→// ===========================================
    12→// DUELS ENDPOINTS
    13→// ===========================================
    14→
    15→/**
    16→ * Get available duel types
    17→ */
    18→router.get('/duels/types', (req, res) => {
    19→  res.json({
    20→    success: true,
    21→    types: duelsService.DUEL_TYPES
    22→  });
    23→});
    24→
    25→/**
    26→ * Create a duel challenge
    27→ * POST /api/battleground/duels/challenge
    28→ */
    29→router.post('/duels/challenge', async (req, res) => {
    30→  try {
    31→    const userId = req.headers['x-user-id'];
    32→    if (!userId) {
    33→      return res.status(401).json({ success: false, error: 'User ID required' });
    34→    }
    35→
    36→    const { targetId, duelType, stakeAmount } = req.body;
    37→
    38→    if (!targetId || !duelType) {
    39→      return res.status(400).json({
    40→        success: false,
    41→        error: 'targetId and duelType are required'
    42→      });
    43→    }
    44→
    45→    const duel = await duelsService.createDuel(
    46→      userId,
    47→      targetId,
    48→      duelType,
    49→      stakeAmount || 100
    50→    );
    51→
    52→    res.json({ success: true, duel });
    53→  } catch (error) {
    54→    console.error('Error creating duel:', error);
    55→    res.status(400).json({ success: false, error: error.message });
    56→  }
    57→});
    58→
    59→/**
    60→ * Accept a duel
    61→ * POST /api/battleground/duels/:id/accept
    62→ */
    63→router.post('/duels/:id/accept', async (req, res) => {
    64→  try {
    65→    const userId = req.headers['x-user-id'];
    66→    if (!userId) {
    67→      return res.status(401).json({ success: false, error: 'User ID required' });
    68→    }
    69→
    70→    const duel = await duelsService.acceptDuel(req.params.id, userId);
    71→    res.json({ success: true, duel });
    72→  } catch (error) {
    73→    console.error('Error accepting duel:', error);
    74→    res.status(400).json({ success: false, error: error.message });
    75→  }
    76→});
    77→
    78→/**
    79→ * Decline a duel
    80→ * POST /api/battleground/duels/:id/decline
    81→ */
    82→router.post('/duels/:id/decline', async (req, res) => {
    83→  try {
    84→    const userId = req.headers['x-user-id'];
    85→    if (!userId) {
    86→      return res.status(401).json({ success: false, error: 'User ID required' });
    87→    }
    88→
    89→    await duelsService.declineDuel(req.params.id, userId);
    90→    res.json({ success: true });
    91→  } catch (error) {
    92→    console.error('Error declining duel:', error);
    93→    res.status(400).json({ success: false, error: error.message });
    94→  }
    95→});
    96→
    97→/**
    98→ * Get user's active duels
    99→ * GET /api/battleground/duels/active
   100→ */
   101→router.get('/duels/active', async (req, res) => {
   102→  try {
   103→    const userId = req.headers['x-user-id'];
   104→    if (!userId) {
   105→      return res.status(401).json({ success: false, error: 'User ID required' });
   106→    }
   107→
   108→    const duels = await duelsService.getUserDuels(userId);
   109→    res.json({ success: true, duels });
   110→  } catch (error) {
   111→    console.error('Error getting active duels:', error);
   112→    res.status(500).json({ success: false, error: error.message });
   113→  }
   114→});
   115→
   116→/**
   117→ * Get user's duel history
   118→ * GET /api/battleground/duels/history
   119→ */
   120→router.get('/duels/history', async (req, res) => {
   121→  try {
   122→    const userId = req.headers['x-user-id'];
   123→    if (!userId) {
   124→      return res.status(401).json({ success: false, error: 'User ID required' });
   125→    }
   126→
   127→    const limit = parseInt(req.query.limit) || 10;
   128→    const duels = await duelsService.getUserDuelHistory(userId, limit);
   129→    res.json({ success: true, duels });
   130→  } catch (error) {
   131→    console.error('Error getting duel history:', error);
   132→    res.status(500).json({ success: false, error: error.message });
   133→  }
   134→});
   135→
   136→/**
   137→ * Get duel scores (live update)
   138→ * GET /api/battleground/duels/:id/scores
   139→ */
   140→router.get('/duels/:id/scores', async (req, res) => {
   141→  try {
   142→    const duel = await duelsService.updateDuelScores(req.params.id);
   143→    if (!duel) {
   144→      return res.status(404).json({ success: false, error: 'Duel not found or not active' });
   145→    }
   146→    res.json({ success: true, duel });
   147→  } catch (error) {
   148→    console.error('Error getting duel scores:', error);
   149→    res.status(500).json({ success: false, error: error.message });
   150→  }
   151→});
   152→
   153→/**
   154→ * Calculate odds for a potential duel
   155→ * GET /api/battleground/duels/odds
   156→ */
   157→router.get('/duels/odds', async (req, res) => {
   158→  try {
   159→    const userId = req.headers['x-user-id'];
   160→    if (!userId) {
   161→      return res.status(401).json({ success: false, error: 'User ID required' });
   162→    }
   163→
   164→    const { targetId, duelType } = req.query;
   165→    if (!targetId || !duelType) {
   166→      return res.status(400).json({
   167→        success: false,
   168→        error: 'targetId and duelType are required'
   169→      });
   170→    }
   171→
   172→    const odds = await duelsService.calculateOdds(userId, targetId, duelType);
   173→
   174→    res.json({
   175→      success: true,
   176→      odds,
   177→      display: odds > 1
   178→        ? `You're ${odds}:1 favorite`
   179→        : `You're ${Math.round(1/odds * 100) / 100}:1 underdog`
   180→    });
   181→  } catch (error) {
   182→    console.error('Error calculating odds:', error);
   183→    res.status(500).json({ success: false, error: error.message });
   184→  }
   185→});
   186→
   187→// ===========================================
   188→// MANAGER CHALLENGES ENDPOINTS
   189→// ===========================================
   190→
   191→/**
   192→ * Get challenge metrics and reward types
   193→ */
   194→router.get('/challenges/config', (req, res) => {
   195→  res.json({
   196→    success: true,
   197→    metrics: challengesService.CHALLENGE_METRICS,
   198→    rewardTypes: challengesService.REWARD_TYPES
   199→  });
   200→});
   201→
   202→/**
   203→ * Create a manager challenge
   204→ * POST /api/battleground/challenges
   205→ */
   206→router.post('/challenges', async (req, res) => {
   207→  try {
   208→    const userId = req.headers['x-user-id'];
   209→    if (!userId) {
   210→      return res.status(401).json({ success: false, error: 'User ID required' });
   211→    }
   212→
   213→    const challenge = await challengesService.createChallenge({
   214→      createdBy: userId,
   215→      ...req.body
   216→    });
   217→
   218→    res.json({ success: true, challenge });
   219→  } catch (error) {
   220→    console.error('Error creating challenge:', error);
   221→    res.status(400).json({ success: false, error: error.message });
   222→  }
   223→});
   224→
   225→/**
   226→ * Get active challenges
   227→ * GET /api/battleground/challenges
   228→ */
   229→router.get('/challenges', async (req, res) => {
   230→  try {
   231→    const challenges = await challengesService.getActiveChallenges();
   232→    res.json({ success: true, challenges });
   233→  } catch (error) {
   234→    console.error('Error getting challenges:', error);
   235→    res.status(500).json({ success: false, error: error.message });
   236→  }
   237→});
   238→
   239→/**
   240→ * Get challenge details
   241→ * GET /api/battleground/challenges/:id
   242→ */
   243→router.get('/challenges/:id', async (req, res) => {
   244→  try {
   245→    const challenge = await challengesService.getChallengeDetails(req.params.id);
   246→    res.json({ success: true, challenge });
   247→  } catch (error) {
   248→    console.error('Error getting challenge details:', error);
   249→    res.status(500).json({ success: false, error: error.message });
   250→  }
   251→});
   252→
   253→/**
   254→ * Join a challenge
   255→ * POST /api/battleground/challenges/:id/join
   256→ */
   257→router.post('/challenges/:id/join', async (req, res) => {
   258→  try {
   259→    const userId = req.headers['x-user-id'];
   260→    if (!userId) {
   261→      return res.status(401).json({ success: false, error: 'User ID required' });
   262→    }
   263→
   264→    await challengesService.joinChallenge(req.params.id, userId);
   265→    res.json({ success: true });
   266→  } catch (error) {
   267→    console.error('Error joining challenge:', error);
   268→    res.status(400).json({ success: false, error: error.message });
   269→  }
   270→});
   271→
   272→/**
   273→ * Cancel a challenge
   274→ * DELETE /api/battleground/challenges/:id
   275→ */
   276→router.delete('/challenges/:id', async (req, res) => {
   277→  try {
   278→    const userId = req.headers['x-user-id'];
   279→    if (!userId) {
   280→      return res.status(401).json({ success: false, error: 'User ID required' });
   281→    }
   282→
   283→    await challengesService.cancelChallenge(req.params.id, userId);
   284→    res.json({ success: true });
   285→  } catch (error) {
   286→    console.error('Error cancelling challenge:', error);
   287→    res.status(400).json({ success: false, error: error.message });
   288→  }
   289→});
   290→
   291→// ===========================================
   292→// CELEBRATIONS ENDPOINTS
   293→// ===========================================
   294→
   295→/**
   296→ * Get recent celebrations (team pulse)
   297→ * GET /api/battleground/celebrations
   298→ */
   299→router.get('/celebrations', async (req, res) => {
   300→  try {
   301→    const limit = parseInt(req.query.limit) || 10;
   302→    const celebrations = await duelsService.getRecentCelebrations(limit);
   303→    res.json({ success: true, celebrations });
   304→  } catch (error) {
   305→    console.error('Error getting celebrations:', error);
   306→    res.status(500).json({ success: false, error: error.message });
   307→  }
   308→});
   309→
   310→/**
   311→ * Get all team members (for duel target selection)
   312→ * GET /api/battleground/team
   313→ */
   314→router.get('/team', async (req, res) => {
   315→  try {
   316→    const userId = req.headers['x-user-id'];
   317→    const supabase = require('../services/supabase');
   318→
   319→    const { data: users, error } = await supabase
   320→      .from('user_xp')
   321→      .select('user_id, display_name, xp, level, avatar_url')
   322→      .neq('user_id', userId)
   323→      .order('xp', { ascending: false });
   324→
   325→    if (error) throw error;
   326→
   327→    res.json({ success: true, team: users || [] });
   328→  } catch (error) {
   329→    console.error('Error getting team:', error);
   330→    res.status(500).json({ success: false, error: error.message });
   331→  }
   332→});
   333→
   334→module.exports = router;
   335→

<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→ * Frontend Tracking Routes
     3→ * No auth required - for collecting web vitals and errors from frontend
     4→ */
     5→
     6→const express = require('express');
     7→const router = express.Router();
     8→const { supabaseAdmin, supabase } = require('../config/database');
     9→
    10→// Use admin client to bypass RLS
    11→const db = supabaseAdmin || supabase;
    12→
    13→/**
    14→ * POST /api/frontend/vitals
    15→ * Store web vital metrics (LCP, INP, CLS, FCP, TTFB)
    16→ */
    17→router.post('/vitals', async (req, res) => {
    18→  try {
    19→    const { metric_name, metric_value, rating, page_path, delta, id, navigationType } = req.body;
    20→
    21→    if (!metric_name || metric_value === undefined) {
    22→      return res.status(400).json({
    23→        success: false,
    24→        error: 'metric_name and metric_value are required'
    25→      });
    26→    }
    27→
    28→    const { error } = await db.from('web_vitals').insert({
    29→      metric_name,
    30→      metric_value,
    31→      rating: rating || null,
    32→      page_path: page_path || null,
    33→      delta: delta || null,
    34→      navigation_type: navigationType || null,
    35→      recorded_at: new Date().toISOString()
    36→    });
    37→
    38→    if (error && error.code === '42P01') {
    39→      // Table doesn't exist yet
    40→      return res.json({ success: true, note: 'Table not created yet' });
    41→    }
    42→
    43→    if (error) throw error;
    44→
    45→    res.json({ success: true });
    46→  } catch (error) {
    47→    console.error('Error storing web vital:', error);
    48→    res.status(500).json({ success: false, error: error.message });
    49→  }
    50→});
    51→
    52→/**
    53→ * POST /api/frontend/error
    54→ * Store frontend errors
    55→ */
    56→router.post('/error', async (req, res) => {
    57→  try {
    58→    const { message, stack, page_path, url, user_agent, type, component_stack, filename, lineno, colno } = req.body;
    59→
    60→    if (!message) {
    61→      return res.status(400).json({
    62→        success: false,
    63→        error: 'message is required'
    64→      });
    65→    }
    66→
    67→    const { error } = await db.from('frontend_errors').insert({
    68→      message,
    69→      stack: stack || null,
    70→      page_path: page_path || null,
    71→      user_agent: user_agent || null,
    72→      error_type: type || 'unknown',
    73→      component_stack: component_stack || null,
    74→      metadata: { url, filename, lineno, colno },
    75→      created_at: new Date().toISOString()
    76→    });
    77→
    78→    if (error && error.code === '42P01') {
    79→      // Table doesn't exist yet
    80→      return res.json({ success: true, note: 'Table not created yet' });
    81→    }
    82→
    83→    if (error) throw error;
    84→
    85→    res.json({ success: true });
    86→  } catch (error) {
    87→    console.error('Error storing frontend error:', error);
    88→    res.status(500).json({ success: false, error: error.message });
    89→  }
    90→});
    91→
    92→/**
    93→ * GET /api/frontend/vitals/summary
    94→ * Get web vitals summary (public for debugging)
    95→ */
    96→router.get('/vitals/summary', async (req, res) => {
    97→  try {
    98→    const { hours = 24 } = req.query;
    99→    const since = new Date(Date.now() - parseInt(hours) * 60 * 60 * 1000).toISOString();
   100→
   101→    const { data, error } = await db
   102→      .from('web_vitals')
   103→      .select('metric_name, metric_value, rating')
   104→      .gte('recorded_at', since);
   105→
   106→    if (error && error.code !== '42P01') throw error;
   107→
   108→    // Aggregate by metric
   109→    const metrics = {};
   110→    const metricNames = ['LCP', 'INP', 'CLS', 'FCP', 'TTFB'];
   111→
   112→    metricNames.forEach(name => {
   113→      const values = (data || []).filter(d => d.metric_name === name).map(d => d.metric_value);
   114→      if (values.length > 0) {
   115→        const avg = values.reduce((sum, v) => sum + v, 0) / values.length;
   116→        const p75 = values.sort((a, b) => a - b)[Math.floor(values.length * 0.75)];
   117→        const ratings = (data || []).filter(d => d.metric_name === name);
   118→        const goodCount = ratings.filter(r => r.rating === 'good').length;
   119→
   120→        metrics[name] = {
   121→          avg: Math.round(avg * 100) / 100,
   122→          p75: Math.round(p75 * 100) / 100,
   123→          count: values.length,
   124→          goodRate: Math.round((goodCount / values.length) * 100)
   125→        };
   126→      }
   127→    });
   128→
   129→    res.json({
   130→      success: true,
   131→      metrics,
   132→      period: `${hours}h`
   133→    });
   134→  } catch (error) {
   135→    console.error('Error fetching vitals summary:', error);
   136→    res.status(500).json({ success: false, error: error.message });
   137→  }
   138→});
   139→
   140→module.exports = router;
   141→

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