From e601311ca330d610ae4ce76dca951d4725cc6c3b Mon Sep 17 00:00:00 2001 From: paul Date: Wed, 16 Jul 2025 15:04:55 +0200 Subject: [PATCH] fix: resolve email queue discrepancy between admin dashboard and processor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The issue: Admin dashboard showed pending emails that the processor wouldn't process because they had exceeded retry limits (retry_count >= 3). Changes: - Updated backend /admin/system/status to provide detailed email queue stats: - pending: total pending emails (as before) - processable: emails that will actually be processed (retry_count < 3) - stuck: emails that exceeded retry limit but are still pending - Updated frontend to display stuck emails with warning when present - Created debug-email-queue.js script to diagnose discrepancies - Created fix-stuck-emails.js script to handle stuck emails: - Can reset retry count, mark as failed, or delete - Usage: node fix-stuck-emails.js [reset|fail|delete] This makes it clear when emails are stuck and won't be processed automatically. šŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- backend/scripts/debug-email-queue.js | 146 ++++++++++++++++++++++ backend/scripts/fix-stuck-emails.js | 126 +++++++++++++++++++ backend/src/routes/adminSystem.js | 10 ++ frontend/src/pages/admin/SettingsPage.tsx | 17 ++- frontend/src/services/admin.service.ts | 3 + frontend/src/services/settings.service.ts | 2 + 6 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 backend/scripts/debug-email-queue.js create mode 100644 backend/scripts/fix-stuck-emails.js diff --git a/backend/scripts/debug-email-queue.js b/backend/scripts/debug-email-queue.js new file mode 100644 index 0000000..ea6d76a --- /dev/null +++ b/backend/scripts/debug-email-queue.js @@ -0,0 +1,146 @@ +const { db } = require('../src/database/db'); +const winston = require('winston'); + +// Create a simple console logger +const logger = winston.createLogger({ + format: winston.format.simple(), + transports: [new winston.transports.Console()] +}); + +async function debugEmailQueue() { + try { + logger.info('=== Email Queue Debug Report ===\n'); + + // 1. Count exactly like the admin dashboard does + logger.info('1. Admin Dashboard Query (ALL pending, no retry filter):'); + const [adminCount] = await db('email_queue').where('status', 'pending').count('* as count'); + logger.info(` Pending emails (admin dashboard view): ${adminCount.count}\n`); + + // 2. Count like the email processor does + logger.info('2. Email Processor Query (pending with retry_count < 3):'); + const [processorCount] = await db('email_queue') + .where('status', 'pending') + .where('retry_count', '<', 3) + .count('* as count'); + logger.info(` Pending emails (processor view): ${processorCount.count}\n`); + + // 3. Show the discrepancy + logger.info('3. Discrepancy Analysis:'); + if (adminCount.count !== processorCount.count) { + logger.info(` āš ļø DISCREPANCY FOUND!`); + logger.info(` Admin shows: ${adminCount.count}`); + logger.info(` Processor will process: ${processorCount.count}`); + logger.info(` Difference: ${adminCount.count - processorCount.count} email(s)\n`); + + // Find the problematic emails + logger.info('4. Emails with retry_count >= 3 (still pending):'); + const stuckEmails = await db('email_queue') + .where('status', 'pending') + .where('retry_count', '>=', 3) + .select('*'); + + if (stuckEmails.length > 0) { + logger.info(` Found ${stuckEmails.length} stuck email(s):\n`); + stuckEmails.forEach((email, index) => { + logger.info(` Email ${index + 1}:`); + logger.info(` ID: ${email.id}`); + logger.info(` Type: ${email.email_type}`); + logger.info(` Recipient: ${email.recipient_email}`); + logger.info(` Status: ${email.status}`); + logger.info(` Retry Count: ${email.retry_count} āš ļø`); + logger.info(` Created: ${email.created_at}`); + logger.info(` Last Error: ${email.error_message || 'None'}\n`); + }); + } + } else { + logger.info(` āœ… No discrepancy - counts match\n`); + } + + // 5. Show ALL pending emails with details + logger.info('5. ALL Pending Emails (regardless of retry count):'); + const allPending = await db('email_queue') + .where('status', 'pending') + .orderBy('retry_count', 'desc') + .orderBy('created_at', 'asc'); + + if (allPending.length > 0) { + allPending.forEach((email, index) => { + const willProcess = email.retry_count < 3; + logger.info(`\n Email ${index + 1}: ${willProcess ? 'āœ… WILL PROCESS' : 'āŒ STUCK (max retries)'}`); + logger.info(` ID: ${email.id}`); + logger.info(` Type: ${email.email_type}`); + logger.info(` Recipient: ${email.recipient_email}`); + logger.info(` Event ID: ${email.event_id}`); + logger.info(` Retry Count: ${email.retry_count}/3`); + logger.info(` Created: ${email.created_at}`); + logger.info(` Scheduled: ${email.scheduled_at}`); + if (email.error_message) { + logger.info(` Last Error: ${email.error_message}`); + } + }); + } else { + logger.info(' No pending emails found'); + } + + // 6. Show counts by status + logger.info('\n\n6. Email Queue Summary by Status:'); + const statusCounts = await db('email_queue') + .select('status') + .count('* as count') + .groupBy('status') + .orderBy('status'); + + statusCounts.forEach(row => { + logger.info(` ${row.status}: ${row.count}`); + }); + + // 7. Failed emails summary + logger.info('\n7. Failed Emails Summary:'); + const failedSummary = await db('email_queue') + .where('status', 'failed') + .select('retry_count') + .count('* as count') + .groupBy('retry_count') + .orderBy('retry_count'); + + if (failedSummary.length > 0) { + failedSummary.forEach(row => { + logger.info(` Retry count ${row.retry_count}: ${row.count} email(s)`); + }); + } else { + logger.info(' No failed emails'); + } + + // 8. Recommendations + logger.info('\n\n=== RECOMMENDATIONS ==='); + + if (adminCount.count > processorCount.count) { + logger.info('\nā— You have emails stuck with retry_count >= 3'); + logger.info(' These emails will NOT be processed automatically.'); + logger.info('\n To fix this, you can:'); + logger.info(' 1. Reset retry count: UPDATE email_queue SET retry_count = 0 WHERE status = \'pending\' AND retry_count >= 3;'); + logger.info(' 2. Mark as failed: UPDATE email_queue SET status = \'failed\' WHERE status = \'pending\' AND retry_count >= 3;'); + logger.info(' 3. Delete them: DELETE FROM email_queue WHERE status = \'pending\' AND retry_count >= 3;'); + } + + const anyPending = adminCount.count > 0; + if (anyPending && processorCount.count === 0) { + logger.info('\nā— All pending emails have exceeded retry limit'); + logger.info(' The email processor will not attempt to send them.'); + } else if (anyPending && processorCount.count > 0) { + logger.info('\nāœ… Email processor should process the pending emails on next run'); + logger.info(' Make sure the email processor service is running.'); + } + + logger.info('\n=== Debug report complete ==='); + + } catch (error) { + logger.error('Error running debug report:', error); + } finally { + await db.destroy(); + process.exit(0); + } +} + +// Run the debug +debugEmailQueue(); \ No newline at end of file diff --git a/backend/scripts/fix-stuck-emails.js b/backend/scripts/fix-stuck-emails.js new file mode 100644 index 0000000..812486c --- /dev/null +++ b/backend/scripts/fix-stuck-emails.js @@ -0,0 +1,126 @@ +const { db } = require('../src/database/db'); +const winston = require('winston'); + +// Create a simple console logger +const logger = winston.createLogger({ + format: winston.format.simple(), + transports: [new winston.transports.Console()] +}); + +async function fixStuckEmails() { + try { + logger.info('=== Fix Stuck Emails Script ===\n'); + + // 1. Find stuck emails + logger.info('1. Finding stuck emails (pending with retry_count >= 3)...'); + const stuckEmails = await db('email_queue') + .where('status', 'pending') + .where('retry_count', '>=', 3) + .select('*'); + + if (stuckEmails.length === 0) { + logger.info(' āœ… No stuck emails found!'); + logger.info('\n=== Script complete ==='); + await db.destroy(); + process.exit(0); + } + + logger.info(` Found ${stuckEmails.length} stuck email(s)\n`); + + // 2. Show details + logger.info('2. Stuck email details:'); + stuckEmails.forEach((email, index) => { + logger.info(`\n Email ${index + 1}:`); + logger.info(` ID: ${email.id}`); + logger.info(` Type: ${email.email_type}`); + logger.info(` Recipient: ${email.recipient_email}`); + logger.info(` Retry Count: ${email.retry_count}`); + logger.info(` Last Error: ${email.error_message || 'None'}`); + }); + + // 3. Ask for action + logger.info('\n\n3. Choose an action:'); + logger.info(' 1. Reset retry count to 0 (emails will be retried)'); + logger.info(' 2. Mark as failed (emails will not be retried)'); + logger.info(' 3. Delete these emails'); + logger.info(' 4. Cancel (do nothing)'); + + // Get command line argument + const action = process.argv[2]; + + if (!action || !['reset', 'fail', 'delete'].includes(action)) { + logger.info('\nā— No valid action specified'); + logger.info('\nUsage:'); + logger.info(' node fix-stuck-emails.js reset - Reset retry count to 0'); + logger.info(' node fix-stuck-emails.js fail - Mark as failed'); + logger.info(' node fix-stuck-emails.js delete - Delete stuck emails'); + await db.destroy(); + process.exit(1); + } + + // 4. Execute action + logger.info(`\n4. Executing action: ${action.toUpperCase()}`); + + const emailIds = stuckEmails.map(e => e.id); + + switch (action) { + case 'reset': + await db('email_queue') + .whereIn('id', emailIds) + .update({ + retry_count: 0, + error_message: null + }); + logger.info(` āœ… Reset retry count for ${emailIds.length} email(s)`); + logger.info(' These emails will be processed on the next run'); + break; + + case 'fail': + await db('email_queue') + .whereIn('id', emailIds) + .update({ + status: 'failed' + }); + logger.info(` āœ… Marked ${emailIds.length} email(s) as failed`); + logger.info(' These emails will not be retried'); + break; + + case 'delete': + await db('email_queue') + .whereIn('id', emailIds) + .delete(); + logger.info(` āœ… Deleted ${emailIds.length} email(s)`); + break; + } + + // 5. Show updated counts + logger.info('\n5. Updated email queue status:'); + const [pendingCount] = await db('email_queue') + .where('status', 'pending') + .count('* as count'); + const [processableCount] = await db('email_queue') + .where('status', 'pending') + .where('retry_count', '<', 3) + .count('* as count'); + + logger.info(` Total pending: ${pendingCount.count}`); + logger.info(` Processable (retry < 3): ${processableCount.count}`); + + if (pendingCount.count !== processableCount.count) { + logger.info(` āš ļø Still have ${pendingCount.count - processableCount.count} stuck email(s)`); + } else { + logger.info(' āœ… No stuck emails remaining'); + } + + logger.info('\n=== Script complete ==='); + + } catch (error) { + logger.error('Error:', error); + } finally { + await db.destroy(); + process.exit(0); + } +} + +// Run the fix +fixStuckEmails(); \ No newline at end of file diff --git a/backend/src/routes/adminSystem.js b/backend/src/routes/adminSystem.js index 9b67325..e56407d 100644 --- a/backend/src/routes/adminSystem.js +++ b/backend/src/routes/adminSystem.js @@ -70,8 +70,16 @@ router.get('/status', adminAuth, async (req, res) => { // Email queue status const [pendingEmails] = await db('email_queue').where('status', 'pending').count('* as count'); + const [processableEmails] = await db('email_queue') + .where('status', 'pending') + .where('retry_count', '<', 3) + .count('* as count'); const [sentEmails] = await db('email_queue').where('status', 'sent').count('* as count'); const [failedEmails] = await db('email_queue').where('status', 'failed').count('* as count'); + const [stuckEmails] = await db('email_queue') + .where('status', 'pending') + .where('retry_count', '>=', 3) + .count('* as count'); // Activity logs count const [activityCount] = await db('activity_logs').count('* as count'); @@ -139,6 +147,8 @@ router.get('/status', adminAuth, async (req, res) => { }, emailQueue: { pending: pendingEmails.count, + processable: processableEmails.count, + stuck: stuckEmails.count, sent: sentEmails.count, failed: failedEmails.count }, diff --git a/frontend/src/pages/admin/SettingsPage.tsx b/frontend/src/pages/admin/SettingsPage.tsx index e29e252..3b79dac 100644 --- a/frontend/src/pages/admin/SettingsPage.tsx +++ b/frontend/src/pages/admin/SettingsPage.tsx @@ -551,7 +551,14 @@ export const SettingsPage: React.FC = () => {
{t('settings.systemStatus.pending')}: - {systemStatus.emailQueue.pending} + + {systemStatus.emailQueue.pending} + {systemStatus.emailQueue.stuck > 0 && ( + + ({systemStatus.emailQueue.stuck} stuck) + + )} +
{t('settings.systemStatus.sent')}: @@ -562,6 +569,14 @@ export const SettingsPage: React.FC = () => { {systemStatus.emailQueue.failed}
+ {systemStatus.emailQueue.stuck > 0 && ( +
+

+ āš ļø {systemStatus.emailQueue.stuck} email(s) stuck: These emails have exceeded retry limits and won't be processed automatically. + Only {systemStatus.emailQueue.processable} of {systemStatus.emailQueue.pending} pending emails will be processed. +

+
+ )} diff --git a/frontend/src/services/admin.service.ts b/frontend/src/services/admin.service.ts index 520963f..4928a60 100644 --- a/frontend/src/services/admin.service.ts +++ b/frontend/src/services/admin.service.ts @@ -23,6 +23,9 @@ export interface SystemHealth { details: { emailQueue: { pending: number; + processable: number; + stuck: number; + sent: number; failed: number; }; memory: { diff --git a/frontend/src/services/settings.service.ts b/frontend/src/services/settings.service.ts index 5deed19..5852fea 100644 --- a/frontend/src/services/settings.service.ts +++ b/frontend/src/services/settings.service.ts @@ -54,6 +54,8 @@ export interface SystemStatus { }; emailQueue: { pending: number; + processable: number; + stuck: number; sent: number; failed: number; };