     1→/**
     2→ * Notion Integration API routes
     3→ * For Clawdbot to sync Growth Engine data to Notion
     4→ */
     5→
     6→const express = require('express');
     7→const router = express.Router();
     8→const { supabase, supabaseAdmin } = require('../config/database');
     9→const { getScoreTier } = require('../services/scoring');
    10→
    11→// Notion API configuration
    12→const NOTION_API_KEY = process.env.NOTION_API_KEY;
    13→const NOTION_BUSINESS_STATE_DB = process.env.NOTION_BUSINESS_STATE_DB;
    14→const NOTION_METRICS_DB = process.env.NOTION_METRICS_DB;
    15→const NOTION_KNOWLEDGE_DB = process.env.NOTION_KNOWLEDGE_DB;
    16→
    17→/**
    18→ * Helper to call Notion API
    19→ */
    20→async function notionRequest(endpoint, method = 'GET', body = null) {
    21→  if (!NOTION_API_KEY) {
    22→    throw new Error('NOTION_API_KEY not configured');
    23→  }
    24→
    25→  const options = {
    26→    method,
    27→    headers: {
    28→      'Authorization': `Bearer ${NOTION_API_KEY}`,
    29→      'Content-Type': 'application/json',
    30→      'Notion-Version': '2022-06-28'
    31→    }
    32→  };
    33→
    34→  if (body) {
    35→    options.body = JSON.stringify(body);
    36→  }
    37→
    38→  const response = await fetch(`https://api.notion.com/v1${endpoint}`, options);
    39→  const data = await response.json();
    40→
    41→  if (!response.ok) {
    42→    throw new Error(data.message || `Notion API error: ${response.status}`);
    43→  }
    44→
    45→  return data;
    46→}
    47→
    48→// Use admin client if available (bypasses RLS)
    49→const db = supabaseAdmin || supabase;
    50→
    51→/**
    52→ * Get today's start in AEST timezone
    53→ */
    54→function getTodayStartAEST() {
    55→  const AEST_OFFSET_HOURS = 10;
    56→  const AEST_OFFSET_MS = AEST_OFFSET_HOURS * 60 * 60 * 1000;
    57→  const now = new Date();
    58→  const aestTime = new Date(now.getTime() + AEST_OFFSET_MS);
    59→  const midnightAESTAsUTC = new Date(Date.UTC(
    60→    aestTime.getUTCFullYear(),
    61→    aestTime.getUTCMonth(),
    62→    aestTime.getUTCDate(),
    63→    0, 0, 0, 0
    64→  ));
    65→  return new Date(midnightAESTAsUTC.getTime() - AEST_OFFSET_MS);
    66→}
    67→
    68→/**
    69→ * Get week start in AEST (Sunday at midnight AEST)
    70→ */
    71→function getWeekStartAEST() {
    72→  const AEST_OFFSET_HOURS = 10;
    73→  const AEST_OFFSET_MS = AEST_OFFSET_HOURS * 60 * 60 * 1000;
    74→  const now = new Date();
    75→  const aestTime = new Date(now.getTime() + AEST_OFFSET_MS);
    76→  const aestDay = aestTime.getUTCDay();
    77→  const aestWeekStart = new Date(Date.UTC(
    78→    aestTime.getUTCFullYear(),
    79→    aestTime.getUTCMonth(),
    80→    aestTime.getUTCDate() - aestDay,
    81→    0, 0, 0, 0
    82→  ));
    83→  return new Date(aestWeekStart.getTime() - AEST_OFFSET_MS);
    84→}
    85→
    86→/**
    87→ * GET /api/notion/snapshot
    88→ * Get current business snapshot for Clawdbot to push to Notion
    89→ */
    90→router.get('/snapshot', async (req, res) => {
    91→  try {
    92→    if (!db) {
    93→      return res.status(503).json({ error: 'Database not configured' });
    94→    }
    95→
    96→    const now = new Date();
    97→    const todayStart = getTodayStartAEST();
    98→    const weekStart = getWeekStartAEST();
    99→    const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
   100→
   101→    // Run parallel queries
   102→    const [
   103→      leadsResult,
   104→      todayCommsResult,
   105→      weekCommsResult,
   106→      conversionsResult,
   107→      teamResult,
   108→      callbacksResult
   109→    ] = await Promise.all([
   110→      // All active leads
   111→      db.from('leads')
   112→        .select('id, score, status, assigned_to, estimated_value')
   113→        .is('deleted_at', null)
   114→        .not('status', 'in', '("converted","opted_out","archived")'),
   115→
   116→      // Today's communications
   117→      db.from('communications')
   118→        .select('type, handled_by, user_id, metadata')
   119→        .gte('created_at', todayStart.toISOString()),
   120→
   121→      // This week's communications
   122→      db.from('communications')
   123→        .select('type, handled_by, user_id, metadata')
   124→        .gte('created_at', weekStart.toISOString()),
   125→
   126→      // This month's conversions
   127→      db.from('conversions')
   128→        .select('value, user_id, converted_at')
   129→        .gte('converted_at', monthStart.toISOString()),
   130→
   131→      // Active team members
   132→      db.from('user_xp')
   133→        .select('user_id, display_name, email, role')
   134→        .eq('is_active', true),
   135→
   136→      // Pending callbacks
   137→      db.from('callbacks')
   138→        .select('id')
   139→        .eq('status', 'pending')
   140→    ]);
   141→
   142→    // Calculate pipeline stats
   143→    const leads = leadsResult.data || [];
   144→    const pipeline = {
   145→      total: leads.length,
   146→      hot: leads.filter(l => getScoreTier(l.score) === 'hot').length,
   147→      warm: leads.filter(l => getScoreTier(l.score) === 'warm').length,
   148→      cool: leads.filter(l => getScoreTier(l.score) === 'cool').length,
   149→      cold: leads.filter(l => getScoreTier(l.score) === 'cold').length,
   150→      totalValue: leads.reduce((sum, l) => sum + (parseFloat(l.estimated_value) || 0), 0)
   151→    };
   152→
   153→    // Calculate today's stats
   154→    const todayComms = todayCommsResult.data || [];
   155→    const today = {
   156→      calls: todayComms.filter(c => c.type === 'call' || c.type === 'call_outbound').length,
   157→      conversations: todayComms.filter(c => {
   158→        if (c.type !== 'call' && c.type !== 'call_outbound') return false;
   159→        const outcome = c.metadata?.outcome;
   160→        return outcome && !['no_answer', 'voicemail', 'busy', 'wrong_number'].includes(outcome);
   161→      }).length,
   162→      sms: todayComms.filter(c => c.type === 'sms_outbound').length,
   163→      smsReplies: todayComms.filter(c => c.type === 'sms_inbound').length
   164→    };
   165→
   166→    // Calculate week's stats
   167→    const weekComms = weekCommsResult.data || [];
   168→    const week = {
   169→      calls: weekComms.filter(c => c.type === 'call' || c.type === 'call_outbound').length,
   170→      conversations: weekComms.filter(c => {
   171→        if (c.type !== 'call' && c.type !== 'call_outbound') return false;
   172→        const outcome = c.metadata?.outcome;
   173→        return outcome && !['no_answer', 'voicemail', 'busy', 'wrong_number'].includes(outcome);
   174→      }).length,
   175→      sms: weekComms.filter(c => c.type === 'sms_outbound').length
   176→    };
   177→
   178→    // Calculate conversions
   179→    const conversions = conversionsResult.data || [];
   180→    const month = {
   181→      conversions: conversions.length,
   182→      revenue: conversions.reduce((sum, c) => sum + (parseFloat(c.value) || 0), 0)
   183→    };
   184→
   185→    // Team activity
   186→    const team = teamResult.data || [];
   187→    const teamActivity = team.map(member => {
   188→      const memberComms = todayComms.filter(c =>
   189→        c.handled_by === member.user_id || c.user_id === member.user_id
   190→      );
   191→      return {
   192→        name: member.display_name,
   193→        role: member.role,
   194→        today: {
   195→          calls: memberComms.filter(c => c.type === 'call' || c.type === 'call_outbound').length,
   196→          sms: memberComms.filter(c => c.type === 'sms_outbound').length
   197→        }
   198→      };
   199→    });
   200→
   201→    // Pending items
   202→    const pendingCallbacks = callbacksResult.data?.length || 0;
   203→
   204→    // Build response
   205→    res.json({
   206→      date: now.toISOString().split('T')[0],
   207→      generatedAt: now.toISOString(),
   208→
   209→      pipeline,
   210→
   211→      today,
   212→
   213→      week,
   214→
   215→      month,
   216→
   217→      team: teamActivity,
   218→
   219→      alerts: {
   220→        pendingCallbacks,
   221→        hotLeadsNoContact: leads.filter(l =>
   222→          getScoreTier(l.score) === 'hot' &&
   223→          !todayComms.some(c => c.lead_id === l.id)
   224→        ).length
   225→      },
   226→
   227→      // Summary for quick Notion entry
   228→      summary: {
   229→        headline: `${today.calls} calls, ${today.conversations} conversations, ${pipeline.hot} hot leads`,
   230→        status: today.calls >= 10 ? 'green' : today.calls >= 5 ? 'yellow' : 'red',
   231→        topMetric: `Pipeline: ${pipeline.total} leads ($${pipeline.totalValue.toLocaleString()})`
   232→      }
   233→    });
   234→
   235→  } catch (error) {
   236→    console.error('Notion snapshot error:', error);
   237→    res.status(500).json({ error: 'Failed to generate snapshot' });
   238→  }
   239→});
   240→
   241→/**
   242→ * GET /api/notion/weekly-summary
   243→ * Get weekly summary for Notion metrics dashboard
   244→ */
   245→router.get('/weekly-summary', async (req, res) => {
   246→  try {
   247→    if (!db) {
   248→      return res.status(503).json({ error: 'Database not configured' });
   249→    }
   250→
   251→    const now = new Date();
   252→    const weekStart = getWeekStartAEST();
   253→
   254→    // Get week's communications
   255→    const { data: comms } = await db
   256→      .from('communications')
   257→      .select('type, metadata, created_at, handled_by, user_id')
   258→      .gte('created_at', weekStart.toISOString());
   259→
   260→    // Get week's conversions with lead details
   261→    const { data: conversions } = await db
   262→      .from('conversions')
   263→      .select(`
   264→        value,
   265→        converted_at,
   266→        lead:leads(trade, source)
   267→      `)
   268→      .gte('converted_at', weekStart.toISOString());
   269→
   270→    // Calculate stats
   271→    const calls = (comms || []).filter(c => c.type === 'call' || c.type === 'call_outbound');
   272→    const conversations = calls.filter(c => {
   273→      const outcome = c.metadata?.outcome;
   274→      return outcome && !['no_answer', 'voicemail', 'busy', 'wrong_number'].includes(outcome);
   275→    });
   276→
   277→    // Find patterns
   278→    const tradeCount = {};
   279→    const sourceCount = {};
   280→    (conversions || []).forEach(conv => {
   281→      if (conv.lead?.trade) {
   282→        tradeCount[conv.lead.trade] = (tradeCount[conv.lead.trade] || 0) + 1;
   283→      }
   284→      if (conv.lead?.source) {
   285→        sourceCount[conv.lead.source] = (sourceCount[conv.lead.source] || 0) + 1;
   286→      }
   287→    });
   288→
   289→    const bestTrade = Object.entries(tradeCount).sort((a, b) => b[1] - a[1])[0];
   290→    const bestSource = Object.entries(sourceCount).sort((a, b) => b[1] - a[1])[0];
   291→
   292→    res.json({
   293→      weekOf: weekStart.toISOString().split('T')[0],
   294→      generatedAt: now.toISOString(),
   295→
   296→      activity: {
   297→        totalCalls: calls.length,
   298→        conversations: conversations.length,
   299→        conversionRate: calls.length > 0 ? ((conversations.length / calls.length) * 100).toFixed(1) + '%' : '0%',
   300→        smsOut: (comms || []).filter(c => c.type === 'sms_outbound').length,
   301→        smsIn: (comms || []).filter(c => c.type === 'sms_inbound').length
   302→      },
   303→
   304→      conversions: {
   305→        count: (conversions || []).length,
   306→        revenue: (conversions || []).reduce((sum, c) => sum + (parseFloat(c.value) || 0), 0)
   307→      },
   308→
   309→      patterns: {
   310→        bestTrade: bestTrade ? bestTrade[0] : null,
   311→        bestSource: bestSource ? bestSource[0] : null,
   312→        tradeBreakdown: tradeCount,
   313→        sourceBreakdown: sourceCount
   314→      },
   315→
   316→      // Ready-to-paste summary
   317→      notionSummary: `Week of ${weekStart.toISOString().split('T')[0]}: ${calls.length} calls, ${conversations.length} conversations (${calls.length > 0 ? ((conversations.length / calls.length) * 100).toFixed(0) : 0}% connect rate). ${(conversions || []).length} conversions ($${(conversions || []).reduce((sum, c) => sum + (parseFloat(c.value) || 0), 0).toLocaleString()}). Best trade: ${bestTrade ? bestTrade[0] : 'N/A'}.`
   318→    });
   319→
   320→  } catch (error) {
   321→    console.error('Weekly summary error:', error);
   322→    res.status(500).json({ error: 'Failed to generate weekly summary' });
   323→  }
   324→});
   325→
   326→/**
   327→ * GET /api/notion/knowledge-export
   328→ * Export knowledge base items for Notion
   329→ */
   330→router.get('/knowledge-export', async (req, res) => {
   331→  try {
   332→    if (!db) {
   333→      return res.status(503).json({ error: 'Database not configured' });
   334→    }
   335→
   336→    // Get winning patterns
   337→    const { data: patterns } = await db
   338→      .from('winning_patterns')
   339→      .select('*')
   340→      .eq('is_active', true)
   341→      .order('effectiveness', { ascending: false })
   342→      .limit(20);
   343→
   344→    // Get proven objection responses
   345→    const { data: objections } = await db
   346→      .from('objection_responses')
   347→      .select('*')
   348→      .gte('effectiveness', 0.6)
   349→      .order('times_used', { ascending: false })
   350→      .limit(20);
   351→
   352→    res.json({
   353→      generatedAt: new Date().toISOString(),
   354→
   355→      winningPatterns: (patterns || []).map(p => ({
   356→        type: p.pattern_type,
   357→        description: p.description,
   358→        recommendation: p.recommendation,
   359→        effectiveness: p.effectiveness,
   360→        category: 'Sales Pattern'
   361→      })),
   362→
   363→      objectionResponses: (objections || []).map(o => ({
   364→        objection: o.objection_text,
   365→        response: o.response_text,
   366→        effectiveness: o.effectiveness,
   367→        timesUsed: o.times_used,
   368→        category: 'Objection Handling'
   369→      })),
   370→
   371→      // Static knowledge base items
   372→      brandVoice: [
   373→        { rule: "Never use 'hire employees'", use: "connect with workers" },
   374→        { rule: "Never use 'our workers'", use: "workers on the platform" },
   375→        { rule: "Never use 'guaranteed'", use: "helps ensure" },
   376→        { rule: "Always mention workers set their own rate", use: "workers set their rate, no middleman markup" }
   377→      ],
   378→
   379→      legalLanguage: {
   380→        bannedWords: ["hire employees", "staff", "workforce", "employ", "our workers", "our crew", "guaranteed", "ensure safety", "we manage"],
   381→        requiredWords: ["connect", "match", "network", "find", "platform", "marketplace", "workers set their rate"],
   382→        reason: "RateRight is platform/facilitator, NOT employer. Wrong language = legal liability."
   383→      }
   384→    });
   385→
   386→  } catch (error) {
   387→    console.error('Knowledge export error:', error);
   388→    res.status(500).json({ error: 'Failed to export knowledge' });
   389→  }
   390→});
   391→
   392→/**
   393→ * POST /api/notion/sync-business-state
   394→ * Push today's business state to Notion
   395→ */
   396→router.post('/sync-business-state', async (req, res) => {
   397→  try {
   398→    if (!NOTION_API_KEY || !NOTION_BUSINESS_STATE_DB) {
   399→      return res.status(503).json({
   400→        error: 'Notion not configured',
   401→        missing: !NOTION_API_KEY ? 'NOTION_API_KEY' : 'NOTION_BUSINESS_STATE_DB'
   402→      });
   403→    }
   404→
   405→    if (!db) {
   406→      return res.status(503).json({ error: 'Database not configured' });
   407→    }
   408→
   409→    // Get snapshot data
   410→    const now = new Date();
   411→    const todayStart = getTodayStartAEST();
   412→    const weekStart = getWeekStartAEST();
   413→    const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
   414→    const dateStr = now.toISOString().split('T')[0];
   415→
   416→    // Run parallel queries
   417→    const [leadsResult, todayCommsResult, conversionsResult, callbacksResult] = await Promise.all([
   418→      db.from('leads')
   419→        .select('id, score, status, estimated_value')
   420→        .is('deleted_at', null)
   421→        .not('status', 'in', '("converted","opted_out","archived")'),
   422→      db.from('communications')
   423→        .select('type, metadata')
   424→        .gte('created_at', todayStart.toISOString()),
   425→      db.from('conversions')
   426→        .select('value')
   427→        .gte('converted_at', monthStart.toISOString()),
   428→      db.from('callbacks')
   429→        .select('id')
   430→        .eq('status', 'pending')
   431→    ]);
   432→
   433→    const leads = leadsResult.data || [];
   434→    const todayComms = todayCommsResult.data || [];
   435→    const conversions = conversionsResult.data || [];
   436→
   437→    // Calculate stats
   438→    const pipeline = {
   439→      total: leads.length,
   440→      hot: leads.filter(l => getScoreTier(l.score) === 'hot').length,
   441→      warm: leads.filter(l => getScoreTier(l.score) === 'warm').length,
   442→      totalValue: leads.reduce((sum, l) => sum + (parseFloat(l.estimated_value) || 0), 0)
   443→    };
   444→
   445→    const todayCalls = todayComms.filter(c => c.type === 'call' || c.type === 'call_outbound').length;
   446→    const todayConversations = todayComms.filter(c => {
   447→      if (c.type !== 'call' && c.type !== 'call_outbound') return false;
   448→      const outcome = c.metadata?.outcome;
   449→      return outcome && !['no_answer', 'voicemail', 'busy', 'wrong_number'].includes(outcome);
   450→    }).length;
   451→    const todaySms = todayComms.filter(c => c.type === 'sms_outbound').length;
   452→
   453→    const monthConversions = conversions.length;
   454→    const monthRevenue = conversions.reduce((sum, c) => sum + (parseFloat(c.value) || 0), 0);
   455→    const pendingCallbacks = callbacksResult.data?.length || 0;
   456→
   457→    // Determine status
   458→    const status = todayCalls >= 10 ? 'Green' : todayCalls >= 5 ? 'Yellow' : 'Red';
   459→
   460→    // Create Notion page
   461→    const notionPage = await notionRequest('/pages', 'POST', {
   462→      parent: { database_id: NOTION_BUSINESS_STATE_DB },
   463→      properties: {
   464→        'Name': {
   465→          title: [{ text: { content: dateStr } }]
   466→        },
   467→        'Date': {
   468→          date: { start: dateStr }
   469→        },
   470→        'Total Leads': {
   471→          number: pipeline.total
   472→        },
   473→        'Hot Leads': {
   474→          number: pipeline.hot
   475→        },
   476→        'Warm Leads': {
   477→          number: pipeline.warm
   478→        },
   479→        'Pipeline Value': {
   480→          number: pipeline.totalValue
   481→        },
   482→        'Calls Today': {
   483→          number: todayCalls
   484→        },
   485→        'Conversations': {
   486→          number: todayConversations
   487→        },
   488→        'SMS Sent': {
   489→          number: todaySms
   490→        },
   491→        'Month Conversions': {
   492→          number: monthConversions
   493→        },
   494→        'Month Revenue': {
   495→          number: monthRevenue
   496→        },
   497→        'Pending Callbacks': {
   498→          number: pendingCallbacks
   499→        },
   500→        'Status': {
   501→          select: { name: status }
   502→        }
   503→      }
   504→    });
   505→
   506→    res.json({
   507→      success: true,
   508→      notionPageId: notionPage.id,
   509→      date: dateStr,
   510→      summary: {
   511→        pipeline: pipeline.total,
   512→        hot: pipeline.hot,
   513→        calls: todayCalls,
   514→        conversations: todayConversations,
   515→        status
   516→      }
   517→    });
   518→
   519→  } catch (error) {
   520→    console.error('Notion sync error:', error);
   521→    res.status(500).json({ error: error.message || 'Failed to sync to Notion' });
   522→  }
   523→});
   524→
   525→/**
   526→ * POST /api/notion/sync-weekly-metrics
   527→ * Push weekly metrics to Notion
   528→ */
   529→router.post('/sync-weekly-metrics', async (req, res) => {
   530→  try {
   531→    if (!NOTION_API_KEY || !NOTION_METRICS_DB) {
   532→      return res.status(503).json({
   533→        error: 'Notion not configured',
   534→        missing: !NOTION_API_KEY ? 'NOTION_API_KEY' : 'NOTION_METRICS_DB'
   535→      });
   536→    }
   537→
   538→    if (!db) {
   539→      return res.status(503).json({ error: 'Database not configured' });
   540→    }
   541→
   542→    const now = new Date();
   543→    const weekStart = getWeekStartAEST();
   544→    const weekNum = Math.ceil((now.getDate() + new Date(now.getFullYear(), now.getMonth(), 1).getDay()) / 7);
   545→    const periodName = `Week ${weekNum} - ${now.toLocaleString('default', { month: 'short' })} ${now.getFullYear()}`;
   546→
   547→    // Get week's data
   548→    const [commsResult, conversionsResult] = await Promise.all([
   549→      db.from('communications')
   550→        .select('type, metadata')
   551→        .gte('created_at', weekStart.toISOString()),
   552→      db.from('conversions')
   553→        .select('value, lead:leads(trade, source)')
   554→        .gte('converted_at', weekStart.toISOString())
   555→    ]);
   556→
   557→    const comms = commsResult.data || [];
   558→    const conversions = conversionsResult.data || [];
   559→
   560→    const calls = comms.filter(c => c.type === 'call' || c.type === 'call_outbound').length;
   561→    const conversations = comms.filter(c => {
   562→      if (c.type !== 'call' && c.type !== 'call_outbound') return false;
   563→      const outcome = c.metadata?.outcome;
   564→      return outcome && !['no_answer', 'voicemail', 'busy', 'wrong_number'].includes(outcome);
   565→    }).length;
   566→    const smsOut = comms.filter(c => c.type === 'sms_outbound').length;
   567→    const smsIn = comms.filter(c => c.type === 'sms_inbound').length;
   568→    const revenue = conversions.reduce((sum, c) => sum + (parseFloat(c.value) || 0), 0);
   569→
   570→    // Find best trade
   571→    const tradeCount = {};
   572→    conversions.forEach(c => {
   573→      if (c.lead?.trade) {
   574→        tradeCount[c.lead.trade] = (tradeCount[c.lead.trade] || 0) + 1;
   575→      }
   576→    });
   577→    const bestTrade = Object.entries(tradeCount).sort((a, b) => b[1] - a[1])[0];
   578→
   579→    // Create Notion page
   580→    const notionPage = await notionRequest('/pages', 'POST', {
   581→      parent: { database_id: NOTION_METRICS_DB },
   582→      properties: {
   583→        'Name': {
   584→          title: [{ text: { content: periodName } }]
   585→        },
   586→        'Week Start': {
   587→          date: { start: weekStart.toISOString().split('T')[0] }
   588→        },
   589→        'Total Calls': {
   590→          number: calls
   591→        },
   592→        'Conversations': {
   593→          number: conversations
   594→        },
   595→        'Connect Rate': {
   596→          number: calls > 0 ? Math.round((conversations / calls) * 100) : 0
   597→        },
   598→        'SMS Sent': {
   599→          number: smsOut
   600→        },
   601→        'SMS Received': {
   602→          number: smsIn
   603→        },
   604→        'Conversions': {
   605→          number: conversions.length
   606→        },
   607→        'Revenue': {
   608→          number: revenue
   609→        },
   610→        'Best Trade': {
   611→          rich_text: [{ text: { content: bestTrade ? bestTrade[0] : 'N/A' } }]
   612→        }
   613→      }
   614→    });
   615→
   616→    res.json({
   617→      success: true,
   618→      notionPageId: notionPage.id,
   619→      period: periodName,
   620→      summary: {
   621→        calls,
   622→        conversations,
   623→        conversions: conversions.length,
   624→        revenue,
   625→        bestTrade: bestTrade ? bestTrade[0] : null
   626→      }
   627→    });
   628→
   629→  } catch (error) {
   630→    console.error('Notion weekly sync error:', error);
   631→    res.status(500).json({ error: error.message || 'Failed to sync weekly metrics' });
   632→  }
   633→});
   634→
   635→/**
   636→ * GET /api/notion/status
   637→ * Check Notion integration status
   638→ */
   639→router.get('/status', async (req, res) => {
   640→  const configured = {
   641→    apiKey: !!NOTION_API_KEY,
   642→    businessStateDb: !!NOTION_BUSINESS_STATE_DB,
   643→    metricsDb: !!NOTION_METRICS_DB,
   644→    knowledgeDb: !!NOTION_KNOWLEDGE_DB
   645→  };
   646→
   647→  res.json({
   648→    configured,
   649→    ready: configured.apiKey && configured.businessStateDb,
   650→    message: configured.apiKey ? 'Notion integration ready' : 'NOTION_API_KEY not set'
   651→  });
   652→});
   653→
   654→module.exports = router;
   655→

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


Let me continue reading the remaining files.


     1→/**
     2→ * Voicemail Drop Routes
     3→ * Manage pre-recorded voicemails and drop them on active calls
     4→ */
     5→
     6→const express = require('express');
     7→const router = express.Router();
     8→const multer = require('multer');
     9→const { supabase } = require('../config/database');
    10→const twilio = require('twilio');
    11→
    12→// Configure multer for audio uploads (memory storage)
    13→const upload = multer({
    14→  storage: multer.memoryStorage(),
    15→  limits: {
    16→    fileSize: 10 * 1024 * 1024, // 10MB max
    17→  },
    18→  fileFilter: (req, file, cb) => {
    19→    // Accept audio files
    20→    if (file.mimetype.startsWith('audio/')) {
    21→      cb(null, true);
    22→    } else {
    23→      cb(new Error('Only audio files are allowed'), false);
    24→    }
    25→  }
    26→});
    27→
    28→/**
    29→ * GET /api/voicemail/recordings
    30→ * List user's recordings + shared recordings
    31→ */
    32→router.get('/recordings', async (req, res) => {
    33→  try {
    34→    if (!supabase) {
    35→      return res.status(500).json({ error: 'Database not configured' });
    36→    }
    37→
    38→    // Get user's own recordings + shared recordings
    39→    const { data, error } = await supabase
    40→      .from('voicemail_recordings')
    41→      .select('id, name, description, recording_url, duration_seconds, is_default, is_shared, usage_count, created_at')
    42→      .or(`user_id.is.null,is_shared.eq.true`)
    43→      .order('is_default', { ascending: false })
    44→      .order('usage_count', { ascending: false });
    45→
    46→    if (error) {
    47→      console.error('Get recordings error:', error);
    48→      return res.status(500).json({ error: error.message });
    49→    }
    50→
    51→    res.json(data || []);
    52→  } catch (err) {
    53→    console.error('Recordings list error:', err);
    54→    res.status(500).json({ error: 'Failed to list recordings' });
    55→  }
    56→});
    57→
    58→/**
    59→ * POST /api/voicemail/recordings
    60→ * Upload a new voicemail recording
    61→ * Accepts multipart/form-data with 'audio' file
    62→ */
    63→router.post('/recordings', upload.single('audio'), async (req, res) => {
    64→  try {
    65→    const { name, description, recordingUrl } = req.body;
    66→
    67→    if (!name) {
    68→      return res.status(400).json({ error: 'Name is required' });
    69→    }
    70→
    71→    if (!supabase) {
    72→      return res.status(500).json({ error: 'Database not configured' });
    73→    }
    74→
    75→    let finalRecordingUrl = recordingUrl;
    76→
    77→    // If audio file uploaded, store in Supabase Storage
    78→    if (req.file) {
    79→      const fileName = `vm_${Date.now()}_${Math.random().toString(36).substring(7)}.mp3`;
    80→
    81→      const { data: uploadData, error: uploadError } = await supabase.storage
    82→        .from('voicemail-recordings')
    83→        .upload(fileName, req.file.buffer, {
    84→          contentType: req.file.mimetype,
    85→          upsert: false
    86→        });
    87→
    88→      if (uploadError) {
    89→        console.error('Upload error:', uploadError);
    90→        // If bucket doesn't exist, create it
    91→        if (uploadError.message?.includes('not found')) {
    92→          return res.status(500).json({
    93→            error: 'Storage bucket not configured. Create "voicemail-recordings" bucket in Supabase Storage.'
    94→          });
    95→        }
    96→        return res.status(500).json({ error: 'Failed to upload audio' });
    97→      }
    98→
    99→      // Get public URL
   100→      const { data: urlData } = supabase.storage
   101→        .from('voicemail-recordings')
   102→        .getPublicUrl(fileName);
   103→
   104→      finalRecordingUrl = urlData.publicUrl;
   105→    }
   106→
   107→    if (!finalRecordingUrl) {
   108→      return res.status(400).json({ error: 'Audio file or recordingUrl required' });
   109→    }
   110→
   111→    // Save to database
   112→    const { data, error } = await supabase
   113→      .from('voicemail_recordings')
   114→      .insert({
   115→        user_id: null, // For now, all recordings are shared
   116→        name,
   117→        description: description || null,
   118→        recording_url: finalRecordingUrl,
   119→        duration_seconds: req.body.duration ? parseInt(req.body.duration) : null,
   120→        is_shared: true
   121→      })
   122→      .select()
   123→      .single();
   124→
   125→    if (error) {
   126→      console.error('Save recording error:', error);
   127→      return res.status(500).json({ error: error.message });
   128→    }
   129→
   130→    res.json(data);
   131→  } catch (err) {
   132→    console.error('Create recording error:', err);
   133→    res.status(500).json({ error: 'Failed to create recording' });
   134→  }
   135→});
   136→
   137→/**
   138→ * DELETE /api/voicemail/recordings/:id
   139→ * Delete a recording
   140→ */
   141→router.delete('/recordings/:id', async (req, res) => {
   142→  try {
   143→    const { id } = req.params;
   144→
   145→    if (!supabase) {
   146→      return res.status(500).json({ error: 'Database not configured' });
   147→    }
   148→
   149→    // Get the recording first to get the file URL
   150→    const { data: recording, error: fetchError } = await supabase
   151→      .from('voicemail_recordings')
   152→      .select('recording_url')
   153→      .eq('id', id)
   154→      .single();
   155→
   156→    if (fetchError || !recording) {
   157→      return res.status(404).json({ error: 'Recording not found' });
   158→    }
   159→
   160→    // Delete from database
   161→    const { error } = await supabase
   162→      .from('voicemail_recordings')
   163→      .delete()
   164→      .eq('id', id);
   165→
   166→    if (error) {
   167→      console.error('Delete recording error:', error);
   168→      return res.status(500).json({ error: error.message });
   169→    }
   170→
   171→    // Try to delete from storage (ignore errors - file might be external)
   172→    if (recording.recording_url?.includes('voicemail-recordings')) {
   173→      const fileName = recording.recording_url.split('/').pop();
   174→      await supabase.storage
   175→        .from('voicemail-recordings')
   176→        .remove([fileName])
   177→        .catch(() => {}); // Ignore storage errors
   178→    }
   179→
   180→    res.json({ success: true });
   181→  } catch (err) {
   182→    console.error('Delete recording error:', err);
   183→    res.status(500).json({ error: 'Failed to delete recording' });
   184→  }
   185→});
   186→
   187→/**
   188→ * PATCH /api/voicemail/recordings/:id/default
   189→ * Set a recording as default
   190→ */
   191→router.patch('/recordings/:id/default', async (req, res) => {
   192→  try {
   193→    const { id } = req.params;
   194→
   195→    if (!supabase) {
   196→      return res.status(500).json({ error: 'Database not configured' });
   197→    }
   198→
   199→    // Clear any existing default
   200→    await supabase
   201→      .from('voicemail_recordings')
   202→      .update({ is_default: false })
   203→      .eq('is_default', true);
   204→
   205→    // Set new default
   206→    const { data, error } = await supabase
   207→      .from('voicemail_recordings')
   208→      .update({ is_default: true })
   209→      .eq('id', id)
   210→      .select()
   211→      .single();
   212→
   213→    if (error) {
   214→      console.error('Set default error:', error);
   215→      return res.status(500).json({ error: error.message });
   216→    }
   217→
   218→    res.json(data);
   219→  } catch (err) {
   220→    console.error('Set default error:', err);
   221→    res.status(500).json({ error: 'Failed to set default' });
   222→  }
   223→});
   224→
   225→/**
   226→ * POST /api/voicemail/drop
   227→ * Drop a voicemail on an active call
   228→ * Uses Twilio's call modification to play audio then hang up
   229→ */
   230→router.post('/drop', async (req, res) => {
   231→  try {
   232→    const { callSid, recordingId, leadId } = req.body;
   233→
   234→    if (!callSid) {
   235→      return res.status(400).json({ error: 'callSid is required' });
   236→    }
   237→
   238→    if (!recordingId) {
   239→      return res.status(400).json({ error: 'recordingId is required' });
   240→    }
   241→
   242→    if (!supabase) {
   243→      return res.status(500).json({ error: 'Database not configured' });
   244→    }
   245→
   246→    // Get recording URL
   247→    const { data: recording, error: recError } = await supabase
   248→      .from('voicemail_recordings')
   249→      .select('id, name, recording_url')
   250→      .eq('id', recordingId)
   251→      .single();
   252→
   253→    if (recError || !recording) {
   254→      return res.status(404).json({ error: 'Recording not found' });
   255→    }
   256→
   257→    // Initialize Twilio client
   258→    const accountSid = process.env.TWILIO_ACCOUNT_SID;
   259→    const authToken = process.env.TWILIO_AUTH_TOKEN;
   260→
   261→    if (!accountSid || !authToken) {
   262→      return res.status(500).json({ error: 'Twilio not configured' });
   263→    }
   264→
   265→    const client = twilio(accountSid, authToken);
   266→
   267→    // Modify the call to play the recording then hang up
   268→    console.log(`[Voicemail Drop] Playing ${recording.name} on call ${callSid}`);
   269→
   270→    await client.calls(callSid).update({
   271→      twiml: `<Response><Play>${recording.recording_url}</Play><Hangup/></Response>`
   272→    });
   273→
   274→    // Log the drop
   275→    await supabase.from('voicemail_drops').insert({
   276→      recording_id: recordingId,
   277→      lead_id: leadId || null,
   278→      user_id: null, // TODO: Get from auth
   279→      call_sid: callSid
   280→    });
   281→
   282→    // Increment usage count
   283→    await supabase
   284→      .from('voicemail_recordings')
   285→      .update({ usage_count: recording.usage_count + 1 || 1 })
   286→      .eq('id', recordingId);
   287→
   288→    console.log(`[Voicemail Drop] Successfully dropped "${recording.name}"`);
   289→
   290→    res.json({
   291→      success: true,
   292→      message: `Voicemail "${recording.name}" dropped`,
   293→      recordingName: recording.name
   294→    });
   295→
   296→  } catch (err) {
   297→    console.error('Drop voicemail error:', err);
   298→    res.status(500).json({ error: 'Failed to drop voicemail: ' + err.message });
   299→  }
   300→});
   301→
   302→/**
   303→ * GET /api/voicemail/drops/stats
   304→ * Get voicemail drop statistics
   305→ */
   306→router.get('/drops/stats', async (req, res) => {
   307→  try {
   308→    if (!supabase) {
   309→      return res.status(500).json({ error: 'Database not configured' });
   310→    }
   311→
   312→    // Get drop count per recording
   313→    const { data, error } = await supabase
   314→      .from('voicemail_recordings')
   315→      .select('id, name, usage_count')
   316→      .order('usage_count', { ascending: false });
   317→
   318→    if (error) {
   319→      return res.status(500).json({ error: error.message });
   320→    }
   321→
   322→    // Get total drops today
   323→    const today = new Date();
   324→    today.setHours(0, 0, 0, 0);
   325→
   326→    const { count: todayDrops } = await supabase
   327→      .from('voicemail_drops')
   328→      .select('*', { count: 'exact', head: true })
   329→      .gte('dropped_at', today.toISOString());
   330→
   331→    res.json({
   332→      recordings: data || [],
   333→      totalDropsToday: todayDrops || 0
   334→    });
   335→  } catch (err) {
   336→    console.error('Stats error:', err);
   337→    res.status(500).json({ error: 'Failed to get stats' });
   338→  }
   339→});
   340→
   341→module.exports = router;
   342→

<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→const express = require('express');
     2→const router = express.Router();
     3→const { testConnection } = require('../config/database');
     4→
     5→/**
     6→ * GET /health
     7→ * Returns system health status
     8→ */
     9→router.get('/', async (req, res) => {
    10→  const startTime = Date.now();
    11→  const dbConnected = await testConnection();
    12→  const responseTime = Date.now() - startTime;
    13→
    14→  const health = {
    15→    status: dbConnected ? 'healthy' : 'degraded',
    16→    timestamp: new Date().toISOString(),
    17→    uptime: process.uptime(),
    18→    checks: {
    19→      api: 'ok',
    20→      database: dbConnected ? 'ok' : 'error',
    21→      openai: process.env.OPENAI_API_KEY ? 'configured' : 'missing',
    22→    },
    23→    responseTime: `${responseTime}ms`,
    24→    version: '1.0.1',
    25→    build: '2026-01-15-v2',
    26→    environment: process.env.NODE_ENV || 'development'
    27→  };
    28→
    29→  const statusCode = dbConnected ? 200 : 503;
    30→  res.status(statusCode).json(health);
    31→});
    32→
    33→/**
    34→ * GET /health/ping
    35→ * Simple ping endpoint for load balancers
    36→ */
    37→router.get('/ping', (req, res) => {
    38→  res.status(200).send('pong');
    39→});
    40→
    41→module.exports = router;
    42→

<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→ * User Management API routes
     3→ * Uses user_xp table as central user profile store
     4→ */
     5→
     6→const express = require('express');
     7→const router = express.Router();
     8→const { supabase, supabaseAdmin } = require('../config/database');
     9→
    10→// Use admin client if available (bypasses RLS)
    11→const db = supabaseAdmin || supabase;
    12→
    13→/**
    14→ * GET /api/users
    15→ * List all active users with roles
    16→ */
    17→router.get('/', async (req, res) => {
    18→  try {
    19→    if (!db) {
    20→      return res.status(503).json({ error: 'Database not configured' });
    21→    }
    22→
    23→    const { data: users, error } = await db
    24→      .from('user_xp')
    25→      .select('user_id, display_name, email, role, level, total_xp, is_active, streak')
    26→      .eq('is_active', true)
    27→      .order('display_name');
    28→
    29→    if (error) {
    30→      console.error('Users list error:', error.message);
    31→      return res.status(500).json({ error: 'Failed to fetch users' });
    32→    }
    33→
    34→    res.json({ users: users || [] });
    35→
    36→  } catch (error) {
    37→    console.error('Users list error:', error);
    38→    res.status(500).json({ error: 'Internal server error' });
    39→  }
    40→});
    41→
    42→/**
    43→ * GET /api/users/me
    44→ * Get current user's profile
    45→ */
    46→router.get('/me', async (req, res) => {
    47→  try {
    48→    if (!db) {
    49→      return res.status(503).json({ error: 'Database not configured' });
    50→    }
    51→
    52→    const userId = req.user?.id;
    53→    if (!userId) {
    54→      return res.status(401).json({ error: 'Not authenticated' });
    55→    }
    56→
    57→    const { data: profile, error } = await db
    58→      .from('user_xp')
    59→      .select('user_id, display_name, email, role, level, total_xp, is_active, streak')
    60→      .eq('user_id', userId)
    61→      .single();
    62→
    63→    if (error) {
    64→      // If no profile found, return basic info
    65→      if (error.code === 'PGRST116') {
    66→        return res.json({
    67→          user_id: userId,
    68→          email: req.user?.email,
    69→          display_name: req.user?.displayName || req.user?.email?.split('@')[0],
    70→          role: 'caller', // Default role
    71→          level: 1,
    72→          total_xp: 0,
    73→          is_active: true
    74→        });
    75→      }
    76→      console.error('User profile error:', error.message);
    77→      return res.status(500).json({ error: 'Failed to fetch profile' });
    78→    }
    79→
    80→    res.json(profile);
    81→
    82→  } catch (error) {
    83→    console.error('User profile error:', error);
    84→    res.status(500).json({ error: 'Internal server error' });
    85→  }
    86→});
    87→
    88→/**
    89→ * POST /api/users/sync
    90→ * Sync current user from auth.users to user_xp
    91→ * Called on login to ensure user profile exists
    92→ */
    93→router.post('/sync', async (req, res) => {
    94→  try {
    95→    if (!db) {
    96→      return res.status(503).json({ error: 'Database not configured' });
    97→    }
    98→
    99→    const userId = req.user?.id;
   100→    const email = req.user?.email;
   101→    const displayName = req.user?.displayName || email?.split('@')[0];
   102→
   103→    if (!userId || !email) {
   104→      return res.status(401).json({ error: 'Not authenticated' });
   105→    }
   106→
   107→    // Check if user_xp record exists
   108→    const { data: existing } = await db
   109→      .from('user_xp')
   110→      .select('user_id, email')
   111→      .eq('user_id', userId)
   112→      .single();
   113→
   114→    if (existing) {
   115→      // Update email if changed
   116→      if (existing.email !== email) {
   117→        await db
   118→          .from('user_xp')
   119→          .update({ email })
   120→          .eq('user_id', userId);
   121→      }
   122→      return res.json({ synced: true, created: false });
   123→    }
   124→
   125→    // Create new user_xp record
   126→    const { error: insertError } = await db
   127→      .from('user_xp')
   128→      .insert({
   129→        user_id: userId,
   130→        display_name: displayName,
   131→        email: email,
   132→        role: 'caller', // Default role for new users
   133→        level: 1,
   134→        total_xp: 0,
   135→        is_active: true
   136→      });
   137→
   138→    if (insertError) {
   139→      // Handle duplicate key (race condition)
   140→      if (insertError.code === '23505') {
   141→        return res.json({ synced: true, created: false });
   142→      }
   143→      console.error('User sync error:', insertError.message);
   144→      return res.status(500).json({ error: 'Failed to sync user' });
   145→    }
   146→
   147→    console.log(`User synced: ${email}`);
   148→    res.json({ synced: true, created: true });
   149→
   150→  } catch (error) {
   151→    console.error('User sync error:', error);
   152→    res.status(500).json({ error: 'Internal server error' });
   153→  }
   154→});
   155→
   156→/**
   157→ * GET /api/users/check/:email
   158→ * Check if an email is allowed (has active user_xp record)
   159→ * Used by AuthContext for database-driven whitelist
   160→ */
   161→router.get('/check/:email', async (req, res) => {
   162→  try {
   163→    if (!db) {
   164→      return res.status(503).json({ error: 'Database not configured' });
   165→    }
   166→
   167→    const { email } = req.params;
   168→    if (!email) {
   169→      return res.status(400).json({ error: 'Email required' });
   170→    }
   171→
   172→    const { data: profile, error } = await db
   173→      .from('user_xp')
   174→      .select('user_id, display_name, role, is_active')
   175→      .eq('email', email.toLowerCase())
   176→      .eq('is_active', true)
   177→      .single();
   178→
   179→    if (error || !profile) {
   180→      return res.json({ allowed: false });
   181→    }
   182→
   183→    res.json({
   184→      allowed: true,
   185→      user_id: profile.user_id,
   186→      display_name: profile.display_name,
   187→      role: profile.role
   188→    });
   189→
   190→  } catch (error) {
   191→    console.error('User check error:', error);
   192→    res.status(500).json({ error: 'Internal server error' });
   193→  }
   194→});
   195→
   196→/**
   197→ * GET /api/users/map
   198→ * Get user ID to display name mapping for UI
   199→ */
   200→router.get('/map', async (req, res) => {
   201→  try {
   202→    if (!db) {
   203→      return res.status(503).json({ error: 'Database not configured' });
   204→    }
   205→
   206→    const { data: users, error } = await db
   207→      .from('user_xp')
   208→      .select('user_id, display_name')
   209→      .eq('is_active', true);
   210→
   211→    if (error) {
   212→      console.error('Users map error:', error.message);
   213→      return res.status(500).json({ error: 'Failed to fetch users' });
   214→    }
   215→
   216→    const map = {};
   217→    (users || []).forEach(u => {
   218→      map[u.user_id] = u.display_name;
   219→    });
   220→
   221→    res.json({ map });
   222→
   223→  } catch (error) {
   224→    console.error('Users map error:', error);
   225→    res.status(500).json({ error: 'Internal server error' });
   226→  }
   227→});
   228→
   229→module.exports = router;
   230→

<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→ * Notifications Routes
     3→ * API endpoints for push notification management
     4→ */
     5→
     6→const express = require('express');
     7→const router = express.Router();
     8→const { supabase, supabaseAdmin } = require('../config/database');
     9→const { requireAuth } = require('../middleware/auth');
    10→const { sendPushNotification } = require('../services/pushNotifications');
    11→
    12→const db = supabaseAdmin || supabase;
    13→
    14→/**
    15→ * GET /api/notifications/vapid-public-key
    16→ * Get the VAPID public key for frontend subscription
    17→ * NOTE: This endpoint is PUBLIC (no auth required)
    18→ */
    19→router.get('/vapid-public-key', (req, res) => {
    20→  const publicKey = process.env.VAPID_PUBLIC_KEY;
    21→
    22→  if (!publicKey) {
    23→    return res.status(503).json({ error: 'Push notifications not configured' });
    24→  }
    25→
    26→  res.json({ publicKey });
    27→});
    28→
    29→/**
    30→ * POST /api/notifications/subscribe
    31→ * Register a push subscription for the current user
    32→ */
    33→router.post('/subscribe', requireAuth, async (req, res) => {
    34→  try {
    35→    const userId = req.user.id;
    36→    const { subscription } = req.body;
    37→
    38→    if (!subscription?.endpoint || !subscription?.keys) {
    39→      return res.status(400).json({ error: 'Invalid subscription - missing endpoint or keys' });
    40→    }
    41→
    42→    // Validate keys structure
    43→    if (!subscription.keys.p256dh || !subscription.keys.auth) {
    44→      return res.status(400).json({ error: 'Invalid subscription keys - missing p256dh or auth' });
    45→    }
    46→
    47→    // Upsert subscription (update if endpoint exists, insert if new)
    48→    const { error } = await db
    49→      .from('push_subscriptions')
    50→      .upsert({
    51→        user_id: userId,
    52→        endpoint: subscription.endpoint,
    53→        keys: subscription.keys,
    54→        user_agent: req.headers['user-agent'],
    55→        created_at: new Date().toISOString()
    56→      }, {
    57→        onConflict: 'user_id,endpoint'
    58→      });
    59→
    60→    if (error) {
    61→      console.error('[Notifications] Subscribe error:', error);
    62→      throw error;
    63→    }
    64→
    65→    console.log(`[Notifications] User ${userId} subscribed to push`);
    66→    res.json({ success: true });
    67→  } catch (err) {
    68→    console.error('[Notifications] Subscribe error:', err);
    69→    res.status(500).json({ error: err.message });
    70→  }
    71→});
    72→
    73→/**
    74→ * DELETE /api/notifications/unsubscribe
    75→ * Remove push subscription(s) for the current user
    76→ */
    77→router.delete('/unsubscribe', requireAuth, async (req, res) => {
    78→  try {
    79→    const userId = req.user.id;
    80→    const { endpoint } = req.body;
    81→
    82→    let query = db.from('push_subscriptions').delete().eq('user_id', userId);
    83→
    84→    // If specific endpoint provided, only delete that one
    85→    if (endpoint) {
    86→      query = query.eq('endpoint', endpoint);
    87→    }
    88→
    89→    const { error } = await query;
    90→
    91→    if (error) throw error;
    92→
    93→    console.log(`[Notifications] User ${userId} unsubscribed from push`);
    94→    res.json({ success: true });
    95→  } catch (err) {
    96→    console.error('[Notifications] Unsubscribe error:', err);
    97→    res.status(500).json({ error: err.message });
    98→  }
    99→});
   100→
   101→/**
   102→ * GET /api/notifications/preferences
   103→ * Get notification preferences for the current user
   104→ */
   105→router.get('/preferences', requireAuth, async (req, res) => {
   106→  try {
   107→    const userId = req.user.id;
   108→
   109→    const { data: prefs, error } = await db
   110→      .from('notification_preferences')
   111→      .select('*')
   112→      .eq('user_id', userId)
   113→      .single();
   114→
   115→    if (error && error.code !== 'PGRST116') { // PGRST116 = no rows found
   116→      throw error;
   117→    }
   118→
   119→    // Return defaults if no preferences exist
   120→    const defaults = {
   121→      high_intent_sms: true,
   122→      new_leads: true,
   123→      callbacks: true,
   124→      conversions: true,
   125→      sequence_replies: true,
   126→      daily_summary: false,
   127→      quiet_hours_start: null,
   128→      quiet_hours_end: null,
   129→      sms_fallback: false,
   130→      sms_fallback_phone: null
   131→    };
   132→
   133→    res.json(prefs || defaults);
   134→  } catch (err) {
   135→    console.error('[Notifications] Get preferences error:', err);
   136→    res.status(500).json({ error: err.message });
   137→  }
   138→});
   139→
   140→/**
   141→ * PUT /api/notifications/preferences
   142→ * Update notification preferences for the current user
   143→ */
   144→router.put('/preferences', requireAuth, async (req, res) => {
   145→  try {
   146→    const userId = req.user.id;
   147→    const updates = req.body;
   148→
   149→    // Remove any fields that shouldn't be updated directly
   150→    delete updates.id;
   151→    delete updates.user_id;
   152→    delete updates.created_at;
   153→
   154→    const { error } = await db
   155→      .from('notification_preferences')
   156→      .upsert({
   157→        user_id: userId,
   158→        ...updates,
   159→        updated_at: new Date().toISOString()
   160→      }, {
   161→        onConflict: 'user_id'
   162→      });
   163→
   164→    if (error) throw error;
   165→
   166→    console.log(`[Notifications] Updated preferences for user ${userId}`);
   167→    res.json({ success: true });
   168→  } catch (err) {
   169→    console.error('[Notifications] Update preferences error:', err);
   170→    res.status(500).json({ error: err.message });
   171→  }
   172→});
   173→
   174→/**
   175→ * POST /api/notifications/test
   176→ * Send a test notification to the current user
   177→ */
   178→router.post('/test', requireAuth, async (req, res) => {
   179→  try {
   180→    const userId = req.user.id;
   181→
   182→    const result = await sendPushNotification(userId, {
   183→      title: '🔔 Test Notification',
   184→      body: 'Push notifications are working!',
   185→      tag: 'test-' + Date.now(),
   186→      type: 'test',
   187→      url: '/dashboard'
   188→    });
   189→
   190→    res.json(result);
   191→  } catch (err) {
   192→    console.error('[Notifications] Test error:', err);
   193→    res.status(500).json({ error: err.message });
   194→  }
   195→});
   196→
   197→/**
   198→ * GET /api/notifications/status
   199→ * Get push notification status for current user
   200→ */
   201→router.get('/status', requireAuth, async (req, res) => {
   202→  try {
   203→    const userId = req.user.id;
   204→
   205→    // Count subscriptions
   206→    const { data: subs, error: subError } = await db
   207→      .from('push_subscriptions')
   208→      .select('id, created_at, last_used_at')
   209→      .eq('user_id', userId);
   210→
   211→    if (subError) throw subError;
   212→
   213→    // Get preferences
   214→    const { data: prefs } = await db
   215→      .from('notification_preferences')
   216→      .select('*')
   217→      .eq('user_id', userId)
   218→      .single();
   219→
   220→    res.json({
   221→      subscriptionCount: subs?.length || 0,
   222→      subscriptions: subs || [],
   223→      preferences: prefs || null,
   224→      vapidConfigured: !!process.env.VAPID_PUBLIC_KEY
   225→    });
   226→  } catch (err) {
   227→    console.error('[Notifications] Status error:', err);
   228→    res.status(500).json({ error: err.message });
   229→  }
   230→});
   231→
   232→module.exports = router;
   233→

<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→const express = require('express');
     2→const router = express.Router();
     3→const { supabase } = require('../config/database');
     4→const { validateUUID, errorResponse } = require('../utils/validation');
     5→const complianceService = require('../services/compliance');
     6→const aiService = require('../services/ai');
     7→
     8→// =====================================================
     9→// BRAND GUIDELINES ENDPOINTS
    10→// =====================================================
    11→
    12→/**
    13→ * GET /api/scripts/guidelines - Get active brand guidelines
    14→ */
    15→router.get('/guidelines', async (req, res) => {
    16→  try {
    17→    const { data, error } = await supabase
    18→      .from('brand_guidelines')
    19→      .select('*')
    20→      .eq('is_active', true)
    21→      .order('version', { ascending: false })
    22→      .limit(1)
    23→      .single();
    24→
    25→    if (error && error.code !== 'PGRST116') {
    26→      console.error('Error fetching brand guidelines:', error);
    27→      return res.status(500).json(errorResponse('Failed to fetch brand guidelines'));
    28→    }
    29→
    30→    res.json({
    31→      success: true,
    32→      guidelines: data || null
    33→    });
    34→  } catch (err) {
    35→    console.error('Brand guidelines error:', err);
    36→    res.status(500).json(errorResponse('Server error'));
    37→  }
    38→});
    39→
    40→/**
    41→ * PUT /api/scripts/guidelines - Update brand guidelines
    42→ */
    43→router.put('/guidelines', async (req, res) => {
    44→  try {
    45→    const {
    46→      tone_description,
    47→      tone_keywords,
    48→      banned_words,
    49→      banned_phrases,
    50→      required_signature,
    51→      must_include_cta,
    52→      max_exclamation_marks
    53→    } = req.body;
    54→
    55→    const userEmail = req.user?.email || 'unknown';
    56→
    57→    // Get current active guidelines for history
    58→    const { data: current } = await supabase
    59→      .from('brand_guidelines')
    60→      .select('*')
    61→      .eq('is_active', true)
    62→      .single();
    63→
    64→    // Deactivate old guidelines
    65→    if (current) {
    66→      await supabase
    67→        .from('brand_guidelines')
    68→        .update({ is_active: false })
    69→        .eq('id', current.id);
    70→    }
    71→
    72→    // Create new version
    73→    const newVersion = (current?.version || 0) + 1;
    74→
    75→    const { data: newGuidelines, error } = await supabase
    76→      .from('brand_guidelines')
    77→      .insert({
    78→        tone_description: tone_description || current?.tone_description,
    79→        tone_keywords: tone_keywords || current?.tone_keywords,
    80→        banned_words: banned_words || current?.banned_words,
    81→        banned_phrases: banned_phrases || current?.banned_phrases,
    82→        required_signature: required_signature || current?.required_signature,
    83→        must_include_cta: must_include_cta ?? current?.must_include_cta ?? true,
    84→        max_exclamation_marks: max_exclamation_marks ?? current?.max_exclamation_marks ?? 1,
    85→        version: newVersion,
    86→        is_active: true,
    87→        created_by: userEmail
    88→      })
    89→      .select()
    90→      .single();
    91→
    92→    if (error) {
    93→      console.error('Error updating brand guidelines:', error);
    94→      return res.status(500).json(errorResponse('Failed to update brand guidelines'));
    95→    }
    96→
    97→    // Log to history
    98→    await supabase.from('brand_guideline_history').insert({
    99→      guideline_id: newGuidelines.id,
   100→      changed_by: userEmail,
   101→      change_type: current ? 'update' : 'create',
   102→      old_values: current ? {
   103→        tone_description: current.tone_description,
   104→        tone_keywords: current.tone_keywords,
   105→        banned_words: current.banned_words,
   106→        banned_phrases: current.banned_phrases
   107→      } : null,
   108→      new_values: {
   109→        tone_description: newGuidelines.tone_description,
   110→        tone_keywords: newGuidelines.tone_keywords,
   111→        banned_words: newGuidelines.banned_words,
   112→        banned_phrases: newGuidelines.banned_phrases
   113→      }
   114→    });
   115→
   116→    res.json({
   117→      success: true,
   118→      guidelines: newGuidelines
   119→    });
   120→  } catch (err) {
   121→    console.error('Update guidelines error:', err);
   122→    res.status(500).json(errorResponse('Server error'));
   123→  }
   124→});
   125→
   126→/**
   127→ * GET /api/scripts/guidelines/history - Get guidelines change history
   128→ */
   129→router.get('/guidelines/history', async (req, res) => {
   130→  try {
   131→    const limit = parseInt(req.query.limit) || 20;
   132→
   133→    const { data, error } = await supabase
   134→      .from('brand_guideline_history')
   135→      .select('*')
   136→      .order('created_at', { ascending: false })
   137→      .limit(limit);
   138→
   139→    if (error) {
   140→      console.error('Error fetching guidelines history:', error);
   141→      return res.status(500).json(errorResponse('Failed to fetch history'));
   142→    }
   143→
   144→    res.json({
   145→      success: true,
   146→      history: data || []
   147→    });
   148→  } catch (err) {
   149→    console.error('Guidelines history error:', err);
   150→    res.status(500).json(errorResponse('Server error'));
   151→  }
   152→});
   153→
   154→// =====================================================
   155→// COMPLIANCE QUEUE ENDPOINTS
   156→// =====================================================
   157→
   158→/**
   159→ * POST /api/scripts/compliance/check - Check message compliance
   160→ */
   161→router.post('/compliance/check', async (req, res) => {
   162→  try {
   163→    const { message, options } = req.body;
   164→
   165→    if (!message) {
   166→      return res.status(400).json(errorResponse('Message is required'));
   167→    }
   168→
   169→    const result = await complianceService.checkCompliance(message, options || {});
   170→
   171→    res.json({
   172→      success: true,
   173→      ...result
   174→    });
   175→  } catch (err) {
   176→    console.error('Compliance check error:', err);
   177→    res.status(500).json(errorResponse('Server error'));
   178→  }
   179→});
   180→
   181→/**
   182→ * POST /api/scripts/compliance/auto-fix - Auto-fix message
   183→ */
   184→router.post('/compliance/auto-fix', async (req, res) => {
   185→  try {
   186→    const { message } = req.body;
   187→
   188→    if (!message) {
   189→      return res.status(400).json(errorResponse('Message is required'));
   190→    }
   191→
   192→    const result = await complianceService.autoFixMessage(message);
   193→
   194→    res.json({
   195→      success: true,
   196→      ...result
   197→    });
   198→  } catch (err) {
   199→    console.error('Auto-fix error:', err);
   200→    res.status(500).json(errorResponse('Server error'));
   201→  }
   202→});
   203→
   204→/**
   205→ * POST /api/scripts/compliance/validate/:id - Validate a script
   206→ */
   207→router.post('/compliance/validate/:id', async (req, res) => {
   208→  try {
   209→    const { id } = req.params;
   210→
   211→    if (!validateUUID(id)) {
   212→      return res.status(400).json(errorResponse('Invalid script ID'));
   213→    }
   214→
   215→    const result = await complianceService.validateScript(id);
   216→
   217→    res.json(result);
   218→  } catch (err) {
   219→    console.error('Script validation error:', err);
   220→    res.status(500).json(errorResponse('Server error'));
   221→  }
   222→});
   223→
   224→/**
   225→ * POST /api/scripts/compliance/batch-validate - Validate multiple scripts
   226→ */
   227→router.post('/compliance/batch-validate', async (req, res) => {
   228→  try {
   229→    const { scriptIds } = req.body;
   230→
   231→    if (!Array.isArray(scriptIds) || scriptIds.length === 0) {
   232→      return res.status(400).json(errorResponse('scriptIds array is required'));
   233→    }
   234→
   235→    if (scriptIds.length > 50) {
   236→      return res.status(400).json(errorResponse('Maximum 50 scripts per batch'));
   237→    }
   238→
   239→    const result = await complianceService.batchValidate(scriptIds);
   240→
   241→    res.json({
   242→      success: true,
   243→      ...result
   244→    });
   245→  } catch (err) {
   246→    console.error('Batch validation error:', err);
   247→    res.status(500).json(errorResponse('Server error'));
   248→  }
   249→});
   250→
   251→/**
   252→ * GET /api/scripts/compliance/queue - Get pending items
   253→ */
   254→router.get('/compliance/queue', async (req, res) => {
   255→  try {
   256→    const status = req.query.status || 'pending';
   257→    const limit = parseInt(req.query.limit) || 20;
   258→
   259→    const { data, error, count } = await supabase
   260→      .from('compliance_queue')
   261→      .select('*, leads(first_name, last_name, company)', { count: 'exact' })
   262→      .eq('status', status)
   263→      .order('created_at', { ascending: false })
   264→      .limit(limit);
   265→
   266→    if (error) {
   267→      console.error('Error fetching compliance queue:', error);
   268→      return res.status(500).json(errorResponse('Failed to fetch queue'));
   269→    }
   270→
   271→    res.json({
   272→      success: true,
   273→      queue: data || [],
   274→      total: count || 0
   275→    });
   276→  } catch (err) {
   277→    console.error('Compliance queue error:', err);
   278→    res.status(500).json(errorResponse('Server error'));
   279→  }
   280→});
   281→
   282→/**
   283→ * GET /api/scripts/compliance/stats - Review stats
   284→ */
   285→router.get('/compliance/stats', async (req, res) => {
   286→  try {
   287→    // Count by status in queue
   288→    const { data: statusCounts } = await supabase
   289→      .from('compliance_queue')
   290→      .select('status');
   291→
   292→    const counts = { pending: 0, approved: 0, rejected: 0, edited: 0 };
   293→    (statusCounts || []).forEach(item => {
   294→      counts[item.status] = (counts[item.status] || 0) + 1;
   295→    });
   296→
   297→    // Count scripts by compliance status
   298→    const { data: scriptCounts } = await supabase
   299→      .from('scripts')
   300→      .select('compliance_status')
   301→      .eq('is_active', true);
   302→
   303→    const scriptCompliance = { approved: 0, pending: 0, rejected: 0, needs_review: 0 };
   304→    (scriptCounts || []).forEach(script => {
   305→      const status = script.compliance_status || 'pending';
   306→      scriptCompliance[status] = (scriptCompliance[status] || 0) + 1;
   307→    });
   308→
   309→    // Calculate compliance score
   310→    const totalScripts = scriptCounts?.length || 0;
   311→    const approvedScripts = scriptCompliance.approved;
   312→    const complianceScore = totalScripts > 0
   313→      ? Math.round((approvedScripts / totalScripts) * 100)
   314→      : 100;
   315→
   316→    res.json({
   317→      success: true,
   318→      stats: {
   319→        queueCounts: counts,
   320→        scriptCompliance,
   321→        complianceScore,
   322→        totalScripts
   323→      }
   324→    });
   325→  } catch (err) {
   326→    console.error('Compliance stats error:', err);
   327→    res.status(500).json(errorResponse('Server error'));
   328→  }
   329→});
   330→
   331→/**
   332→ * POST /api/scripts/compliance/queue/:id/approve - Approve message
   333→ */
   334→router.post('/compliance/queue/:id/approve', async (req, res) => {
   335→  try {
   336→    const { id } = req.params;
   337→    const userEmail = req.user?.email || 'unknown';
   338→
   339→    if (!validateUUID(id)) {
   340→      return res.status(400).json(errorResponse('Invalid queue item ID'));
   341→    }
   342→
   343→    const { data, error } = await supabase
   344→      .from('compliance_queue')
   345→      .update({
   346→        status: 'approved',
   347→        reviewed_by: userEmail,
   348→        reviewed_at: new Date().toISOString()
   349→      })
   350→      .eq('id', id)
   351→      .select()
   352→      .single();
   353→
   354→    if (error) {
   355→      console.error('Error approving item:', error);
   356→      return res.status(500).json(errorResponse('Failed to approve'));
   357→    }
   358→
   359→    res.json({ success: true, item: data });
   360→  } catch (err) {
   361→    console.error('Approve error:', err);
   362→    res.status(500).json(errorResponse('Server error'));
   363→  }
   364→});
   365→
   366→/**
   367→ * POST /api/scripts/compliance/queue/:id/reject - Reject message
   368→ */
   369→router.post('/compliance/queue/:id/reject', async (req, res) => {
   370→  try {
   371→    const { id } = req.params;
   372→    const { reason } = req.body;
   373→    const userEmail = req.user?.email || 'unknown';
   374→
   375→    if (!validateUUID(id)) {
   376→      return res.status(400).json(errorResponse('Invalid queue item ID'));
   377→    }
   378→
   379→    const { data, error } = await supabase
   380→      .from('compliance_queue')
   381→      .update({
   382→        status: 'rejected',
   383→        reviewed_by: userEmail,
   384→        reviewed_at: new Date().toISOString(),
   385→        issues: reason ? [{ type: 'rejected', message: reason }] : undefined
   386→      })
   387→      .eq('id', id)
   388→      .select()
   389→      .single();
   390→
   391→    if (error) {
   392→      console.error('Error rejecting item:', error);
   393→      return res.status(500).json(errorResponse('Failed to reject'));
   394→    }
   395→
   396→    res.json({ success: true, item: data });
   397→  } catch (err) {
   398→    console.error('Reject error:', err);
   399→    res.status(500).json(errorResponse('Server error'));
   400→  }
   401→});
   402→
   403→/**
   404→ * POST /api/scripts/compliance/queue/:id/edit - Edit and approve
   405→ */
   406→router.post('/compliance/queue/:id/edit', async (req, res) => {
   407→  try {
   408→    const { id } = req.params;
   409→    const { final_content } = req.body;
   410→    const userEmail = req.user?.email || 'unknown';
   411→
   412→    if (!validateUUID(id)) {
   413→      return res.status(400).json(errorResponse('Invalid queue item ID'));
   414→    }
   415→
   416→    if (!final_content) {
   417→      return res.status(400).json(errorResponse('Final content is required'));
   418→    }
   419→
   420→    const { data, error } = await supabase
   421→      .from('compliance_queue')
   422→      .update({
   423→        status: 'edited',
   424→        final_content,
   425→        reviewed_by: userEmail,
   426→        reviewed_at: new Date().toISOString()
   427→      })
   428→      .eq('id', id)
   429→      .select()
   430→      .single();
   431→
   432→    if (error) {
   433→      console.error('Error editing item:', error);
   434→      return res.status(500).json(errorResponse('Failed to edit'));
   435→    }
   436→
   437→    res.json({ success: true, item: data });
   438→  } catch (err) {
   439→    console.error('Edit error:', err);
   440→    res.status(500).json(errorResponse('Server error'));
   441→  }
   442→});
   443→
   444→// =====================================================
   445→// SCRIPTS LIBRARY ENDPOINTS (Extended)
   446→// =====================================================
   447→
   448→// GET /api/scripts - List all scripts
   449→router.get('/', async (req, res) => {
   450→  try {
   451→    const { category, target_type, target_stage, active_only } = req.query;
   452→
   453→    let query = supabase
   454→      .from('scripts')
   455→      .select('*')
   456→      .order('times_used', { ascending: false });
   457→
   458→    if (category) {
   459→      query = query.eq('category', category);
   460→    }
   461→    if (target_type) {
   462→      query = query.eq('target_type', target_type);
   463→    }
   464→    if (target_stage) {
   465→      query = query.eq('target_stage', target_stage);
   466→    }
   467→    if (active_only === 'true') {
   468→      query = query.eq('is_active', true);
   469→    }
   470→
   471→    const { data: scripts, error } = await query;
   472→
   473→    if (error) throw error;
   474→
   475→    // Calculate success rate for each script
   476→    const scriptsWithStats = scripts.map(script => ({
   477→      ...script,
   478→      successRate: script.times_used > 0
   479→        ? Math.round((script.success_count / script.times_used) * 100)
   480→        : null
   481→    }));
   482→
   483→    res.json({ scripts: scriptsWithStats });
   484→  } catch (error) {
   485→    console.error('Error fetching scripts:', error);
   486→    res.status(500).json({ error: 'Failed to fetch scripts' });
   487→  }
   488→});
   489→
   490→// GET /api/scripts/:id - Get single script with objections
   491→router.get('/:id', async (req, res) => {
   492→  try {
   493→    const { id } = req.params;
   494→
   495→    // Get script
   496→    const { data: script, error: scriptError } = await supabase
   497→      .from('scripts')
   498→      .select('*')
   499→      .eq('id', id)
   500→      .single();
   501→
   502→    if (scriptError) throw scriptError;
   503→    if (!script) {
   504→      return res.status(404).json({ error: 'Script not found' });
   505→    }
   506→
   507→    // Get linked objections
   508→    const { data: scriptObjections } = await supabase
   509→      .from('script_objections')
   510→      .select(`
   511→        frequency,
   512→        objection:objections(*)
   513→      `)
   514→      .eq('script_id', id)
   515→      .order('frequency', { ascending: false });
   516→
   517→    // Get recent sessions for stats
   518→    const { data: sessions } = await supabase
   519→      .from('script_sessions')
   520→      .select('outcome, duration_seconds')
   521→      .eq('script_id', id)
   522→      .order('created_at', { ascending: false })
   523→      .limit(50);
   524→
   525→    // Calculate stats
   526→    const stats = {
   527→      totalUses: script.times_used,
   528→      successRate: script.times_used > 0
   529→        ? Math.round((script.success_count / script.times_used) * 100)
   530→        : null,
   531→      avgDuration: sessions?.length > 0
   532→        ? Math.round(sessions.filter(s => s.duration_seconds).reduce((sum, s) => sum + s.duration_seconds, 0) / sessions.filter(s => s.duration_seconds).length)
   533→        : null,
   534→      outcomeBreakdown: sessions?.reduce((acc, s) => {
   535→        if (s.outcome) {
   536→          acc[s.outcome] = (acc[s.outcome] || 0) + 1;
   537→        }
   538→        return acc;
   539→      }, {}) || {}
   540→    };
   541→
   542→    res.json({
   543→      script,
   544→      objections: scriptObjections?.map(so => ({
   545→        ...so.objection,
   546→        frequency: so.frequency
   547→      })) || [],
   548→      stats
   549→    });
   550→  } catch (error) {
   551→    console.error('Error fetching script:', error);
   552→    res.status(500).json({ error: 'Failed to fetch script' });
   553→  }
   554→});
   555→
   556→// POST /api/scripts - Create new script
   557→router.post('/', async (req, res) => {
   558→  try {
   559→    const {
   560→      name,
   561→      description,
   562→      category,
   563→      subcategory,
   564→      target_type,
   565→      target_stage,
   566→      target_score_min,
   567→      target_score_max,
   568→      content,
   569→      variables,
   570→      notes,
   571→      is_seed,
   572→      // Legacy fields for call scripts
   573→      opening,
   574→      discovery_questions,
   575→      value_props,
   576→      closing_techniques
   577→    } = req.body;
   578→
   579→    if (!name) {
   580→      return res.status(400).json({ error: 'Script name is required' });
   581→    }
   582→
   583→    // Validate category if provided
   584→    if (category) {
   585→      const validCategories = ['sms', 'call', 'email', 'objection'];
   586→      if (!validCategories.includes(category)) {
   587→        return res.status(400).json({ error: `Category must be one of: ${validCategories.join(', ')}` });
   588→      }
   589→    }
   590→
   591→    const userEmail = req.user?.email || 'unknown';
   592→
   593→    const { data: script, error } = await supabase
   594→      .from('scripts')
   595→      .insert({
   596→        name,
   597→        description,
   598→        category: category || 'call',
   599→        subcategory,
   600→        target_type: target_type || 'both',
   601→        target_stage,
   602→        target_score_min,
   603→        target_score_max,
   604→        content,
   605→        variables: variables || [],
   606→        notes,
   607→        is_seed: is_seed || false,
   608→        compliance_status: 'pending',
   609→        created_by: userEmail,
   610→        // Legacy fields
   611→        opening,
   612→        discovery_questions: discovery_questions || [],
   613→        value_props: value_props || [],
   614→        closing_techniques: closing_techniques || []
   615→      })
   616→      .select()
   617→      .single();
   618→
   619→    if (error) throw error;
   620→
   621→    res.status(201).json({ success: true, script });
   622→  } catch (error) {
   623→    console.error('Error creating script:', error);
   624→    res.status(500).json({ error: 'Failed to create script' });
   625→  }
   626→});
   627→
   628→// PATCH /api/scripts/:id - Update script
   629→router.patch('/:id', async (req, res) => {
   630→  try {
   631→    const { id } = req.params;
   632→    const updates = req.body;
   633→
   634→    // Remove fields that shouldn't be updated directly
   635→    delete updates.id;
   636→    delete updates.times_used;
   637→    delete updates.success_count;
   638→    delete updates.created_at;
   639→
   640→    updates.updated_at = new Date().toISOString();
   641→
   642→    const { data: script, error } = await supabase
   643→      .from('scripts')
   644→      .update(updates)
   645→      .eq('id', id)
   646→      .select()
   647→      .single();
   648→
   649→    if (error) throw error;
   650→
   651→    res.json({ script });
   652→  } catch (error) {
   653→    console.error('Error updating script:', error);
   654→    res.status(500).json({ error: 'Failed to update script' });
   655→  }
   656→});
   657→
   658→// DELETE /api/scripts/:id - Delete script
   659→router.delete('/:id', async (req, res) => {
   660→  try {
   661→    const { id } = req.params;
   662→
   663→    const { error } = await supabase
   664→      .from('scripts')
   665→      .delete()
   666→      .eq('id', id);
   667→
   668→    if (error) throw error;
   669→
   670→    res.status(204).send();
   671→  } catch (error) {
   672→    console.error('Error deleting script:', error);
   673→    res.status(500).json({ error: 'Failed to delete script' });
   674→  }
   675→});
   676→
   677→// POST /api/scripts/:id/use - Log script usage (start session)
   678→router.post('/:id/use', async (req, res) => {
   679→  try {
   680→    const { id } = req.params;
   681→    const { lead_id } = req.body;
   682→
   683→    // Create session
   684→    const { data: session, error: sessionError } = await supabase
   685→      .from('script_sessions')
   686→      .insert({
   687→        script_id: id,
   688→        lead_id,
   689→        started_at: new Date().toISOString()
   690→      })
   691→      .select()
   692→      .single();
   693→
   694→    if (sessionError) throw sessionError;
   695→
   696→    // Increment times_used
   697→    await supabase.rpc('increment_script_usage', { script_id: id });
   698→
   699→    res.status(201).json({ session });
   700→  } catch (error) {
   701→    console.error('Error starting script session:', error);
   702→    res.status(500).json({ error: 'Failed to start script session' });
   703→  }
   704→});
   705→
   706→// PATCH /api/scripts/sessions/:sessionId - End session with outcome
   707→router.patch('/sessions/:sessionId', async (req, res) => {
   708→  try {
   709→    const { sessionId } = req.params;
   710→    const { outcome, objections_encountered, notes } = req.body;
   711→
   712→    // Get session to calculate duration
   713→    const { data: existingSession } = await supabase
   714→      .from('script_sessions')
   715→      .select('started_at, script_id')
   716→      .eq('id', sessionId)
   717→      .single();
   718→
   719→    const duration_seconds = existingSession?.started_at
   720→      ? Math.round((Date.now() - new Date(existingSession.started_at).getTime()) / 1000)
   721→      : null;
   722→
   723→    const { data: session, error } = await supabase
   724→      .from('script_sessions')
   725→      .update({
   726→        ended_at: new Date().toISOString(),
   727→        duration_seconds,
   728→        outcome,
   729→        objections_encountered,
   730→        notes
   731→      })
   732→      .eq('id', sessionId)
   733→      .select()
   734→      .single();
   735→
   736→    if (error) throw error;
   737→
   738→    // If success, increment success_count
   739→    if (outcome === 'success' && existingSession?.script_id) {
   740→      await supabase.rpc('increment_script_success', { script_id: existingSession.script_id });
   741→    }
   742→
   743→    res.json({ session });
   744→  } catch (error) {
   745→    console.error('Error ending script session:', error);
   746→    res.status(500).json({ error: 'Failed to end script session' });
   747→  }
   748→});
   749→
   750→// GET /api/scripts/for-lead/:leadId - Get recommended script for a lead
   751→router.get('/for-lead/:leadId', async (req, res) => {
   752→  try {
   753→    const { leadId } = req.params;
   754→
   755→    // Get lead info
   756→    const { data: lead, error: leadError } = await supabase
   757→      .from('leads')
   758→      .select('lead_type, platform_stage')
   759→      .eq('id', leadId)
   760→      .single();
   761→
   762→    if (leadError) throw leadError;
   763→    if (!lead) {
   764→      return res.status(404).json({ error: 'Lead not found' });
   765→    }
   766→
   767→    // Find matching scripts
   768→    let query = supabase
   769→      .from('scripts')
   770→      .select('*')
   771→      .eq('is_active', true)
   772→      .order('times_used', { ascending: false });
   773→
   774→    // Match by type
   775→    if (lead.lead_type) {
   776→      query = query.or(`target_type.eq.${lead.lead_type},target_type.eq.both`);
   777→    }
   778→
   779→    // Match by stage
   780→    if (lead.platform_stage) {
   781→      query = query.eq('target_stage', lead.platform_stage);
   782→    }
   783→
   784→    const { data: scripts, error } = await query.limit(5);
   785→
   786→    if (error) throw error;
   787→
   788→    res.json({
   789→      lead: {
   790→        type: lead.lead_type,
   791→        stage: lead.platform_stage
   792→      },
   793→      recommendedScripts: scripts || []
   794→    });
   795→  } catch (error) {
   796→    console.error('Error finding scripts for lead:', error);
   797→    res.status(500).json({ error: 'Failed to find scripts' });
   798→  }
   799→});
   800→
   801→// GET /api/scripts/ai-recommend/:leadId - AI-powered script recommendation
   802→// Cache: 2 hours (stored in lead.metadata.ai_script_recommendation)
   803→router.get('/ai-recommend/:leadId', async (req, res) => {
   804→  try {
   805→    const { leadId } = req.params;
   806→    const forceRefresh = req.query.refresh === 'true';
   807→    const CACHE_HOURS = 2;
   808→
   809→    if (!validateUUID(leadId)) {
   810→      return res.status(400).json(errorResponse('Invalid lead ID'));
   811→    }
   812→
   813→    // Check cache first
   814→    if (!forceRefresh) {
   815→      const { data: lead } = await supabase
   816→        .from('leads')
   817→        .select('metadata')
   818→        .eq('id', leadId)
   819→        .single();
   820→
   821→      const cached = lead?.metadata?.ai_script_recommendation;
   822→      if (cached && cached.generated_at) {
   823→        const cacheAge = Date.now() - new Date(cached.generated_at).getTime();
   824→        const cacheHours = cacheAge / (1000 * 60 * 60);
   825→
   826→        if (cacheHours < CACHE_HOURS) {
   827→          console.log(`[Scripts] Using cached recommendation (${cacheHours.toFixed(1)}h old) for lead ${leadId}`);
   828→          return res.json({
   829→            success: true,
   830→            cached: true,
   831→            cacheAge: Math.round(cacheHours * 60), // minutes
   832→            ...cached.recommendation
   833→          });
   834→        }
   835→      }
   836→    }
   837→
   838→    console.log('[Scripts] AI recommend called for lead:', leadId);
   839→
   840→    const result = await aiService.recommendScriptForLead(leadId);
   841→
   842→    console.log('[Scripts] AI recommend result:', result?.success, result?.error);
   843→
   844→    if (!result) {
   845→      return res.status(500).json(errorResponse('AI service returned null'));
   846→    }
   847→
   848→    if (!result.success) {
   849→      return res.status(400).json(errorResponse(result.error || 'Unknown error'));
   850→    }
   851→
   852→    if (!result.recommendation) {
   853→      return res.status(500).json(errorResponse('AI returned no recommendation'));
   854→    }
   855→
   856→    // Cache the recommendation in lead metadata
   857→    const { data: currentLead } = await supabase
   858→      .from('leads')
   859→      .select('metadata')
   860→      .eq('id', leadId)
   861→      .single();
   862→
   863→    const updatedMetadata = {
   864→      ...(currentLead?.metadata || {}),
   865→      ai_script_recommendation: {
   866→        recommendation: result.recommendation,
   867→        generated_at: new Date().toISOString()
   868→      }
   869→    };
   870→
   871→    await supabase
   872→      .from('leads')
   873→      .update({ metadata: updatedMetadata })
   874→      .eq('id', leadId);
   875→
   876→    res.json({
   877→      success: true,
   878→      cached: false,
   879→      ...result.recommendation
   880→    });
   881→  } catch (error) {
   882→    console.error('AI script recommendation error:', error.message);
   883→    console.error('AI script recommendation stack:', error.stack);
   884→    res.status(500).json(errorResponse(`AI recommendation failed: ${error.message}`));
   885→  }
   886→});
   887→
   888→// ============ ANALYTICS ============
   889→
   890→// GET /api/scripts/analytics - Get script performance analytics
   891→router.get('/analytics/overview', async (req, res) => {
   892→  try {
   893→    const { days = 30 } = req.query;
   894→    const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
   895→
   896→    // Get all active scripts with their stats
   897→    const { data: scripts, error: scriptsError } = await supabase
   898→      .from('scripts')
   899→      .select('id, name, category, target_type, times_used, success_count')
   900→      .eq('is_active', true)
   901→      .order('times_used', { ascending: false });
   902→
   903→    if (scriptsError) throw scriptsError;
   904→
   905→    // Get recent sessions for detailed analytics
   906→    const { data: sessions, error: sessionsError } = await supabase
   907→      .from('script_sessions')
   908→      .select('script_id, outcome, duration_seconds, created_at')
   909→      .gte('created_at', since);
   910→
   911→    if (sessionsError) throw sessionsError;
   912→
   913→    // Calculate metrics
   914→    const totalSessions = sessions?.length || 0;
   915→    const successCount = sessions?.filter(s => s.outcome === 'success').length || 0;
   916→    const overallSuccessRate = totalSessions > 0 ? Math.round((successCount / totalSessions) * 100) : 0;
   917→
   918→    // Outcome distribution
   919→    const outcomeDistribution = sessions?.reduce((acc, s) => {
   920→      if (s.outcome) {
   921→        acc[s.outcome] = (acc[s.outcome] || 0) + 1;
   922→      }
   923→      return acc;
   924→    }, {}) || {};
   925→
   926→    // Script performance ranking
   927→    const scriptPerformance = scripts.map(script => {
   928→      const scriptSessions = sessions?.filter(s => s.script_id === script.id) || [];
   929→      const scriptSuccess = scriptSessions.filter(s => s.outcome === 'success').length;
   930→      const sessionCount = scriptSessions.length;
   931→      const avgDuration = scriptSessions.length > 0
   932→        ? Math.round(scriptSessions.filter(s => s.duration_seconds).reduce((sum, s) => sum + (s.duration_seconds || 0), 0) / scriptSessions.filter(s => s.duration_seconds).length)
   933→        : null;
   934→
   935→      return {
   936→        id: script.id,
   937→        name: script.name,
   938→        category: script.category,
   939→        targetType: script.target_type,
   940→        totalUses: script.times_used,
   941→        recentUses: sessionCount,
   942→        successRate: sessionCount > 0 ? Math.round((scriptSuccess / sessionCount) * 100) : null,
   943→        avgDuration,
   944→      };
   945→    }).sort((a, b) => {
   946→      // Sort by success rate (with uses), then by total uses
   947→      if (a.successRate !== null && b.successRate !== null) {
   948→        return b.successRate - a.successRate;
   949→      }
   950→      return b.totalUses - a.totalUses;
   951→    });
   952→
   953→    // Category performance
   954→    const categoryStats = scripts.reduce((acc, script) => {
   955→      const cat = script.category || 'other';
   956→      if (!acc[cat]) {
   957→        acc[cat] = { total: 0, success: 0, count: 0 };
   958→      }
   959→      acc[cat].total += script.times_used;
   960→      acc[cat].success += script.success_count || 0;
   961→      acc[cat].count += 1;
   962→      return acc;
   963→    }, {});
   964→
   965→    const categoryPerformance = Object.entries(categoryStats).map(([category, stats]) => ({
   966→      category,
   967→      scriptCount: stats.count,
   968→      totalUses: stats.total,
   969→      successRate: stats.total > 0 ? Math.round((stats.success / stats.total) * 100) : null,
   970→    }));
   971→
   972→    // Usage trend (daily for last N days)
   973→    const usageByDay = {};
   974→    sessions?.forEach(session => {
   975→      const day = session.created_at.split('T')[0];
   976→      usageByDay[day] = (usageByDay[day] || 0) + 1;
   977→    });
   978→
   979→    const usageTrend = Object.entries(usageByDay)
   980→      .sort(([a], [b]) => a.localeCompare(b))
   981→      .map(([date, count]) => ({ date, count }));
   982→
   983→    res.json({
   984→      summary: {
   985→        totalScripts: scripts.length,
   986→        totalSessions,
   987→        overallSuccessRate,
   988→        periodDays: parseInt(days),
   989→      },
   990→      outcomeDistribution,
   991→      scriptPerformance: scriptPerformance.slice(0, 10), // Top 10
   992→      categoryPerformance,
   993→      usageTrend,
   994→    });
   995→  } catch (error) {
   996→    console.error('Error fetching script analytics:', error);
   997→    res.status(500).json({ error: 'Failed to fetch script analytics' });
   998→  }
   999→});
  1000→
  1001→// ============ OBJECTIONS ============
  1002→
  1003→// GET /api/scripts/objections - List all objections
  1004→router.get('/objections/list', async (req, res) => {
  1005→  try {
  1006→    const { category, search } = req.query;
  1007→
  1008→    let query = supabase
  1009→      .from('objections')
  1010→      .select('*')
  1011→      .order('frequency', { ascending: false });
  1012→
  1013→    if (category) {
  1014→      query = query.eq('category', category);
  1015→    }
  1016→
  1017→    if (search) {
  1018→      query = query.ilike('objection', `%${search}%`);
  1019→    }
  1020→
  1021→    const { data: objections, error } = await query;
  1022→
  1023→    if (error) throw error;
  1024→
  1025→    res.json({ objections });
  1026→  } catch (error) {
  1027→    console.error('Error fetching objections:', error);
  1028→    res.status(500).json({ error: 'Failed to fetch objections' });
  1029→  }
  1030→});
  1031→
  1032→// GET /api/scripts/objections/:id - Get single objection
  1033→router.get('/objections/:id', async (req, res) => {
  1034→  try {
  1035→    const { id } = req.params;
  1036→
  1037→    const { data: objection, error } = await supabase
  1038→      .from('objections')
  1039→      .select('*')
  1040→      .eq('id', id)
  1041→      .single();
  1042→
  1043→    if (error) throw error;
  1044→
  1045→    res.json({ objection });
  1046→  } catch (error) {
  1047→    console.error('Error fetching objection:', error);
  1048→    res.status(500).json({ error: 'Failed to fetch objection' });
  1049→  }
  1050→});
  1051→
  1052→// POST /api/scripts/objections - Create objection
  1053→router.post('/objections', async (req, res) => {
  1054→  try {
  1055→    const { objection, category, responses, tags } = req.body;
  1056→
  1057→    if (!objection) {
  1058→      return res.status(400).json({ error: 'Objection text is required' });
  1059→    }
  1060→
  1061→    const { data, error } = await supabase
  1062→      .from('objections')
  1063→      .insert({
  1064→        objection,
  1065→        category,
  1066→        responses: responses || [],
  1067→        tags: tags || []
  1068→      })
  1069→      .select()
  1070→      .single();
  1071→
  1072→    if (error) throw error;
  1073→
  1074→    res.status(201).json({ objection: data });
  1075→  } catch (error) {
  1076→    console.error('Error creating objection:', error);
  1077→    res.status(500).json({ error: 'Failed to create objection' });
  1078→  }
  1079→});
  1080→
  1081→// POST /api/scripts/objections/:id/response - Add response to objection
  1082→router.post('/objections/:id/response', async (req, res) => {
  1083→  try {
  1084→    const { id } = req.params;
  1085→    const { response } = req.body;
  1086→
  1087→    if (!response) {
  1088→      return res.status(400).json({ error: 'Response text is required' });
  1089→    }
  1090→
  1091→    // Get current responses
  1092→    const { data: objection, error: fetchError } = await supabase
  1093→      .from('objections')
  1094→      .select('responses')
  1095→      .eq('id', id)
  1096→      .single();
  1097→
  1098→    if (fetchError) throw fetchError;
  1099→
  1100→    const newResponses = [
  1101→      ...(objection.responses || []),
  1102→      { response, effectiveness: 50, timesUsed: 0 }
  1103→    ];
  1104→
  1105→    const { data, error } = await supabase
  1106→      .from('objections')
  1107→      .update({
  1108→        responses: newResponses,
  1109→        updated_at: new Date().toISOString()
  1110→      })
  1111→      .eq('id', id)
  1112→      .select()
  1113→      .single();
  1114→
  1115→    if (error) throw error;
  1116→
  1117→    res.json({ objection: data });
  1118→  } catch (error) {
  1119→    console.error('Error adding response:', error);
  1120→    res.status(500).json({ error: 'Failed to add response' });
  1121→  }
  1122→});
  1123→
  1124→module.exports = router;
  1125→

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