Skip to content

feat(focaccia-58247): broadcast script for GPP Party Guide email - #576

Draft
snackman wants to merge 8 commits into
masterfrom
focaccia-58247-gpp-guide-email
Draft

snackman wants to merge 8 commits into
masterfrom
focaccia-58247-gpp-guide-email

Conversation

@snackman

Copy link
Copy Markdown
Contributor

Summary

One-off CLI under backend/scripts/ to email approved GPP 2026 hosts (and their non-partner co-hosts) a link to their Party Guide tab.

  • Filter: event_type='gpp' AND underboss_status='approved'
  • Co-hosts where isPartner !== true are included (sponsor/brand contacts excluded)
  • Dedups by lowercased email; host role wins over cohost; soonest-upcoming event provides the link
  • Reuses Resend + the 10/500ms batching pattern from backend/src/routes/v1/guests.ts
  • List-Unsubscribe + List-Unsubscribe-Post headers on every send
  • Three modes: --dry-run, --send --confirm, --send --only=a@b.com,c@d.com
  • --skip-already-sent reads the prior send log for retry idempotency

Plan: plans/focaccia-58247.md (also in this PR for reference).

What this PR does NOT do

  • No deploy. Script runs locally from Snax's machine against prod DB + Resend.
  • No schema change. Pure read-only query.
  • No backend route, no frontend change. Standalone script — does not import from backend/src/ or @prisma/client.

Test plan

  • Vercel preview build passes (no frontend changes — should be a no-op build)
  • node -c backend/scripts/send-gpp-guide-email.cjs exits 0 (syntax check)
  • node backend/scripts/send-gpp-guide-email.cjs --help prints CLI surface
  • Dry-run review: Snax fills bullet 1/2/3 + sign-off, runs --dry-run, opens /tmp/gpp-guide-sample.html in browser, verifies layout + Party Guide link
  • Spot-check partner filter: confirm a known isPartner=true co-host email is NOT in the dry-run CSV
  • Test send: --send --only=samgold24@gmail.com; confirm Gmail delivery + List-Unsubscribe header visible in "show original"
  • Full send: --send --confirm; expect ~500-700 recipients across ~5-10 min wall time
  • Send log shows >=98% sent

Follow-ups (separate tasks)

  • Real suppression infra (users.broadcast_unsubscribed_at + /unsubscribe?token=...) before the next broadcast
  • Reusable /underboss broadcast UI if we send broadcasts more than 2-3 times

🤖 Generated with Claude Code

One-off CLI under backend/scripts/ that sends approved GPP 2026 hosts and
their non-partner co-hosts a link to /host/{invite_code}/party-guide.

- Filters: event_type='gpp' AND underboss_status='approved'
- Includes co-hosts where isPartner !== true (excludes sponsor/brand contacts)
- Dedups by lowercased email; prefers host role over cohost; soonest event wins
- Resend send with 10/500ms batching (mirrors v1/guests bulk-invite pattern)
- List-Unsubscribe + List-Unsubscribe-Post headers on every send
- --dry-run writes CSV + sample HTML to OS temp dir (no network)
- --send --confirm requires explicit confirm; --skip-already-sent idempotency
- --send --only=a@b.com,c@d.com for single-recipient test mode

Bullet copy in the email body is left as {bullet 1/2/3} placeholders;
Snax fills these in before --send.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rsvpizza Ready Ready Preview, Comment May 22, 2026 9:01am

Request Review

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- "User" model has no @@Map so the Postgres table is "User" (case-sensitive,
  quoted), not unquoted lowercase "users". Caught at first dry-run.
  Mirrors the gotcha in feedback_prisma_table_name_verify memory.
- Secrets live in backend/.env.local on dev machines (RESEND_API_KEY in
  particular), with backend/.env carrying empty placeholders. Load .env.local
  first when present, then .env fills any gaps. Standard dotenv convention.

Tested with --send --only=snax@rarepizzas.com; recipient list = 710 deduped
(376 hosts + 334 cohosts; 1,165 partner cohosts excluded).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…coordinators

- Email now lists every approved upcoming GPP event the recipient hosts or
  co-hosts (sorted soonest first), with one CTA button per event.
- Single-event recipients still get the single-CTA layout — visually identical
  to the v1 send except for the date filter.
- SQL adds (p.date IS NULL OR p.date >= CURRENT_DATE) to skip past events.
  Sending a Party Guide link for an event that already happened is just spam.
- Exclude recipients with >20 events as a hard cap. These are coordinator/
  admin accounts added as co-hosts at scale (hello@rarepizzas.com has 399);
  emailing them a 400-button list is spam and a deliverability risk. 12
  recipients excluded; 696 remain.
- CSV gains event_count + pipe-joined event_names/urls columns.
- Sample HTML now appends the most-events recipient so the multi-event
  rendering is visually reviewable in a dry-run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…izza Party'

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Hero swaps the pizza-slice emoji for the Molto Benny mascot image
  (https://rsv.pizza/molto-benny.png).
- Each event renders as a card with venue name, address, formatted
  date/time (timezone-aware), and an "Open Party Guide" button.
- The soonest event also gets a "See you in N hours/days/minutes"
  countdown line (omitted past 7 days out).
- SQL pulls p.address and p.venue_name alongside the existing date+timezone.
- Single-event recipients still see one card; multi-event recipients see
  a stack of cards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… limit)

The bulk-invite pattern (BATCH_SIZE=10, 500ms delay) bursts ~20 req/sec,
which exceeds Resend's 5 req/sec hard limit on our tier. First production
run: 172 sent / 529 failed (all 429 rate_limit_exceeded).

Drop to 4 in parallel + 1100ms delay = ~3.6 req/sec, safe margin under 5.
Wall time goes from ~30s to ~3min for 700 sends — fine for a one-off blast.

The mirrored bulk-invite endpoint in routes/v1/guests.ts has the same bug;
it just hasn't been noticed at <50-invite batch sizes (which complete before
the rate-limit window enforces). Not fixing that here; separate task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Allow require()-ing the email builder from a one-off node script — useful
when an address needs to be sent to but can't be surfaced via the DB-driven
recipient pool (e.g., typo'd User.email collides with a clean duplicate
account at the corrected address, so a plain UPDATE is blocked by the
User_email_lower_unique constraint).

Used today to deliver to 3 typo'd addresses after the main blast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant