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.