fix(email): show a queue nobody is working instead of reporting all-clear
Closes #1262. "Gallery email queued" reads as a delivery confirmation, and System Health agreed with it: "No stuck or failed emails -- all clear", while not one email had gone out. Both statements were true and neither was the one the admin needed. Queueing writes an email_queue row at status='pending', retry_count 0 -- nothing more. /failures matched only status='failed' or pending-with-retry_count>=3, so it matched none of those rows, and there are two ordinary ways they never leave that state: - startEmailQueueProcessor() was never reached, so nothing polls the queue. - Every pass returns early. processEmailQueue bails when the transporter will not initialise, before it touches a single row, so retry_count stays 0 and no error_message is ever written. A working SMTP test button does not contradict this: that path builds its own transport. adminSystem.js made it worse by reporting `emailProcessor: { status: 'active' }` as a literal, so the one place that named the worker always said it was fine. - emailProcessor records what each pass did -- started, lastRunAt, lastResult, lastError -- and exports getQueueProcessorStatus(). The transporter bail and the queue-query failure, the two silent early returns, both write lastError. - /failures gains `waitingEmails`: pending, under the retry cap, past any scheduled_at, and queued more than 10 minutes ago. The predicate mirrors the processor's own pickup query, so a row listed there is one it should already have taken; rows over the cap stay in `stuckEmails` and are not counted twice. A future scheduled_at is left alone -- split-payment invoices and the business-hours floor park rows deliberately. - System Health leads with the processor's state (running / stopped / degraded) and lists waiting emails in their own table. The all-clear now needs both buckets empty. - adminSystem reports the real processor state instead of the literal. - The two "queued" toasts say the queue processor is what sends it and where to look if it doesn't arrive. 8 route tests, all 8 failing before the change.
This commit is contained in:
@@ -922,9 +922,32 @@ async function renderQueuedEmail(templateKey, variables = {}, to = '') {
|
||||
// because ignoreSchedule also bypasses that cap.
|
||||
//
|
||||
// Returns { processed, sent, failed }.
|
||||
// What the last pass actually did, so System Health can say whether the queue
|
||||
// is being worked at all (#1262). "Queued" is not "delivered", and the two
|
||||
// ways a queue silently stops -- the processor never started, or every pass
|
||||
// returns early because the transport will not initialise -- both leave rows
|
||||
// at status='pending' with retry_count 0, which no failure query matches.
|
||||
const processorStatus = {
|
||||
started: false,
|
||||
lastRunAt: null,
|
||||
lastResult: null,
|
||||
lastError: null,
|
||||
};
|
||||
|
||||
function getQueueProcessorStatus() {
|
||||
return {
|
||||
started: processorStatus.started,
|
||||
lastRunAt: processorStatus.lastRunAt,
|
||||
lastResult: processorStatus.lastResult,
|
||||
lastError: processorStatus.lastError,
|
||||
};
|
||||
}
|
||||
|
||||
async function processEmailQueue({ ignoreSchedule = false, limit = 10, onlyId = null } = {}) {
|
||||
logger.info('Email queue processor: Checking for pending emails...');
|
||||
const result = { processed: 0, sent: 0, failed: 0 };
|
||||
processorStatus.lastRunAt = new Date().toISOString();
|
||||
processorStatus.lastError = null;
|
||||
|
||||
try {
|
||||
// Try to initialize transporter if it's null (in case it failed at startup).
|
||||
@@ -937,6 +960,10 @@ async function processEmailQueue({ ignoreSchedule = false, limit = 10, onlyId =
|
||||
transporter = await initializeTransporter();
|
||||
if (!transporter) {
|
||||
logger.warn('Email transporter could not be initialized, skipping queue processing');
|
||||
// #1262 — the row stays pending with retry_count 0, so nothing in the
|
||||
// queue itself records that this pass did nothing. Say so here.
|
||||
processorStatus.lastError = 'Email transporter could not be initialised — check the SMTP settings';
|
||||
processorStatus.lastResult = result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -969,6 +996,8 @@ async function processEmailQueue({ ignoreSchedule = false, limit = 10, onlyId =
|
||||
.limit(limit);
|
||||
} catch (dbError) {
|
||||
logger.error('Failed to query email queue:', dbError);
|
||||
processorStatus.lastError = dbError.message;
|
||||
processorStatus.lastResult = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1043,8 +1072,10 @@ async function processEmailQueue({ ignoreSchedule = false, limit = 10, onlyId =
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error processing email queue:', error);
|
||||
processorStatus.lastError = error.message;
|
||||
}
|
||||
|
||||
processorStatus.lastResult = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1203,6 +1234,7 @@ function startEmailQueueProcessor() {
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
processorStatus.started = true;
|
||||
logger.info('Email queue processor started successfully');
|
||||
} else {
|
||||
logger.info('Email queue processor: Already running');
|
||||
@@ -1213,6 +1245,7 @@ function stopEmailQueueProcessor() {
|
||||
if (emailQueueInterval) {
|
||||
clearInterval(emailQueueInterval);
|
||||
emailQueueInterval = null;
|
||||
processorStatus.started = false;
|
||||
logger.info('Email queue processor stopped');
|
||||
}
|
||||
}
|
||||
@@ -1231,6 +1264,7 @@ module.exports = {
|
||||
sendRawEmail,
|
||||
renderQueuedEmail,
|
||||
processEmailQueue,
|
||||
getQueueProcessorStatus,
|
||||
queueEmail,
|
||||
stopEmailQueueProcessor,
|
||||
testEmailConnection,
|
||||
|
||||
Reference in New Issue
Block a user