     1→/**
     2→ * Slack notification service
     3→ */
     4→
     5→const { fetchWithTimeout, isTimeoutError } = require('../utils/fetchWithTimeout');
     6→
     7→// Rate limiting to prevent spam (max 1 message per second globally)
     8→let lastSlackMessageTime = 0;
     9→const MIN_SLACK_INTERVAL_MS = 1000;
    10→let slackRateLimitWarningLogged = false;
    11→
    12→/**
    13→ * Send a message to Slack webhook
    14→ * @param {object} payload - Slack message payload
    15→ * @returns {Promise<{success: boolean, error?: string}>}
    16→ */
    17→async function sendSlackMessage(payload) {
    18→  const webhookUrl = process.env.SLACK_WEBHOOK_URL;
    19→
    20→  if (!webhookUrl) {
    21→    console.warn('Slack not configured - missing SLACK_WEBHOOK_URL');
    22→    return { success: false, error: 'Slack not configured' };
    23→  }
    24→
    25→  // Rate limit check - prevent spam
    26→  const now = Date.now();
    27→  if (now - lastSlackMessageTime < MIN_SLACK_INTERVAL_MS) {
    28→    if (!slackRateLimitWarningLogged) {
    29→      console.warn('[Slack] Rate limited - too many messages. Suppressing for 1s.');
    30→      slackRateLimitWarningLogged = true;
    31→      setTimeout(() => { slackRateLimitWarningLogged = false; }, 5000);
    32→    }
    33→    return { success: false, error: 'Rate limited' };
    34→  }
    35→  lastSlackMessageTime = now;
    36→
    37→  try {
    38→    const response = await fetchWithTimeout(webhookUrl, {
    39→      method: 'POST',
    40→      headers: { 'Content-Type': 'application/json' },
    41→      body: JSON.stringify(payload)
    42→    }, 10000); // 10 second timeout - Slack should be fast
    43→
    44→    if (!response.ok) {
    45→      throw new Error(`Slack API error: ${response.status}`);
    46→    }
    47→
    48→    return { success: true };
    49→  } catch (error) {
    50→    if (isTimeoutError(error)) {
    51→      console.error('[Slack] Request timed out after 10 seconds');
    52→      return { success: false, error: 'Request timed out' };
    53→    }
    54→    console.error('Failed to send Slack message:', error.message);
    55→    return { success: false, error: error.message };
    56→  }
    57→}
    58→
    59→/**
    60→ * Send inbound SMS alert to Slack
    61→ * @param {object} lead - Lead object (or null if unknown)
    62→ * @param {string} message - SMS message body
    63→ * @param {string} intent - Classified intent
    64→ * @param {string} phone - Sender phone number
    65→ * @param {object} buyingSignal - Buying signal detection result {level, score, matchedPattern}
    66→ * @returns {Promise<{success: boolean, error?: string}>}
    67→ */
    68→async function sendInboundAlert(lead, message, intent, phone, buyingSignal = null) {
    69→  const isHighIntent = buyingSignal?.level === 'high';
    70→  const isMediumIntent = buyingSignal?.level === 'medium';
    71→  const hasSignificantIntent = isHighIntent || isMediumIntent;
    72→
    73→  // Override colors/emojis for buying signals
    74→  let emoji, color, headerText;
    75→
    76→  if (isHighIntent) {
    77→    emoji = ':rotating_light:';
    78→    color = '#dc2626'; // Red - URGENT
    79→    headerText = `🚨 *HIGH BUYING INTENT!* - RESPOND NOW!`;
    80→  } else if (isMediumIntent) {
    81→    emoji = ':fire:';
    82→    color = '#f97316'; // Orange - Hot
    83→    headerText = `🔥 *BUYING SIGNAL DETECTED*`;
    84→  } else if (intent === 'negative') {
    85→    emoji = ':x:';
    86→    color = '#dc3545';
    87→    headerText = ':x: Opt-out Request';
    88→  } else if (intent === 'positive') {
    89→    emoji = ':white_check_mark:';
    90→    color = '#36a64f';
    91→    headerText = ':white_check_mark: Positive Response';
    92→  } else if (intent === 'question') {
    93→    emoji = ':question:';
    94→    color = '#ffc107';
    95→    headerText = ':question: Question Received';
    96→  } else {
    97→    emoji = ':speech_balloon:';
    98→    color = '#6c757d';
    99→    headerText = ':speech_balloon: New Message';
   100→  }
   101→
   102→  const leadName = lead ? `${lead.first_name || ''} ${lead.last_name || ''}`.trim() : 'Unknown Lead';
   103→  const leadInfo = lead ? `<${process.env.ADMIN_URL || 'https://admin.rateright.com.au'}/leads/${lead.id}|${leadName}>` : leadName;
   104→
   105→  const blocks = [
   106→    {
   107→      type: 'section',
   108→      text: {
   109→        type: 'mrkdwn',
   110→        text: headerText
   111→      }
   112→    },
   113→    {
   114→      type: 'section',
   115→      fields: [
   116→        {
   117→          type: 'mrkdwn',
   118→          text: `*From:*\n${leadInfo}\n${phone}`
   119→        },
   120→        {
   121→          type: 'mrkdwn',
   122→          text: `*Intent:*\n${intent.toUpperCase()}${hasSignificantIntent ? ` (${buyingSignal.level.toUpperCase()} INTENT)` : ''}`
   123→        }
   124→      ]
   125→    },
   126→    {
   127→      type: 'section',
   128→      text: {
   129→        type: 'mrkdwn',
   130→        text: `*Message:*\n>${message}`
   131→      }
   132→    }
   133→  ];
   134→
   135→  // Add buying signal details for high/medium intent
   136→  if (hasSignificantIntent && buyingSignal.matchedPattern) {
   137→    blocks.push({
   138→      type: 'context',
   139→      elements: [
   140→        {
   141→          type: 'mrkdwn',
   142→          text: `:dart: *Signal detected:* "${buyingSignal.matchedPattern}"`
   143→        }
   144→      ]
   145→    });
   146→  }
   147→
   148→  // Add action guidance
   149→  if (isHighIntent) {
   150→    blocks.push({
   151→      type: 'section',
   152→      text: {
   153→        type: 'mrkdwn',
   154→        text: `:warning: *ACTION REQUIRED:* This lead is ready to buy! No auto-reply sent - respond personally IMMEDIATELY.`
   155→      }
   156→    });
   157→  } else if (isMediumIntent) {
   158→    blocks.push({
   159→      type: 'context',
   160→      elements: [
   161→        {
   162→          type: 'mrkdwn',
   163→          text: `:point_right: *No auto-reply sent* - Craft a personal response to close this deal!`
   164→        }
   165→      ]
   166→    });
   167→  } else if (intent === 'positive' || intent === 'question') {
   168→    blocks.push({
   169→      type: 'context',
   170→      elements: [
   171→        {
   172→          type: 'mrkdwn',
   173→          text: ':phone: *Callback created* - Check the call list'
   174→        }
   175→      ]
   176→    });
   177→  }
   178→
   179→  // Add call button for high intent
   180→  if (hasSignificantIntent && lead?.id) {
   181→    blocks.push({
   182→      type: 'actions',
   183→      elements: [
   184→        {
   185→          type: 'button',
   186→          text: {
   187→            type: 'plain_text',
   188→            text: '📞 CALL NOW',
   189→            emoji: true
   190→          },
   191→          style: 'danger',
   192→          url: `${process.env.ADMIN_URL || 'https://admin.rateright.com.au'}/leads/${lead.id}?action=call`
   193→        },
   194→        {
   195→          type: 'button',
   196→          text: {
   197→            type: 'plain_text',
   198→            text: '💬 Reply',
   199→            emoji: true
   200→          },
   201→          url: `${process.env.ADMIN_URL || 'https://admin.rateright.com.au'}/messages?lead=${lead.id}`
   202→        }
   203→      ]
   204→    });
   205→  }
   206→
   207→  const payload = {
   208→    text: `${emoji} ${hasSignificantIntent ? 'BUYING SIGNAL' : 'Inbound SMS'} from ${leadName}`,
   209→    attachments: [
   210→      {
   211→        color,
   212→        blocks
   213→      }
   214→    ]
   215→  };
   216→
   217→  return sendSlackMessage(payload);
   218→}
   219→
   220→/**
   221→ * Send inbound call alert to Slack
   222→ * @param {object} lead - Lead object (or null if unknown)
   223→ * @param {string} phone - Caller phone number (formatted)
   224→ * @param {string} callerName - Caller name
   225→ * @param {boolean} isHot - Whether this is a hot lead
   226→ * @param {boolean} isHighIntent - Whether this lead has called 3+ times
   227→ * @param {number} callCount - Number of inbound calls from this lead
   228→ * @returns {Promise<{success: boolean, error?: string}>}
   229→ */
   230→async function sendInboundCallAlert(lead, phone, callerName, isHot = false, isHighIntent = false, callCount = 0) {
   231→  const leadInfo = lead
   232→    ? `<${process.env.ADMIN_URL || 'https://admin.rateright.com.au'}/leads/${lead.id}|${callerName}>`
   233→    : callerName;
   234→
   235→  // HIGH INTENT leads get special treatment
   236→  const isUrgent = isHot || isHighIntent;
   237→  let headerText = ':telephone_receiver: *Incoming Call*';
   238→  let headerEmoji = ':telephone_receiver: Incoming Call';
   239→
   240→  if (isHighIntent) {
   241→    headerText = `:rotating_light: *HIGH INTENT - THEY'RE CHASING YOU!*\n_This lead has called you ${callCount} times_`;
   242→    headerEmoji = `🚨 HIGH INTENT LEAD CALLING! (Call #${callCount})`;
   243→  } else if (isHot) {
   244→    headerText = `:fire: *HOT LEAD CALLING - ANSWER NOW!*`;
   245→    headerEmoji = ':fire: HOT LEAD CALLING!';
   246→  }
   247→
   248→  const payload = {
   249→    text: headerEmoji,
   250→    attachments: [
   251→      {
   252→        color: isHighIntent ? '#dc2626' : (isHot ? '#f97316' : '#22c55e'),
   253→        blocks: [
   254→          {
   255→            type: 'section',
   256→            text: {
   257→              type: 'mrkdwn',
   258→              text: headerText
   259→            }
   260→          },
   261→          {
   262→            type: 'section',
   263→            fields: [
   264→              {
   265→                type: 'mrkdwn',
   266→                text: `*From:*\n${leadInfo}`
   267→              },
   268→              {
   269→                type: 'mrkdwn',
   270→                text: `*Phone:*\n${phone}`
   271→              }
   272→            ]
   273→          },
   274→          {
   275→            type: 'context',
   276→            elements: [
   277→              {
   278→                type: 'mrkdwn',
   279→                text: isHighIntent
   280→                  ? `:chart_with_upwards_trend: *HIGH INTENT* - Called ${callCount}x (+${callCount * 20} points) - Close this lead!`
   281→                  : (isHot
   282→                    ? ':fire: *HOT LEAD* (score 80+) - Ring extended to 60s'
   283→                    : (lead ? `:white_check_mark: Known lead${callCount > 0 ? ` (Call #${callCount} - +20 points)` : ''}` : ':grey_question: Unknown caller'))
   284→              }
   285→            ]
   286→          }
   287→        ]
   288→      }
   289→    ]
   290→  };
   291→
   292→  // Add AI insight for high intent leads
   293→  if (isHighIntent) {
   294→    payload.attachments[0].blocks.push({
   295→      type: 'section',
   296→      text: {
   297→        type: 'mrkdwn',
   298→        text: ':robot_face: *AI Insight:* _They\'re chasing YOU. This is extremely high intent. Close this deal._'
   299→      }
   300→    });
   301→  }
   302→
   303→  return sendSlackMessage(payload);
   304→}
   305→
   306→/**
   307→ * Send missed call alert to Slack
   308→ * @param {string} leadId - Lead ID (or null)
   309→ * @param {string} phone - Caller phone
   310→ * @param {boolean} isHot - Whether this was a hot lead
   311→ * @returns {Promise<{success: boolean, error?: string}>}
   312→ */
   313→async function sendMissedCallAlert(leadId, phone, isHot) {
   314→  const leadLink = leadId
   315→    ? `<${process.env.ADMIN_URL || 'https://admin.rateright.com.au'}/leads/${leadId}|View Lead>`
   316→    : null;
   317→
   318→  const payload = {
   319→    text: isHot ? ':rotating_light: MISSED HOT LEAD CALL!' : ':phone: Missed Call',
   320→    attachments: [
   321→      {
   322→        color: isHot ? '#dc2626' : '#eab308',
   323→        blocks: [
   324→          {
   325→            type: 'section',
   326→            text: {
   327→              type: 'mrkdwn',
   328→              text: isHot
   329→                ? `:rotating_light: *MISSED CALL FROM HOT LEAD!*\nAuto-SMS sent. Call back IMMEDIATELY!`
   330→                : `:phone: *Missed Inbound Call*\nCallback created.`
   331→            }
   332→          },
   333→          {
   334→            type: 'section',
   335→            fields: [
   336→              {
   337→                type: 'mrkdwn',
   338→                text: `*Phone:*\n${phone}`
   339→              },
   340→              {
   341→                type: 'mrkdwn',
   342→                text: `*Priority:*\n${isHot ? ':fire: URGENT' : 'High'}`
   343→              }
   344→            ]
   345→          },
   346→          ...(leadLink ? [{
   347→            type: 'actions',
   348→            elements: [
   349→              {
   350→                type: 'button',
   351→                text: { type: 'plain_text', text: 'View Lead', emoji: true },
   352→                url: `${process.env.ADMIN_URL || 'https://admin.rateright.com.au'}/leads/${leadId}`
   353→              }
   354→            ]
   355→          }] : [])
   356→        ]
   357→      }
   358→    ]
   359→  };
   360→
   361→  return sendSlackMessage(payload);
   362→}
   363→
   364→/**
   365→ * Send voicemail alert to Slack
   366→ * @param {string} leadId - Lead ID (or null)
   367→ * @param {string} phone - Caller phone
   368→ * @param {number} duration - Recording duration in seconds
   369→ * @param {string} callerName - Caller name
   370→ * @returns {Promise<{success: boolean, error?: string}>}
   371→ */
   372→async function sendVoicemailAlert(leadId, phone, duration, callerName) {
   373→  const leadLink = leadId
   374→    ? `<${process.env.ADMIN_URL || 'https://admin.rateright.com.au'}/leads/${leadId}|${callerName}>`
   375→    : callerName;
   376→
   377→  const payload = {
   378→    text: ':voicemail: New Voicemail',
   379→    attachments: [
   380→      {
   381→        color: '#8b5cf6',
   382→        blocks: [
   383→          {
   384→            type: 'section',
   385→            text: {
   386→              type: 'mrkdwn',
   387→              text: `:voicemail: *New Voicemail Received*`
   388→            }
   389→          },
   390→          {
   391→            type: 'section',
   392→            fields: [
   393→              {
   394→                type: 'mrkdwn',
   395→                text: `*From:*\n${leadLink}`
   396→              },
   397→              {
   398→                type: 'mrkdwn',
   399→                text: `*Duration:*\n${duration}s`
   400→              }
   401→            ]
   402→          },
   403→          {
   404→            type: 'context',
   405→            elements: [
   406→              {
   407→                type: 'mrkdwn',
   408→                text: ':hourglass_flowing_sand: Transcription processing...'
   409→              }
   410→            ]
   411→          }
   412→        ]
   413→      }
   414→    ]
   415→  };
   416→
   417→  return sendSlackMessage(payload);
   418→}
   419→
   420→/**
   421→ * Send callback reminder to Slack
   422→ * @param {object} callback - Callback record
   423→ * @param {object} lead - Lead object
   424→ * @returns {Promise<{success: boolean, error?: string}>}
   425→ */
   426→async function sendCallbackReminder(callback, lead) {
   427→  const leadName = lead ? `${lead.first_name || ''} ${lead.last_name || ''}`.trim() : 'Unknown';
   428→
   429→  const payload = {
   430→    text: ':alarm_clock: Callback Reminder',
   431→    attachments: [
   432→      {
   433→        color: '#17a2b8',
   434→        blocks: [
   435→          {
   436→            type: 'section',
   437→            text: {
   438→              type: 'mrkdwn',
   439→              text: `*${leadName}* is due for a callback`
   440→            }
   441→          },
   442→          {
   443→            type: 'section',
   444→            fields: [
   445→              {
   446→                type: 'mrkdwn',
   447→                text: `*Phone:*\n${lead?.phone || 'Unknown'}`
   448→              },
   449→              {
   450→                type: 'mrkdwn',
   451→                text: `*Reason:*\n${callback.reason || 'Follow-up'}`
   452→              }
   453→            ]
   454→          }
   455→        ]
   456→      }
   457→    ]
   458→  };
   459→
   460→  return sendSlackMessage(payload);
   461→}
   462→
   463→/**
   464→ * Send daily summary to Slack
   465→ * @param {object} metrics - Daily metrics
   466→ * @returns {Promise<{success: boolean, error?: string}>}
   467→ */
   468→async function sendDailySummary(metrics) {
   469→  const payload = {
   470→    text: ':chart_with_upwards_trend: Daily Summary',
   471→    attachments: [
   472→      {
   473→        color: '#007bff',
   474→        blocks: [
   475→          {
   476→            type: 'section',
   477→            text: {
   478→              type: 'mrkdwn',
   479→              text: '*RateRight Growth Engine - Daily Summary*'
   480→            }
   481→          },
   482→          {
   483→            type: 'section',
   484→            fields: [
   485→              {
   486→                type: 'mrkdwn',
   487→                text: `*Calls Made:*\n${metrics.callsMade || 0}`
   488→              },
   489→              {
   490→                type: 'mrkdwn',
   491→                text: `*SMS Sent:*\n${metrics.smsSent || 0}`
   492→              },
   493→              {
   494→                type: 'mrkdwn',
   495→                text: `*Inbound SMS:*\n${metrics.inboundSms || 0}`
   496→              },
   497→              {
   498→                type: 'mrkdwn',
   499→                text: `*Conversions:*\n${metrics.conversions || 0}`
   500→              }
   501→            ]
   502→          },
   503→          {
   504→            type: 'section',
   505→            fields: [
   506→              {
   507→                type: 'mrkdwn',
   508→                text: `*Hot Leads:*\n${metrics.hotLeads || 0}`
   509→              },
   510→              {
   511→                type: 'mrkdwn',
   512→                text: `*Pending Callbacks:*\n${metrics.pendingCallbacks || 0}`
   513→              }
   514→            ]
   515→          }
   516→        ]
   517→      }
   518→    ]
   519→  };
   520→
   521→  return sendSlackMessage(payload);
   522→}
   523→
   524→/**
   525→ * Send conversion alert to Slack
   526→ * @param {object} lead - Lead object
   527→ * @param {string} type - Conversion type (job_posted, worker_hired, subscription)
   528→ * @returns {Promise<{success: boolean, error?: string}>}
   529→ */
   530→async function sendConversionAlert(lead, type) {
   531→  const typeInfo = {
   532→    job_posted: { emoji: ':briefcase:', text: 'posted a job' },
   533→    worker_hired: { emoji: ':handshake:', text: 'hired a worker' },
   534→    subscription: { emoji: ':star:', text: 'upgraded to subscription' }
   535→  };
   536→
   537→  const info = typeInfo[type] || { emoji: ':tada:', text: 'converted' };
   538→  const leadName = lead ? `${lead.first_name || ''} ${lead.last_name || ''}`.trim() : 'Unknown';
   539→
   540→  const payload = {
   541→    text: `${info.emoji} Conversion Alert`,
   542→    attachments: [
   543→      {
   544→        color: '#28a745',
   545→        blocks: [
   546→          {
   547→            type: 'section',
   548→            text: {
   549→              type: 'mrkdwn',
   550→              text: `${info.emoji} *${leadName}* ${info.text}!`
   551→            }
   552→          }
   553→        ]
   554→      }
   555→    ]
   556→  };
   557→
   558→  return sendSlackMessage(payload);
   559→}
   560→
   561→/**
   562→ * Send weekly "What's Working" report to Slack
   563→ * @param {object} report - Weekly report data
   564→ * @returns {Promise<{success: boolean, error?: string}>}
   565→ */
   566→async function sendWeeklyReport(report) {
   567→  const { insights, weekStats, totalConversions } = report;
   568→
   569→  // Build insight blocks
   570→  const insightBlocks = insights.map(insight => ({
   571→    type: 'section',
   572→    text: {
   573→      type: 'mrkdwn',
   574→      text: `${insight.icon} *${insight.title}*\n${insight.detail}\n_${insight.action}_`
   575→    }
   576→  }));
   577→
   578→  // If no insights, add a placeholder
   579→  if (insightBlocks.length === 0) {
   580→    insightBlocks.push({
   581→      type: 'section',
   582→      text: {
   583→        type: 'mrkdwn',
   584→        text: ':chart_with_upwards_trend: Keep calling! More data will unlock insights.'
   585→      }
   586→    });
   587→  }
   588→
   589→  const payload = {
   590→    text: ':crystal_ball: Weekly "What\'s Working" Report',
   591→    attachments: [
   592→      {
   593→        color: '#6366f1',
   594→        blocks: [
   595→          {
   596→            type: 'header',
   597→            text: {
   598→              type: 'plain_text',
   599→              text: '🔮 What\'s Working This Week',
   600→              emoji: true
   601→            }
   602→          },
   603→          {
   604→            type: 'section',
   605→            text: {
   606→              type: 'mrkdwn',
   607→              text: `Based on *${totalConversions} conversions* over the last 30 days`
   608→            }
   609→          },
   610→          {
   611→            type: 'divider'
   612→          },
   613→          ...insightBlocks,
   614→          {
   615→            type: 'divider'
   616→          },
   617→          {
   618→            type: 'section',
   619→            text: {
   620→              type: 'mrkdwn',
   621→              text: '*This Week\'s Activity*'
   622→            }
   623→          },
   624→          {
   625→            type: 'section',
   626→            fields: [
   627→              {
   628→                type: 'mrkdwn',
   629→                text: `:phone: *Calls:* ${weekStats.calls}`
   630→              },
   631→              {
   632→                type: 'mrkdwn',
   633→                text: `:outbox_tray: *SMS Out:* ${weekStats.smsOut}`
   634→              },
   635→              {
   636→                type: 'mrkdwn',
   637→                text: `:inbox_tray: *SMS In:* ${weekStats.smsIn}`
   638→              },
   639→              {
   640→                type: 'mrkdwn',
   641→                text: `:trophy: *Conversions:* ${weekStats.conversions}`
   642→              }
   643→            ]
   644→          },
   645→          {
   646→            type: 'context',
   647→            elements: [
   648→              {
   649→                type: 'mrkdwn',
   650→                text: ':robot_face: _AI has auto-tuned the call list based on these patterns_'
   651→              }
   652→            ]
   653→          }
   654→        ]
   655→      }
   656→    ]
   657→  };
   658→
   659→  return sendSlackMessage(payload);
   660→}
   661→
   662→/**
   663→ * Send bug report to Slack
   664→ * @param {object} report - Bug report data
   665→ * @returns {Promise<{success: boolean, error?: string}>}
   666→ */
   667→async function sendBugReport(report) {
   668→  const { description, pageUrl, userEmail, deviceInfo, timestamp, screenshot } = report;
   669→
   670→  // Format timestamp in AEST
   671→  const aestTime = new Date(timestamp).toLocaleString('en-AU', {
   672→    timeZone: 'Australia/Sydney',
   673→    year: 'numeric',
   674→    month: '2-digit',
   675→    day: '2-digit',
   676→    hour: '2-digit',
   677→    minute: '2-digit',
   678→  });
   679→
   680→  const blocks = [
   681→    {
   682→      type: 'header',
   683→      text: {
   684→        type: 'plain_text',
   685→        text: '🐛 BUG REPORT',
   686→        emoji: true
   687→      }
   688→    },
   689→    {
   690→      type: 'section',
   691→      fields: [
   692→        {
   693→          type: 'mrkdwn',
   694→          text: `*From:*\n${userEmail}`
   695→        },
   696→        {
   697→          type: 'mrkdwn',
   698→          text: `*Page:*\n${pageUrl}`
   699→        },
   700→        {
   701→          type: 'mrkdwn',
   702→          text: `*Time:*\n${aestTime} AEST`
   703→        },
   704→        {
   705→          type: 'mrkdwn',
   706→          text: `*Device:*\n${deviceInfo}`
   707→        }
   708→      ]
   709→    },
   710→    {
   711→      type: 'section',
   712→      text: {
   713→        type: 'mrkdwn',
   714→        text: `*Description:*\n> "${description}"`
   715→      }
   716→    }
   717→  ];
   718→
   719→  // Add screenshot indicator if present
   720→  if (screenshot) {
   721→    blocks.push({
   722→      type: 'context',
   723→      elements: [
   724→        {
   725→          type: 'mrkdwn',
   726→          text: ':camera: _Screenshot attached below_'
   727→        }
   728→      ]
   729→    });
   730→  }
   731→
   732→  const payload = {
   733→    text: '🐛 Bug Report from ' + userEmail,
   734→    attachments: [
   735→      {
   736→        color: '#dc3545',
   737→        blocks
   738→      }
   739→    ]
   740→  };
   741→
   742→  // Send the main message
   743→  const result = await sendSlackMessage(payload);
   744→
   745→  // If there's a screenshot, send it as a follow-up message
   746→  // Note: Slack webhooks can't directly upload images, so we send the base64 data
   747→  // For production, you'd want to upload to S3/Cloudinary first
   748→  if (screenshot && result.success) {
   749→    // For now, just indicate screenshot was captured in the message
   750→    // Full image upload would require Slack API tokens, not just webhook
   751→    console.log('[BugReport] Screenshot captured (base64 length:', screenshot.length, ')');
   752→  }
   753→
   754→  return result;
   755→}
   756→
   757→/**
   758→ * Send contractor referral alert to Slack
   759→ * @param {object} contractor - New contractor lead data
   760→ * @param {object} worker - Worker who provided the referral
   761→ * @returns {Promise<{success: boolean, error?: string}>}
   762→ */
   763→async function sendContractorReferralAlert(contractor, worker) {
   764→  const workerName = worker ? `${worker.first_name || ''} ${worker.last_name || ''}`.trim() : 'Unknown Worker';
   765→  const contractorName = contractor ? `${contractor.first_name || ''} ${contractor.last_name || ''}`.trim() : 'Unknown';
   766→
   767→  const payload = {
   768→    text: ':phone: New Contractor Lead from Worker Intel',
   769→    attachments: [
   770→      {
   771→        color: '#f59e0b',
   772→        blocks: [
   773→          {
   774→            type: 'header',
   775→            text: {
   776→              type: 'plain_text',
   777→              text: 'New Contractor Lead from Worker Intel',
   778→              emoji: true
   779→            }
   780→          },
   781→          {
   782→            type: 'section',
   783→            fields: [
   784→              {
   785→                type: 'mrkdwn',
   786→                text: `*Contractor:*\n${contractorName}${contractor?.job_title ? ` (${contractor.job_title})` : ''}`
   787→              },
   788→              {
   789→                type: 'mrkdwn',
   790→                text: `*Phone:*\n${contractor?.phone || 'Unknown'}`
   791→              }
   792→            ]
   793→          },
   794→          {
   795→            type: 'section',
   796→            text: {
   797→              type: 'mrkdwn',
   798→              text: `*Source:*\n:speech_balloon: ${workerName} texted the number`
   799→            }
   800→          },
   801→          {
   802→            type: 'context',
   803→            elements: [
   804→              {
   805→                type: 'mrkdwn',
   806→                text: ':fire: *Hot referral* - insider contact from worker'
   807→              }
   808→            ]
   809→          }
   810→        ]
   811→      }
   812→    ]
   813→  };
   814→
   815→  // Add link to new lead if we have the ID
   816→  if (contractor?.id) {
   817→    payload.attachments[0].blocks.push({
   818→      type: 'actions',
   819→      elements: [
   820→        {
   821→          type: 'button',
   822→          text: {
   823→            type: 'plain_text',
   824→            text: 'View Lead',
   825→            emoji: true
   826→          },
   827→          url: `${process.env.ADMIN_URL || 'https://admin.rateright.com.au'}/leads/${contractor.id}`
   828→        }
   829→      ]
   830→    });
   831→  }
   832→
   833→  return sendSlackMessage(payload);
   834→}
   835→
   836→/**
   837→ * Send website signup alert to Slack (URGENT formatting)
   838→ * @param {object} lead - Lead object
   839→ * @param {boolean} isReEngaged - Whether this is a returning lead
   840→ * @param {array} previousHistory - Previous communications history
   841→ * @param {string} aiInsight - AI-generated insight for re-engaged leads
   842→ * @param {string} signupMessage - Message from the signup form
   843→ * @returns {Promise<{success: boolean, error?: string}>}
   844→ */
   845→async function sendWebsiteSignupAlert(lead, isReEngaged, previousHistory, aiInsight, signupMessage) {
   846→  const leadName = lead ? `${lead.first_name || ''} ${lead.last_name || ''}`.trim() : 'Unknown';
   847→  const leadLink = lead?.id
   848→    ? `<${process.env.ADMIN_URL || 'https://admin.rateright.com.au'}/leads/${lead.id}|${leadName}>`
   849→    : leadName;
   850→
   851→  // Calculate time since signup (for urgency)
   852→  const now = Date.now();
   853→  const signupTime = lead?.signup_at ? new Date(lead.signup_at).getTime() : now;
   854→  const secondsAgo = Math.floor((now - signupTime) / 1000);
   855→
   856→  // Build header based on re-engagement status
   857→  let headerText, headerEmoji, color;
   858→  if (isReEngaged) {
   859→    headerText = `:rotating_light: *THEY'RE BACK!*\n_${leadName} just signed up again on your website_`;
   860→    headerEmoji = `🔴 THEY'RE BACK! ${leadName} re-engaged!`;
   861→    color = '#dc2626'; // Red for re-engaged
   862→  } else {
   863→    headerText = `:large_orange_circle: *NEW WEBSITE SIGNUP*\n_${leadName} just signed up - call within 5 minutes for 10x conversion!_`;
   864→    headerEmoji = `🟠 NEW WEBSITE SIGNUP - ${leadName}`;
   865→    color = '#f97316'; // Orange for new
   866→  }
   867→
   868→  const blocks = [
   869→    {
   870→      type: 'section',
   871→      text: {
   872→        type: 'mrkdwn',
   873→        text: headerText
   874→      }
   875→    },
   876→    {
   877→      type: 'section',
   878→      fields: [
   879→        {
   880→          type: 'mrkdwn',
   881→          text: `*Lead:*\n${leadLink}`
   882→        },
   883→        {
   884→          type: 'mrkdwn',
   885→          text: `*Company:*\n${lead?.company || 'Unknown'}`
   886→        },
   887→        {
   888→          type: 'mrkdwn',
   889→          text: `*Phone:*\n${lead?.phone || 'Unknown'}`
   890→        },
   891→        {
   892→          type: 'mrkdwn',
   893→          text: `*Score:*\n${lead?.score || 70}`
   894→        }
   895→      ]
   896→    }
   897→  ];
   898→
   899→  // Add signup message if provided
   900→  if (signupMessage) {
   901→    blocks.push({
   902→      type: 'section',
   903→      text: {
   904→        type: 'mrkdwn',
   905→        text: `*Their Message:*\n> "${signupMessage}"`
   906→      }
   907→    });
   908→  }
   909→
   910→  // Add AI insight for re-engaged leads
   911→  if (isReEngaged && aiInsight) {
   912→    blocks.push({
   913→      type: 'section',
   914→      text: {
   915→        type: 'mrkdwn',
   916→        text: `:robot_face: *AI Insight:*\n_${aiInsight}_`
   917→      }
   918→    });
   919→  }
   920→
   921→  // Add previous history summary for re-engaged leads
   922→  if (isReEngaged && previousHistory && previousHistory.length > 0) {
   923→    const historyItems = previousHistory.slice(0, 3).map(h => {
   924→      const date = new Date(h.created_at).toLocaleDateString('en-AU');
   925→      const type = h.type.replace('_', ' ');
   926→      return `• ${date}: ${type}`;
   927→    }).join('\n');
   928→
   929→    blocks.push({
   930→      type: 'section',
   931→      text: {
   932→        type: 'mrkdwn',
   933→        text: `*Previous Contact:*\n${historyItems}`
   934→      }
   935→    });
   936→  }
   937→
   938→  // Add urgency message
   939→  blocks.push({
   940→    type: 'context',
   941→    elements: [
   942→      {
   943→        type: 'mrkdwn',
   944→        text: isReEngaged
   945→          ? `:fire: *HIGH PRIORITY* - They came back! Previous interest confirmed. Close this deal!`
   946→          : `:stopwatch: *${secondsAgo}s ago* - Call within 5 mins = 10x conversion rate!`
   947→      }
   948→    ]
   949→  });
   950→
   951→  // Add call button
   952→  if (lead?.id) {
   953→    blocks.push({
   954→      type: 'actions',
   955→      elements: [
   956→        {
   957→          type: 'button',
   958→          text: {
   959→            type: 'plain_text',
   960→            text: '📞 CALL NOW',
   961→            emoji: true
   962→          },
   963→          style: 'danger',
   964→          url: `${process.env.ADMIN_URL || 'https://admin.rateright.com.au'}/leads/${lead.id}?action=call`
   965→        }
   966→      ]
   967→    });
   968→  }
   969→
   970→  const payload = {
   971→    text: headerEmoji,
   972→    attachments: [
   973→      {
   974→        color,
   975→        blocks
   976→      }
   977→    ]
   978→  };
   979→
   980→  return sendSlackMessage(payload);
   981→}
   982→
   983→/**
   984→ * Send dev queue item to Slack (bug or feature)
   985→ * @param {object} item - Dev queue item
   986→ * @param {string} itemType - 'bug', 'test_bug', or 'feature'
   987→ * @returns {Promise<{success: boolean, error?: string}>}
   988→ */
   989→async function sendDevQueueItem(item, itemType) {
   990→  const isFeature = itemType === 'feature';
   991→  const isTestBug = itemType === 'test_bug';
   992→
   993→  // Format based on type
   994→  let emoji, color, typeLabel;
   995→  if (isFeature) {
   996→    emoji = ':bulb:';
   997→    color = '#6366f1'; // Purple
   998→    typeLabel = 'FEATURE';
   999→  } else if (isTestBug) {
  1000→    emoji = ':warning:';
  1001→    color = '#f59e0b'; // Amber
  1002→    typeLabel = `TEST-BUG #${item.parent_feature_id}`;
  1003→  } else {
  1004→    emoji = ':bug:';
  1005→    color = '#dc3545'; // Red
  1006→    typeLabel = 'BUG';
  1007→  }
  1008→
  1009→  const blocks = [
  1010→    {
  1011→      type: 'header',
  1012→      text: {
  1013→        type: 'plain_text',
  1014→        text: `${emoji} [${typeLabel} #${item.id}] ${item.title}`,
  1015→        emoji: true
  1016→      }
  1017→    }
  1018→  ];
  1019→
  1020→  // Add description if present
  1021→  if (item.description) {
  1022→    blocks.push({
  1023→      type: 'section',
  1024→      text: {
  1025→        type: 'mrkdwn',
  1026→        text: `> ${item.description}`
  1027→      }
  1028→    });
  1029→  }
  1030→
  1031→  // Add context
  1032→  const contextParts = [`*From:* ${item.reported_by}`];
  1033→  if (item.page_url) {
  1034→    contextParts.push(`*Page:* ${item.page_url}`);
  1035→  }
  1036→
  1037→  blocks.push({
  1038→    type: 'section',
  1039→    fields: contextParts.map(text => ({ type: 'mrkdwn', text }))
  1040→  });
  1041→
  1042→  // Add CC protocol reminder
  1043→  blocks.push({
  1044→    type: 'context',
  1045→    elements: [
  1046→      {
  1047→        type: 'mrkdwn',
  1048→        text: ':robot_face: *CC Protocol:* Read `docs/LESSONS.md` first | Trace data flow | Check if similar bug documented'
  1049→      }
  1050→    ]
  1051→  });
  1052→
  1053→  const payload = {
  1054→    text: `${emoji} [${typeLabel} #${item.id}] ${item.title}`,
  1055→    attachments: [{ color, blocks }]
  1056→  };
  1057→
  1058→  return sendSlackMessage(payload);
  1059→}
  1060→
  1061→/**
  1062→ * Send "Ready to Test" notification to Slack
  1063→ * @param {object} item - Dev queue item that's ready
  1064→ * @returns {Promise<{success: boolean, error?: string}>}
  1065→ */
  1066→async function sendDevQueueReady(item) {
  1067→  const isFeature = item.type === 'feature';
  1068→  const emoji = isFeature ? ':white_check_mark:' : ':wrench:';
  1069→  const typeLabel = isFeature ? 'FEATURE' : 'BUG FIX';
  1070→
  1071→  const blocks = [
  1072→    {
  1073→      type: 'header',
  1074→      text: {
  1075→        type: 'plain_text',
  1076→        text: `${emoji} [READY #${item.id}] ${item.title}`,
  1077→        emoji: true
  1078→      }
  1079→    },
  1080→    {
  1081→      type: 'section',
  1082→      text: {
  1083→        type: 'mrkdwn',
  1084→        text: `*${typeLabel} ready for testing!*`
  1085→      }
  1086→    }
  1087→  ];
  1088→
  1089→  // Add CC notes if present
  1090→  if (item.cc_notes) {
  1091→    blocks.push({
  1092→      type: 'section',
  1093→      text: {
  1094→        type: 'mrkdwn',
  1095→        text: `:memo: *Build Notes:*\n${item.cc_notes}`
  1096→      }
  1097→    });
  1098→  }
  1099→
  1100→  // Add testing instructions
  1101→  blocks.push({
  1102→    type: 'context',
  1103→    elements: [
  1104→      {
  1105→        type: 'mrkdwn',
  1106→        text: `:point_right: Test this ${isFeature ? 'feature' : 'fix'}. Report bugs with \`#${item.id}\` to link them.`
  1107→      }
  1108→    ]
  1109→  });
  1110→
  1111→  const payload = {
  1112→    text: `${emoji} [READY #${item.id}] ${item.title} - Ready to test!`,
  1113→    attachments: [{ color: '#22c55e', blocks }]
  1114→  };
  1115→
  1116→  return sendSlackMessage(payload);
  1117→}
  1118→
  1119→/**
  1120→ * Check if Slack is configured
  1121→ * @returns {boolean}
  1122→ */
  1123→function isConfigured() {
  1124→  return !!process.env.SLACK_WEBHOOK_URL;
  1125→}
  1126→
  1127→module.exports = {
  1128→  sendSlackMessage,
  1129→  sendInboundAlert,
  1130→  sendInboundCallAlert,
  1131→  sendMissedCallAlert,
  1132→  sendVoicemailAlert,
  1133→  sendCallbackReminder,
  1134→  sendDailySummary,
  1135→  sendConversionAlert,
  1136→  sendWeeklyReport,
  1137→  sendBugReport,
  1138→  sendContractorReferralAlert,
  1139→  sendWebsiteSignupAlert,
  1140→  sendDevQueueItem,
  1141→  sendDevQueueReady,
  1142→  isConfigured
  1143→};
  1144→

<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→ * XP Service - Award XP for user actions
     3→ * Used across routes to consistently award XP
     4→ */
     5→
     6→const { supabase } = require('../config/database');
     7→
     8→// XP Award amounts for different actions
     9→const XP_AWARDS = {
    10→  call_made: 5,
    11→  call_connected: 10,
    12→  conversation_started: 15,
    13→  callback_scheduled: 10,
    14→  meeting_set: 25,
    15→  conversion: 100,
    16→  sms_sent: 2,
    17→  sms_reply_received: 5,
    18→  copilot_used: 5,
    19→  script_completed: 15,
    20→  daily_target_met: 50,
    21→  streak_bonus: 10, // Per streak day
    22→};
    23→
    24→// Level names
    25→const LEVEL_NAMES = {
    26→  1: 'Rookie',
    27→  2: 'Prospect',
    28→  3: 'Closer',
    29→  4: 'Hustler',
    30→  5: 'Deal Maker',
    31→  6: 'Sales Star',
    32→  7: 'Rainmaker',
    33→  8: 'Legend',
    34→  9: 'Hall of Famer',
    35→  10: 'GOAT',
    36→};
    37→
    38→// Calculate level from XP
    39→function calculateLevel(xp) {
    40→  return Math.max(1, Math.floor(Math.sqrt(xp / 100)) + 1);
    41→}
    42→
    43→/**
    44→ * Award XP to a user for an action
    45→ * @param {string} userId - The user ID
    46→ * @param {string} action - The action type (from XP_AWARDS keys)
    47→ * @param {object} options - Additional options
    48→ * @param {number} options.customAmount - Override the default amount
    49→ * @param {string} options.relatedId - ID of related entity (lead, call, etc.)
    50→ * @param {string} options.reason - Custom reason description
    51→ * @returns {object} - Result with awarded XP, level info, etc.
    52→ */
    53→async function awardXP(userId, action, options = {}) {
    54→  try {
    55→    const { customAmount, relatedId, reason } = options;
    56→
    57→    // Determine XP amount
    58→    const amount = customAmount || XP_AWARDS[action] || 0;
    59→
    60→    if (amount <= 0) {
    61→      return { success: false, error: 'Invalid action or amount' };
    62→    }
    63→
    64→    // Get current XP record
    65→    let { data: xpData } = await supabase
    66→      .from('user_xp')
    67→      .select('*')
    68→      .eq('user_id', userId)
    69→      .single();
    70→
    71→    const today = new Date().toISOString().split('T')[0];
    72→    let levelUp = false;
    73→    let newLevel = null;
    74→    let streakBonus = 0;
    75→
    76→    if (xpData) {
    77→      const oldLevel = xpData.level || calculateLevel(xpData.total_xp);
    78→      const newTotalXP = xpData.total_xp + amount;
    79→      newLevel = calculateLevel(newTotalXP);
    80→      levelUp = newLevel > oldLevel;
    81→
    82→      // Check and update streak
    83→      let newStreak = xpData.current_streak;
    84→      let newLongestStreak = xpData.longest_streak;
    85→
    86→      if (xpData.last_activity_date) {
    87→        const lastDate = new Date(xpData.last_activity_date);
    88→        const todayDate = new Date(today);
    89→        const daysDiff = Math.floor((todayDate - lastDate) / (1000 * 60 * 60 * 24));
    90→
    91→        if (daysDiff === 1) {
    92→          // Consecutive day - extend streak
    93→          newStreak = xpData.current_streak + 1;
    94→          // Add streak bonus (capped at 100)
    95→          streakBonus = Math.min(newStreak * XP_AWARDS.streak_bonus, 100);
    96→        } else if (daysDiff > 1) {
    97→          // Streak broken
    98→          newStreak = 1;
    99→        }
   100→        // daysDiff === 0 means same day, keep streak
   101→      } else {
   102→        newStreak = 1;
   103→      }
   104→
   105→      newLongestStreak = Math.max(newLongestStreak, newStreak);
   106→
   107→      // Update XP record
   108→      const { error: updateError } = await supabase
   109→        .from('user_xp')
   110→        .update({
   111→          total_xp: newTotalXP + streakBonus,
   112→          level: newLevel,
   113→          current_streak: newStreak,
   114→          longest_streak: newLongestStreak,
   115→          last_activity_date: today,
   116→        })
   117→        .eq('user_id', userId);
   118→
   119→      if (updateError) throw updateError;
   120→
   121→      xpData.total_xp = newTotalXP + streakBonus;
   122→      xpData.current_streak = newStreak;
   123→    } else {
   124→      // Create new record
   125→      const { data: newData, error: insertError } = await supabase
   126→        .from('user_xp')
   127→        .insert({
   128→          user_id: userId,
   129→          total_xp: amount,
   130→          level: 1,
   131→          current_streak: 1,
   132→          longest_streak: 1,
   133→          last_activity_date: today,
   134→        })
   135→        .select()
   136→        .single();
   137→
   138→      if (insertError) throw insertError;
   139→      xpData = newData;
   140→      newLevel = 1;
   141→    }
   142→
   143→    // Log XP history
   144→    await supabase.from('xp_history').insert({
   145→      user_id: userId,
   146→      amount: amount + streakBonus,
   147→      reason: reason || action,
   148→      related_id: relatedId,
   149→    });
   150→
   151→    return {
   152→      success: true,
   153→      awarded: amount,
   154→      streakBonus,
   155→      totalAwarded: amount + streakBonus,
   156→      newTotal: xpData.total_xp,
   157→      level: newLevel || xpData.level,
   158→      levelName: LEVEL_NAMES[Math.min(newLevel || xpData.level, 10)],
   159→      levelUp,
   160→      currentStreak: xpData.current_streak,
   161→    };
   162→  } catch (error) {
   163→    console.error('Error awarding XP:', error);
   164→    return { success: false, error: error.message };
   165→  }
   166→}
   167→
   168→/**
   169→ * Award XP for a call outcome
   170→ * Maps call outcomes to appropriate XP awards
   171→ */
   172→async function awardXPForCall(userId, outcome, leadId) {
   173→  const outcomeToAction = {
   174→    connected: 'call_connected',
   175→    voicemail: 'call_made',
   176→    no_answer: 'call_made',
   177→    busy: 'call_made',
   178→    wrong_number: 'call_made',
   179→    callback_scheduled: 'callback_scheduled',
   180→    not_interested: 'call_connected',
   181→    interested: 'conversation_started',
   182→    converted: 'conversion',
   183→  };
   184→
   185→  const action = outcomeToAction[outcome] || 'call_made';
   186→  return awardXP(userId, action, {
   187→    relatedId: leadId,
   188→    reason: `Call: ${outcome}`,
   189→  });
   190→}
   191→
   192→/**
   193→ * Award XP for sending an SMS
   194→ */
   195→async function awardXPForSMS(userId, leadId, isReply = false) {
   196→  const action = isReply ? 'sms_reply_received' : 'sms_sent';
   197→  return awardXP(userId, action, {
   198→    relatedId: leadId,
   199→    reason: isReply ? 'SMS reply received' : 'SMS sent',
   200→  });
   201→}
   202→
   203→module.exports = {
   204→  XP_AWARDS,
   205→  LEVEL_NAMES,
   206→  calculateLevel,
   207→  awardXP,
   208→  awardXPForCall,
   209→  awardXPForSMS,
   210→};
   211→

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


Conversation compacted


This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.

Analysis:
Let me analyze the conversation chronologically:

1. The user requested a detailed audit of every file in `/home/ccuser/rateright-growth/src/services/`. They want 7 specific things reported for each file:
   - Functions exported and their purpose
   - Bug risks (race conditions, unhandled promises, logic errors)
   - Error handling (caught properly? swallowed silently?)
   - Dead code (unused exports, commented-out code, stale functions)
   - Security issues (API key exposure, SQL injection, unsafe string interpolation)
   - External API calls (third-party services, response validation)
   - Database queries (tables accessed, raw SQL injection risks)

2. I started by reading the CLAUDE.md instructions, then attempted to read `.cc-local/MEMORY.md` which didn't exist. I then listed all files in the services directory and created a todo list to track progress.

3. I've read the following files so far (about 30 out of 44 files):
   - Batch 1: challengesServiceV2.js, aiCallScoring.js, activationAI.js, challengesService.js
   - Batch 2: badgesService.js, callListRanker.js, callAnalytics.js, promptManagement.js
   - Batch 3: learning.js, duelsService.js, dealIntelligence.js, selfImprovement.js
   - Batch 4: platformSync.js, perplexity.js, repPerformance.js, compliance.js
   - Batch 5: openai.js, criticalAlerts.js, leaderboardService.js, deepgram.js
   - Batch 6: scoring.js, pushNotifications.js, weeklyIntelligence.js
   - Batch 7 (partial): twilioMedia.js, twilioMediaSetup.js, templates.js, slack.js, xpService.js

4. Files NOT yet read:
   - wisdom.js, stageDiagnosis.js, stageCalculator.js
   - twimlWithStream.js, qualityAudit.js, ai.js, notifications.js
   - taskExecutor.js, taskParser.js, exportService.js, webhookDelivery.js
   - enrichment.js, jobLocking.js, twilio.js, cache.js

5. I have NOT yet written the actual audit report. I've only been reading files to gather data. The user explicitly asked me to "Be thorough and brutal. Report EVERYTHING with specific line numbers and code snippets."

6. Key findings I should note from my reading:
   - deepgram.js exposes API key to client via getClientConfig() (line 47)
   - weeklyIntelligence.js creates OpenAI client at module level without checking for API key (line 9)
   - promptManagement.js uses db.raw() which may not work with Supabase client (lines 167, 203)
   - criticalAlerts.js has a setInterval at module level that could cause memory issues (line 34)
   - Multiple files have race conditions in read-then-write patterns
   - Multiple files silently swallow errors with .catch(() => {})
   - challengesService.js matches challenges to actions by string matching on titles (fragile)
   - duelsService.js has race conditions in updateDuelHistory (read-then-write without locking)
   - Many files use sequential DB queries in loops instead of batch operations

The user has not provided any feedback yet - this is all first-pass work. No errors were encountered during reading.

The task was interrupted mid-way through reading files. I still need to read about 14 more files and then compile the full audit report.

Summary:
1. Primary Request and Intent:
   The user requested a comprehensive, detailed security and code quality audit of ALL files in `/home/ccuser/rateright-growth/src/services/`. They explicitly listed 44 files to audit and specified 7 dimensions for each file:
   1. Functions exported and their purpose
   2. Bug risks — race conditions, unhandled promises, logic errors
   3. Error handling — are errors caught properly? Are they swallowed silently?
   4. Dead code — unused exports, commented-out code, stale functions
   5. Security issues — API key exposure, SQL injection, unsafe string interpolation in queries
   6. External API calls — what third-party services are used? Are responses validated?
   7. Database queries — what tables are accessed? Any raw SQL injection risks?

   The user emphasized: "Be thorough and brutal. Report EVERYTHING with specific line numbers and code snippets."

2. Key Technical Concepts:
   - Node.js/Express backend services for a sales CRM (RateRight Growth Engine)
   - Supabase as the database layer (PostgreSQL via Supabase JS client)
   - OpenAI GPT-4o / GPT-4o-mini for AI features (call scoring, nudges, analysis)
   - Perplexity AI for web research (company/person intel)
   - Deepgram for real-time speech-to-text transcription
   - Twilio for SMS/calls with media streaming
   - Slack webhooks for notifications
   - Web Push notifications via VAPID
   - Gamification system (XP, duels, challenges, badges, leaderboards)
   - Self-improvement/intelligence loop (threshold auto-adjustment, prompt evolution)
   - Lead scoring, call analytics, deal intelligence
   - Platform sync with RateRight Admin API

3. Files and Code Sections:
   **Files successfully read (30 of 44):**

   - `/home/ccuser/rateright-growth/src/services/challengesServiceV2.js` (367 lines)
     - Manager challenges with real stakes. Exports: createChallenge, joinChallenge, getActiveChallenges, getChallengeDetails, updateChallengeScores, completeChallenge, cancelChallenge, processExpiredChallenges
     - Bug: line 293 `.catch(() => {})` silently swallows XP award errors
     - Bug: line 296 dynamic require inside function: `const { createCelebrationEvent } = require('./duelsService')`
     - Race condition: updateChallengeScores does sequential per-participant DB queries in a for loop (lines 194-235)

   - `/home/ccuser/rateright-growth/src/services/aiCallScoring.js` (441 lines)
     - AI-powered call scoring using GPT-4. Exports: scoreCall, getCallScore, getUserScores, getBestCalls, getCallsNeedingCoaching, getUserScoreStats, getScoreLeaderboard
     - Uses `supabaseAdmin || supabase` fallback pattern (line 9)
     - Truncates transcript at 8000 chars (line 137) - could miss context
     - Heuristic fallback scoring when OpenAI unavailable

   - `/home/ccuser/rateright-growth/src/services/activationAI.js` (469 lines)
     - AI-powered funnel analysis and nudge generation. Exports: analyzeBlockers, generateNudge, generateBulkNudges, suggestAction, analyzeStageDropoff
     - No DB queries - purely AI prompt service with fallback templates
     - generateBulkNudges processes leads sequentially (line 185) - no parallelism

   - `/home/ccuser/rateright-growth/src/services/challengesService.js` (429 lines)
     - Daily individual + team challenges. Exports: INDIVIDUAL_CHALLENGES, TEAM_CHALLENGES, generateDailyChallenges, generateTeamChallenge, updateChallengeProgress, getTodayChallenges, updateTeamChallengeProgress
     - Bug: challenge matching based on title string matching (lines 189-235) - extremely fragile
     - Race condition handled for duplicate key (23505) on lines 90-96 and 145-155
     - Dynamic require inside loop: `const xpService = require('./xpService')` (lines 260, 398)

   - `/home/ccuser/rateright-growth/src/services/badgesService.js` (353 lines)
     - Weekly badge awards. Exports: awardWeeklyBadges, getUserBadges, getCurrentWeekBadges, userHasBadge, getWeekStart
     - Performance issue: fires N+1 queries - one per user for call stats (lines 28-71)
     - Bug: "Consistent" badge only awards first qualifying user, not all (line 103)
     - Dynamic require: `const slackService = require('./slack')` (line 254)

   - `/home/ccuser/rateright-growth/src/services/callListRanker.js` (558 lines)
     - Lead priority scoring with 5-minute in-memory cache. Exports: computeLeadPriority, rankLeadsForCallList, buildRankingContext, refreshLeadPriority, computeUrgencyScore, computeIntentScore, computeTimingScore, computePatternScore, computeBaseScore
     - Well-structured with O(1) callback lookup via Map (line 380)
     - Tables: leads, callbacks, winning_patterns, conversion_patterns

   - `/home/ccuser/rateright-growth/src/services/callAnalytics.js` (802 lines)
     - Call quality metrics analysis. Exports: analyzeCall, getCallAnalytics, getLeadCallAnalytics, getQualityTrends, getCoachingAlerts, reviewCoachingAlert, getQualityLeaderboard
     - Bug: analyzeQuestions assumes ALL questions are from agent (line 275) - `agentTotal++` without speaker detection
     - callerTotal is never incremented (dead variable, line 253)

   - `/home/ccuser/rateright-growth/src/services/promptManagement.js` (373 lines)
     - Prompt version management. Exports: DEFAULT_PROMPTS, getActivePrompt, fillTemplate, recordPromptOutcome, getPromptPerformance, createPromptVersion, activatePromptVersion, getAllPromptStatus
     - Bug: uses `db.raw('usage_count + 1')` (line 167) - Supabase JS client does NOT support `.raw()`, this will fail silently due to `.catch(() => {})`
     - Same issue on line 203: `db.raw(`${updateField} + 1`)`
     - Race condition in activatePromptVersion: deactivate-then-activate (lines 309-321) is not atomic

   - `/home/ccuser/rateright-growth/src/services/learning.js` (470 lines)
     - Conversion pattern capture. Exports: captureConversionPattern, recordSuggestionFeedback, getEffectiveResponses, getSimilarWins
     - Uses Promise.allSettled correctly (line 27) for parallel queries with fallbacks
     - Tables: leads, communications, copilot_sessions, lead_notes, conversion_patterns, objection_responses, suggestion_feedback

   - `/home/ccuser/rateright-growth/src/services/duelsService.js` (610 lines)
     - XP duels/head-to-head challenges. Exports: DUEL_TYPES, calculateOdds, createDuel, acceptDuel, declineDuel, getUserDuels, getUserDuelHistory, updateDuelScores, completeDuel, createCelebrationEvent, getRecentCelebrations, processExpiredDuels
     - Race condition: updateDuelHistory (lines 493-530) is read-then-write without locking
     - Race condition: createDuel checks for existing duels then inserts - TOCTOU vulnerability (lines 162-201)
     - MAX_DAILY_LOSS_PERCENT constant defined (line 39) but never used - dead code

   - `/home/ccuser/rateright-growth/src/services/dealIntelligence.js` (602 lines)
     - Deal stage tracking and stall prediction. Exports: recordStageTransition, recordDealOutcome, analyzeObjectionImpact, findWinningResponses, getDealVelocity, predictStall, getLossReasons, getLossReasonOptions, getDealsAtRisk
     - Bug: updateObjectionOutcomes (line 158) updates ALL responses of a matching objection type, not just ones used for this specific lead
     - N+1 query in getDealsAtRisk: calls predictStall per lead (up to 50 leads, line 566)

   - `/home/ccuser/rateright-growth/src/services/selfImprovement.js` (434 lines)
     - Self-adjusting threshold system. Exports: getThresholds, saveThresholds, analyzeSuggestionPerformance, analyzeFrictionAccuracy, autoAdjustThresholds, learnFromSuccess, generateSelfImprovementReport, DEFAULT_THRESHOLDS
     - Tables: system_settings, intelligence_suggestions, detected_friction, intelligence_adjustments
     - Sends Slack notifications on threshold changes

   - `/home/ccuser/rateright-growth/src/services/platformSync.js` (550 lines)
     - Syncs users from RateRight platform to CRM. Exports: platformRequest, fetchPlatformUsers, fetchPlatformStats, fetchPlatformJobs, fetchPlatformContracts, syncPlatformUser, syncAllPlatformUsers, checkPlatformConnection, findExistingLead, getLeadType, getUserJobs, getUserContracts
     - External API: RateRight Admin API with X-API-Key auth and 30s timeout
     - Sequential processing of users in syncAllPlatformUsers (line 461) - no batching/parallelism

   - `/home/ccuser/rateright-growth/src/services/perplexity.js` (713 lines)
     - Web research via Perplexity AI. Exports: searchCompany, searchPerson, searchDecisionMaker, searchInterestNews, getCachedCompanyIntel, getCachedLeadIntel, normalizeCompanyName, autoResearchFromTranscript
     - External API: Perplexity AI (sonar model) with 45s timeout
     - 30-day cache in company_intel and lead_intel tables
     - Privacy concern: searchPerson scrapes personal info (family status, hobbies, social profiles)

   - `/home/ccuser/rateright-growth/src/services/repPerformance.js` (587 lines)
     - Rep performance analysis. Exports: generateWeeklyRepReport, aggregateRepWeeklyStats, analyzeRepPerformance, scoreCall, saveCallScore, saveWeeklySnapshot, getAllReps, formatDuration, formatHour
     - External API: OpenAI via callOpenAI for performance analysis
     - Tables: user_xp, communications, leads, call_quality_scores, rep_weekly_snapshots

   - `/home/ccuser/rateright-growth/src/services/compliance.js` (578 lines)
     - Brand guideline compliance checking. Exports: getGuidelines, clearGuidelinesCache, checkCompliance, autoFixMessage, validateScript, batchValidate, addToReviewQueue, checkToneWithAI
     - 2-hour cache for guidelines
     - checkToneWithAI lazy-loads ai.js to avoid circular dependency (line 535)

   - `/home/ccuser/rateright-growth/src/services/openai.js` (70 lines)
     - Thin OpenAI wrapper. Exports: generateChatCompletion, isConfigured
     - Clean, minimal. No error handling issues.

   - `/home/ccuser/rateright-growth/src/services/criticalAlerts.js` (536 lines)
     - Real-time Slack alerting. Exports: processEventForAlerts, checkRageClicks, checkErrorSpike, checkAbandonmentSpike, checkApiFailures, checkSmsDeliveryFailures, checkSlowApiResponses, runFullAlertCheck, sendCriticalAlert, THRESHOLDS
     - Bug: `setInterval` at module level (line 34) - runs even in test environments, prevents clean shutdown
     - Duplicate cleanup logic: shouldSendAlert (line 62) AND the setInterval both clean stale entries

   - `/home/ccuser/rateright-growth/src/services/leaderboardService.js` (531 lines)
     - Gamification leaderboards. Exports: RECORD_TYPES, WEEKLY_BADGES, STREAK_TIERS, getLeaderboard, checkOvertake, checkRecord, getHallOfFame, createPulseEvent, getTeamPulse, updateStreakTier, saveLeaderboardSnapshot, getTodaysCallCount, formatRecordName
     - AEST timezone handling throughout
     - Bug: getTodaysCallCount (line 499) doesn't filter by userId - counts ALL users' calls

   - `/home/ccuser/rateright-growth/src/services/deepgram.js` (86 lines)
     - Deepgram configuration. Exports: isConfigured, getClientConfig, getApiKey, getAuthenticatedWSUrl
     - **SECURITY: getClientConfig() returns the DEEPGRAM_API_KEY to the client (line 47)** - API key exposure
     - Comment on line 54 says "NEVER expose this to the client" but getClientConfig does exactly that

   - `/home/ccuser/rateright-growth/src/services/scoring.js` (278 lines)
     - Lead scoring. Exports: calculateLeadScore, updateLeadScore, recalculateAllScores, getScoreTier, SCORE_RULES
     - recalculateAllScores processes leads sequentially (line 246) - could be very slow
     - Tables: leads, communications, platform_activity

   - `/home/ccuser/rateright-growth/src/services/pushNotifications.js` (306 lines)
     - Web push notifications. Exports: sendPushNotification, broadcastPush, isQuietHours, notifyHighIntentSms, notifyNewSignup, notifyCallbackDue, notifyConversion
     - Properly handles expired subscriptions (410/404 status codes)
     - VAPID configuration at module level

   - `/home/ccuser/rateright-growth/src/services/weeklyIntelligence.js` (506 lines)
     - Weekly usage analysis. Exports: runWeeklyAnalysis, getLatestReport, getPageStats, getFlowStats, getErrorStats, getAIQualityStats
     - **BUG: Line 9 `const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })` - crashes at module load if OpenAI package present but API key missing** (no guard like other files)
     - Bug: getWeekStart() mutates `now` via `setDate()` (line 71) - side effect on the Date object

   - `/home/ccuser/rateright-growth/src/services/twilioMedia.js` (365 lines)
     - Twilio Media Streams to Deepgram transcription. Exports: handleTwilioMediaStream, subscribeToTranscripts, activeStreams
     - **Memory leak risk**: activeStreams Map (line 16) entries may not be cleaned up if WebSocket closes abnormally without 'stop' event
     - Audio mixing logic for stereo channels

   - `/home/ccuser/rateright-growth/src/services/twilioMediaSetup.js` (34 lines)
     - WebSocket server setup for Twilio Media. Exports: setupTwilioMediaStreams
     - **Security: Logs all request headers (line 21)** - could log sensitive auth headers

   - `/home/ccuser/rateright-growth/src/services/templates.js` (97 lines)
     - SMS template service. Exports: getTemplate, getAllTemplates, renderTemplate, buildLeadVariables
     - Clean, simple. Removes unmatched template variables (line 69).

   - `/home/ccuser/rateright-growth/src/services/slack.js` (1143 lines)
     - Slack notification service. Exports: sendSlackMessage, sendInboundAlert, sendInboundCallAlert, sendMissedCallAlert, sendVoicemailAlert, sendCallbackReminder, sendDailySummary, sendConversionAlert, sendWeeklyReport, sendBugReport, sendContractorReferralAlert, sendWebsiteSignupAlert, sendDevQueueItem, sendDevQueueReady, isConfigured
     - Rate limiting: 1 message/second (line 9)
     - Uses SLACK_WEBHOOK_URL environment variable

   - `/home/ccuser/rateright-growth/src/services/xpService.js` (210 lines)
     - XP award system. Exports: XP_AWARDS, LEVEL_NAMES, calculateLevel, awardXP, awardXPForCall, awardXPForSMS
     - Race condition: read-then-update on user_xp (lines 65-118) - concurrent XP awards could lose data
     - Tables: user_xp, xp_history

   **Files NOT yet read (14 remaining):**
   - wisdom.js, stageDiagnosis.js, stageCalculator.js
   - twimlWithStream.js, qualityAudit.js, ai.js, notifications.js
   - taskExecutor.js, taskParser.js, exportService.js, webhookDelivery.js
   - enrichment.js, jobLocking.js, twilio.js, cache.js

4. Errors and fixes:
   - Initial attempt to read `.cc-local/MEMORY.md` failed with "File does not exist" error. The sibling tool calls also errored due to the cascade. Fixed by re-running the `ls` command separately.

5. Problem Solving:
   - Organized the 44-file audit into batches of 4 files read in parallel to maximize efficiency
   - Created a todo list to track progress through the batches
   - Reading phase is approximately 68% complete (30 of 44 files read)
   - The actual audit report has NOT been written yet - still in data gathering phase

6. All user messages:
   - "Read EVERY file in /home/ccuser/rateright-growth/src/services/ and provide a detailed audit. For each file report: 1. What functions it exports and their purpose 2. Bug risks — race conditions, unhandled promises, logic errors 3. Error handling — are errors caught properly? Are they swallowed silently? 4. Dead code — unused exports, commented-out code, stale functions 5. Security issues — API key exposure, SQL injection, unsafe string interpolation in queries 6. External API calls — what third-party services are used? Are responses validated? 7. Database queries — what tables are accessed? Any raw SQL injection risks? The files to read are: - challengesServiceV2.js, aiCallScoring.js, activationAI.js, challengesService.js - badgesService.js, callListRanker.js, callAnalytics.js, promptManagement.js - learning.js, duelsService.js, dealIntelligence.js, selfImprovement.js - platformSync.js, perplexity.js, repPerformance.js, compliance.js - openai.js, criticalAlerts.js, leaderboardService.js, deepgram.js - scoring.js, pushNotifications.js, weeklyIntelligence.js - twilioMedia.js, twilioMediaSetup.js, templates.js, slack.js - xpService.js, wisdom.js, stageDiagnosis.js, stageCalculator.js - twimlWithStream.js, qualityAudit.js, ai.js, notifications.js - taskExecutor.js, taskParser.js, exportService.js, webhookDelivery.js - enrichment.js, jobLocking.js, twilio.js, cache.js Be thorough and brutal. Report EVERYTHING with specific line numbers and code snippets."

7. Pending Tasks:
   - Read remaining 14 files: wisdom.js, stageDiagnosis.js, stageCalculator.js, twimlWithStream.js, qualityAudit.js, ai.js, notifications.js, taskExecutor.js, taskParser.js, exportService.js, webhookDelivery.js, enrichment.js, jobLocking.js, twilio.js, cache.js
   - Compile the comprehensive audit report covering all 7 dimensions for all 44 files with specific line numbers and code snippets

8. Current Work:
   I was in the middle of reading service files in batches. The last batch read included twilioMedia.js, twilioMediaSetup.js, templates.js, slack.js, and xpService.js. The todo list shows batches 1-6 completed, batch 7 partially completed (twilioMedia, twilioMediaSetup, templates, slack read; plus xpService from batch 8 was read). Remaining files to read are in batches 8-11 per the todo list:
   - Batch 8: wisdom.js, stageDiagnosis.js, stageCalculator.js (xpService already read)
   - Batch 9: twimlWithStream.js, qualityAudit.js, ai.js, notifications.js
   - Batch 10: taskExecutor.js, taskParser.js, exportService.js, webhookDelivery.js
   - Batch 11: enrichment.js, jobLocking.js, twilio.js, cache.js
   - Final step: Compile comprehensive audit report

9. Optional Next Step:
   Continue reading the remaining 14 unread files (wisdom.js, stageDiagnosis.js, stageCalculator.js, twimlWithStream.js, qualityAudit.js, ai.js, notifications.js, taskExecutor.js, taskParser.js, exportService.js, webhookDelivery.js, enrichment.js, jobLocking.js, twilio.js, cache.js), then compile the full audit report with specific line numbers and code snippets as the user requested: "Be thorough and brutal. Report EVERYTHING with specific line numbers and code snippets."

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /home/ccuser/.claude/projects/-home-ccuser-rateright-growth/6138a2b4-4fed-4a8e-841b-ea0e29be8b8c.jsonl
Please continue the conversation from where we left it off without asking the user any further questions. Continue with the last task that you were asked to work on.


I'll continue reading the remaining unread files and then compile the full audit report. Let me read all remaining files now.


     1→/**
     2→ * Contextual AI Wisdom Service
     3→ * Generates situation-specific coaching based on user's current state
     4→ */
     5→
     6→const OpenAI = require('openai');
     7→const { supabase } = require('../config/database');
     8→const logger = require('../utils/logger');
     9→
    10→// Initialize OpenAI client
    11→let openai = null;
    12→if (process.env.OPENAI_API_KEY) {
    13→  openai = new OpenAI({
    14→    apiKey: process.env.OPENAI_API_KEY,
    15→  });
    16→}
    17→
    18→// In-memory cache for wisdom (per user per trigger)
    19→const wisdomCache = new Map();
    20→const CACHE_TTL = 2 * 60 * 60 * 1000; // 2 hours
    21→
    22→// Day names
    23→const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
    24→
    25→/**
    26→ * Get current time of day
    27→ */
    28→function getTimeOfDay(hour) {
    29→  if (hour >= 6 && hour < 10) return 'morning';
    30→  if (hour >= 10 && hour < 14) return 'midday';
    31→  if (hour >= 14 && hour < 18) return 'afternoon';
    32→  return 'evening';
    33→}
    34→
    35→/**
    36→ * Analyze dashboard data and build context object
    37→ */
    38→function analyzeContext(dashboardData) {
    39→  const now = new Date();
    40→  // Approximate AEST (UTC+10)
    41→  const aestHour = (now.getUTCHours() + 10) % 24;
    42→  const dayOfWeek = now.getDay();
    43→
    44→  const context = {
    45→    // Time context
    46→    hour: aestHour,
    47→    dayOfWeek,
    48→    dayName: DAY_NAMES[dayOfWeek],
    49→    timeOfDay: getTimeOfDay(aestHour),
    50→    timestamp: Date.now(),
    51→
    52→    // Activity context
    53→    callsToday: dashboardData.stats?.callsToday || 0,
    54→    callTarget: dashboardData.mission?.callTarget || 15,
    55→    callsToTarget: dashboardData.mission?.callsToTarget || 15,
    56→    percentToTarget: ((dashboardData.stats?.callsToday || 0) / (dashboardData.mission?.callTarget || 15)) * 100,
    57→
    58→    // Streak context
    59→    streak: dashboardData.stats?.streak || 0,
    60→
    61→    // Urgent context
    62→    overdueCallbacks: dashboardData.urgent?.overdueCallbacks?.length || 0,
    63→    hotLeadsStale: dashboardData.urgent?.hotLeadsStale?.length || 0,
    64→    smsWaiting: dashboardData.notifications?.smsReplies?.length || 0,
    65→
    66→    // Wins context
    67→    winsToday: dashboardData.stats?.winsToday || 0,
    68→    winsThisWeek: dashboardData.stats?.winsThisWeek || 0,
    69→
    70→    // Leads context
    71→    hotLeadsWaiting: dashboardData.mission?.hotLeadsWaiting || 0,
    72→    totalLeadsWaiting: dashboardData.leadCounts?.total || 0,
    73→  };
    74→
    75→  // Determine primary trigger
    76→  context.trigger = determineTrigger(context);
    77→
    78→  return context;
    79→}
    80→
    81→/**
    82→ * Determine the primary context trigger based on situation
    83→ * Priority order matters - most urgent/impactful first
    84→ */
    85→function determineTrigger(ctx) {
    86→  // Post-win energy (highest priority if recent win today)
    87→  if (ctx.winsToday > 0) {
    88→    return 'post_win';
    89→  }
    90→
    91→  // Broken streak (no activity, needs intervention)
    92→  if (ctx.streak === 0 && ctx.callsToday === 0 && ctx.hour >= 12) {
    93→    return 'streak_broken';
    94→  }
    95→
    96→  // Overdue callbacks (people waiting)
    97→  if (ctx.overdueCallbacks >= 2) {
    98→    return 'overdue_callbacks';
    99→  }
   100→
   101→  // SMS waiting (quick wins available)
   102→  if (ctx.smsWaiting >= 2) {
   103→    return 'sms_waiting';
   104→  }
   105→
   106→  // Zero calls late in day (urgent)
   107→  if (ctx.callsToday === 0 && ctx.hour >= 12) {
   108→    return 'zero_calls';
   109→  }
   110→
   111→  // Friday push
   112→  if (ctx.dayOfWeek === 5 && ctx.hour >= 15) {
   113→    return 'friday_push';
   114→  }
   115→
   116→  // Monday start
   117→  if (ctx.dayOfWeek === 1 && ctx.hour < 11) {
   118→    return 'monday_start';
   119→  }
   120→
   121→  // Behind target in afternoon
   122→  if (ctx.hour >= 14 && ctx.percentToTarget < 50) {
   123→    return 'behind_target';
   124→  }
   125→
   126→  // Afternoon slump
   127→  if (ctx.hour >= 14 && ctx.hour <= 16) {
   128→    return 'afternoon_slump';
   129→  }
   130→
   131→  // Hot leads going stale
   132→  if (ctx.hotLeadsStale >= 3) {
   133→    return 'hot_leads_stale';
   134→  }
   135→
   136→  // Strong streak
   137→  if (ctx.streak >= 5) {
   138→    return 'streak_strong';
   139→  }
   140→
   141→  // Crushing it
   142→  if (ctx.callsToday >= ctx.callTarget && ctx.winsToday > 0) {
   143→    return 'crushing_it';
   144→  }
   145→
   146→  // Winning streak
   147→  if (ctx.winsThisWeek >= 2) {
   148→    return 'winning_streak';
   149→  }
   150→
   151→  // End of day
   152→  if (ctx.hour >= 17) {
   153→    return 'end_of_day';
   154→  }
   155→
   156→  // Default
   157→  return 'general';
   158→}
   159→
   160→/**
   161→ * Check if context has changed significantly (warrants new wisdom)
   162→ */
   163→function hasContextChangedSignificantly(oldCtx, newCtx) {
   164→  if (!oldCtx) return true;
   165→
   166→  // Trigger changed
   167→  if (oldCtx.trigger !== newCtx.trigger) return true;
   168→
   169→  // Calls jumped by 3+
   170→  if (Math.abs(newCtx.callsToday - oldCtx.callsToday) >= 3) return true;
   171→
   172→  // New win
   173→  if (newCtx.winsToday > oldCtx.winsToday) return true;
   174→
   175→  // New overdue callback
   176→  if (newCtx.overdueCallbacks > oldCtx.overdueCallbacks) return true;
   177→
   178→  // Time period changed
   179→  if (newCtx.timeOfDay !== oldCtx.timeOfDay) return true;
   180→
   181→  // 2 hours elapsed
   182→  if (Date.now() - oldCtx.timestamp > CACHE_TTL) return true;
   183→
   184→  return false;
   185→}
   186→
   187→/**
   188→ * Generate wisdom using OpenAI
   189→ */
   190→async function generateWisdomWithAI(context) {
   191→  if (!openai) {
   192→    return null;
   193→  }
   194→
   195→  const prompt = `You're an elite sales coach for a construction industry rep in Australia.
   196→
   197→Their current situation:
   198→- Time: ${context.timeOfDay} on ${context.dayName}
   199→- Calls today: ${context.callsToday} / ${context.callTarget} target (${Math.round(context.percentToTarget)}% to target)
   200→- Streak: ${context.streak} consecutive active days
   201→- Overdue callbacks: ${context.overdueCallbacks} people waiting for a call back
   202→- Hot leads going cold: ${context.hotLeadsStale} (not contacted in 48+ hours)
   203→- SMS replies waiting: ${context.smsWaiting}
   204→- Wins today: ${context.winsToday}, this week: ${context.winsThisWeek}
   205→- Hot leads available: ${context.hotLeadsWaiting}
   206→- Total leads waiting: ${context.totalLeadsWaiting}
   207→
   208→Primary situation: ${context.trigger}
   209→
   210→Generate ONE line of wisdom specific to their situation right now.
   211→Not generic motivation. Speak directly to what they're facing.
   212→Tone: Direct, wise, slightly tough love. Like a mentor who's been there.
   213→No emojis. No fluff. Make it hit.
   214→
   215→Also provide a 2-3 sentence expansion that explains the concept deeper - something they'd want to read if they tapped for more.
   216→
   217→Return valid JSON only:
   218→{
   219→  "message": "The one-liner wisdom",
   220→  "expanded": "The deeper explanation (2-3 sentences)"
   221→}`;
   222→
   223→  try {
   224→    const response = await openai.chat.completions.create({
   225→      model: 'gpt-4o-mini',
   226→      messages: [{ role: 'user', content: prompt }],
   227→      temperature: 0.8,
   228→      max_tokens: 300,
   229→      response_format: { type: 'json_object' },
   230→    });
   231→
   232→    const result = JSON.parse(response.choices[0].message.content);
   233→    return {
   234→      message: result.message,
   235→      expanded: result.expanded,
   236→      source: 'ai',
   237→    };
   238→  } catch (error) {
   239→    logger.error('Failed to generate wisdom with AI', { error: error.message });
   240→    return null;
   241→  }
   242→}
   243→
   244→/**
   245→ * Get a curated fallback wisdom for the given trigger
   246→ */
   247→async function getCuratedWisdom(trigger, excludeIds = []) {
   248→  if (!supabase) {
   249→    // Return hardcoded fallback if no database
   250→    return {
   251→      id: 'fallback',
   252→      message: "The money's in the follow-up. Always has been.",
   253→      expanded: "Most deals don't close on the first call. They close on the fifth, the eighth, the twelfth. The rep who follows up wins.",
   254→      context_trigger: 'general',
   255→      source: 'curated',
   256→    };
   257→  }
   258→
   259→  try {
   260→    let query = supabase
   261→      .from('wisdom')
   262→      .select('*')
   263→      .eq('context_trigger', trigger)
   264→      .eq('source', 'curated');
   265→
   266→    if (excludeIds.length > 0) {
   267→      query = query.not('id', 'in', `(${excludeIds.join(',')})`);
   268→    }
   269→
   270→    const { data, error } = await query.limit(10);
   271→
   272→    if (error || !data || data.length === 0) {
   273→      // Fallback to general if no matches for trigger
   274→      const { data: generalData } = await supabase
   275→        .from('wisdom')
   276→        .select('*')
   277→        .eq('context_trigger', 'general')
   278→        .eq('source', 'curated')
   279→        .limit(5);
   280→
   281→      if (generalData && generalData.length > 0) {
   282→        return generalData[Math.floor(Math.random() * generalData.length)];
   283→      }
   284→
   285→      // Ultimate fallback
   286→      return {
   287→        id: 'fallback',
   288→        message: "The money's in the follow-up. Always has been.",
   289→        expanded: "Most deals don't close on the first call. The rep who follows up wins.",
   290→        context_trigger: 'general',
   291→        source: 'curated',
   292→      };
   293→    }
   294→
   295→    // Random selection from matching wisdoms
   296→    return data[Math.floor(Math.random() * data.length)];
   297→  } catch (error) {
   298→    logger.error('Failed to get curated wisdom', { error: error.message });
   299→    return {
   300→      id: 'fallback',
   301→      message: "The money's in the follow-up. Always has been.",
   302→      expanded: "Most deals don't close on the first call. The rep who follows up wins.",
   303→      context_trigger: 'general',
   304→      source: 'curated',
   305→    };
   306→  }
   307→}
   308→
   309→/**
   310→ * Save AI-generated wisdom to database for future reuse
   311→ */
   312→async function saveWisdom(wisdom, trigger) {
   313→  if (!supabase || wisdom.source !== 'ai') return;
   314→
   315→  try {
   316→    await supabase.from('wisdom').insert({
   317→      message: wisdom.message,
   318→      expanded: wisdom.expanded,
   319→      context_trigger: trigger,
   320→      source: 'ai',
   321→    });
   322→  } catch (error) {
   323→    logger.error('Failed to save wisdom', { error: error.message });
   324→  }
   325→}
   326→
   327→/**
   328→ * Record that wisdom was shown to a user
   329→ */
   330→async function recordWisdomShown(userId, wisdomId, context) {
   331→  if (!supabase || !userId || !wisdomId || wisdomId === 'fallback') return;
   332→
   333→  try {
   334→    await supabase.from('wisdom_shown').insert({
   335→      user_id: userId,
   336→      wisdom_id: wisdomId,
   337→      context_snapshot: context,
   338→    });
   339→
   340→    // Increment times_shown
   341→    await supabase.rpc('increment_wisdom_shown', { wisdom_id: wisdomId });
   342→  } catch (error) {
   343→    // Non-critical, just log
   344→    logger.warn('Failed to record wisdom shown', { error: error.message });
   345→  }
   346→}
   347→
   348→/**
   349→ * Get recently shown wisdom IDs for a user (today)
   350→ */
   351→async function getRecentlyShownIds(userId) {
   352→  if (!supabase || !userId) return [];
   353→
   354→  try {
   355→    const today = new Date().toISOString().split('T')[0];
   356→    const { data } = await supabase
   357→      .from('wisdom_shown')
   358→      .select('wisdom_id')
   359→      .eq('user_id', userId)
   360→      .gte('shown_at', `${today}T00:00:00Z`)
   361→      .order('shown_at', { ascending: false })
   362→      .limit(5);
   363→
   364→    return data?.map(d => d.wisdom_id) || [];
   365→  } catch (error) {
   366→    return [];
   367→  }
   368→}
   369→
   370→/**
   371→ * Main function: Get wisdom for the current context
   372→ */
   373→async function getWisdom(dashboardData, userId = null) {
   374→  const context = analyzeContext(dashboardData);
   375→  const cacheKey = userId ? `${userId}:${context.trigger}` : context.trigger;
   376→
   377→  // Check cache
   378→  const cached = wisdomCache.get(cacheKey);
   379→  if (cached && !hasContextChangedSignificantly(cached.context, context)) {
   380→    return {
   381→      ...cached.wisdom,
   382→      context_trigger: cached.context.trigger,
   383→      cached: true,
   384→    };
   385→  }
   386→
   387→  // Get recently shown IDs to avoid repeats
   388→  const excludeIds = await getRecentlyShownIds(userId);
   389→
   390→  // Try AI generation first
   391→  let wisdom = await generateWisdomWithAI(context);
   392→
   393→  if (wisdom) {
   394→    // Save AI wisdom to database for future use
   395→    const savedResult = await saveWisdomToDb(wisdom, context.trigger);
   396→    wisdom.id = savedResult?.id || null;
   397→  } else {
   398→    // Fallback to curated
   399→    wisdom = await getCuratedWisdom(context.trigger, excludeIds);
   400→  }
   401→
   402→  // Cache the result
   403→  wisdomCache.set(cacheKey, {
   404→    wisdom,
   405→    context,
   406→    timestamp: Date.now(),
   407→  });
   408→
   409→  // Record that we showed this wisdom
   410→  if (wisdom.id && userId) {
   411→    await recordWisdomShown(userId, wisdom.id, context);
   412→  }
   413→
   414→  return {
   415→    message: wisdom.message,
   416→    expanded: wisdom.expanded,
   417→    context_trigger: context.trigger,
   418→    wisdomId: wisdom.id || null,
   419→    source: wisdom.source,
   420→    cached: false,
   421→  };
   422→}
   423→
   424→/**
   425→ * Save wisdom to database
   426→ */
   427→async function saveWisdomToDb(wisdom, trigger) {
   428→  if (!supabase) return null;
   429→
   430→  try {
   431→    const { data, error } = await supabase
   432→      .from('wisdom')
   433→      .insert({
   434→        message: wisdom.message,
   435→        expanded: wisdom.expanded,
   436→        context_trigger: trigger,
   437→        source: 'ai',
   438→      })
   439→      .select()
   440→      .single();
   441→
   442→    if (error) throw error;
   443→    return data;
   444→  } catch (error) {
   445→    logger.error('Failed to save wisdom to db', { error: error.message });
   446→    return null;
   447→  }
   448→}
   449→
   450→/**
   451→ * Rate a wisdom (thumbs up/down)
   452→ */
   453→async function rateWisdom(wisdomId, rating, userId) {
   454→  if (!supabase || !wisdomId || wisdomId === 'fallback') {
   455→    return { success: false, error: 'Invalid wisdom ID' };
   456→  }
   457→
   458→  try {
   459→    // Update the wisdom_shown record with rating
   460→    await supabase
   461→      .from('wisdom_shown')
   462→      .update({ rating })
   463→      .eq('user_id', userId)
   464→      .eq('wisdom_id', wisdomId)
   465→      .order('shown_at', { ascending: false })
   466→      .limit(1);
   467→
   468→    // Increment thumbs_up or thumbs_down on wisdom
   469→    const column = rating === 'up' ? 'thumbs_up' : 'thumbs_down';
   470→    const { error } = await supabase.rpc('increment_wisdom_rating', {
   471→      wisdom_id: wisdomId,
   472→      rating_type: column,
   473→    });
   474→
   475→    if (error) throw error;
   476→
   477→    return { success: true };
   478→  } catch (error) {
   479→    logger.error('Failed to rate wisdom', { error: error.message });
   480→    return { success: false, error: error.message };
   481→  }
   482→}
   483→
   484→module.exports = {
   485→  getWisdom,
   486→  rateWisdom,
   487→  analyzeContext,
   488→  determineTrigger,
   489→};
   490→

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