Send admin emails via a background job with live status reporting #462

Closed
opened 2026-09-04 08:33:37 +02:00 by johnsturgeon · 0 comments
Owner

Background

The admin compose-email page (#445, PR #461) sends synchronously inside the POST
handler: it loops the selected players, awaits a full SMTP round trip each, and
only then renders the bounce page. At pool size (~30) that is a few seconds, but
it holds the request open and is prone to gateway/worker timeouts as the list
grows. The bounce page's delivery lights are rendered once from the finished
results, so there is nothing live about them.

What we want

Move the fan-out off the request and onto a background job, with the same
job-reporting the rest of the app already has, and push per-recipient send
status to the page as it happens.

Sending

  • Enqueue the send as an APScheduler job (see app/jobs/scheduler.py,
    SQLAlchemyJobStore) rather than looping in send_admin_email.
  • The POST returns immediately to a landing page; the job does the SMTP work.
  • Reuse custom_email_body so the sent message matches the preview.

Job reporting

  • Track the run the way scheduled jobs are tracked (JobState /
    last_success_at / last_error), so a failed batch is visible on the
    admin schedule page alongside everything else.
  • Record per-recipient outcome (sent / failed) somewhere the landing page can
    read it back -- a small status row per send, keyed by a batch id.

Live status on the landing page

  • The landing page shows the recipient list with a status light per player,
    same as the current bounce page, but the lights start neutral and flip to
    green/red as the job reports each send.
  • AJAX poll (or SSE) an endpoint that returns the batch's per-recipient status;
    stop polling once every recipient is terminal.
  • This is the piece the current synchronous version deliberately skips -- see
    the note in PR #461: live lights only earn their keep once sending is
    backgrounded.

Out of scope

  • Retry logic for failed sends (log to Sentry as today; a manual resend is fine).
  • Scheduling a send for a future time (this is fire-now, just off-request).

Part of #460.

## Background The admin compose-email page (#445, PR #461) sends synchronously inside the POST handler: it loops the selected players, `await`s a full SMTP round trip each, and only then renders the bounce page. At pool size (~30) that is a few seconds, but it holds the request open and is prone to gateway/worker timeouts as the list grows. The bounce page's delivery lights are rendered once from the finished results, so there is nothing live about them. ## What we want Move the fan-out off the request and onto a background job, with the same job-reporting the rest of the app already has, and push per-recipient send status to the page as it happens. ### Sending - Enqueue the send as an APScheduler job (see `app/jobs/scheduler.py`, `SQLAlchemyJobStore`) rather than looping in `send_admin_email`. - The POST returns immediately to a landing page; the job does the SMTP work. - Reuse `custom_email_body` so the sent message matches the preview. ### Job reporting - Track the run the way scheduled jobs are tracked (`JobState` / `last_success_at` / `last_error`), so a failed batch is visible on the admin schedule page alongside everything else. - Record per-recipient outcome (sent / failed) somewhere the landing page can read it back -- a small status row per send, keyed by a batch id. ### Live status on the landing page - The landing page shows the recipient list with a status light per player, same as the current bounce page, but the lights start neutral and flip to green/red as the job reports each send. - AJAX poll (or SSE) an endpoint that returns the batch's per-recipient status; stop polling once every recipient is terminal. - This is the piece the current synchronous version deliberately skips -- see the note in PR #461: live lights only earn their keep once sending is backgrounded. ## Out of scope - Retry logic for failed sends (log to Sentry as today; a manual resend is fine). - Scheduling a send for a future time (this is fire-now, just off-request). Part of #460.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
johnsturgeon/tgfp-web#462
No description provided.