Add Compare with Resend panel to Subscribers; stop resubscribing opted-out contacts; v2.6.1
Publish Container / build (push) Successful in 16m27s

- Subscribers: compare in-app list with Resend audience/segment, with one-click fixes
- syncContactToResend no longer resets unsubscribed on existing contacts

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-01 08:29:56 -04:00
1 parent e74bd1df5d
commit af967d0049
5 files changed
+273 -6

No files matched your search

+145
View File
@@ -429,6 +429,151 @@ export function register(app) {
res.send(csv)
})
// ── Subscribers ⇄ Resend comparison ─────────────────────────────────────────
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
function getAppSubscribers() {
const byEmail = new Map()
for (const entry of state.contactSubmissions) {
if (entry.subscribe !== true || !entry.email) continue
const email = String(entry.email).trim().toLowerCase()
if (!byEmail.has(email)) byEmail.set(email, { name: entry.name ?? '', email })
}
return byEmail
}
async function listAllResendContacts(resend, segmentId) {
const out = []
let after
for (let page = 0; page < 50; page += 1) {
const { data, error } = await resend.contacts.list({ limit: 100, ...(segmentId ? { segmentId } : {}), ...(after ? { after } : {}) })
if (error) throw new Error(error.message || 'Resend contacts.list failed')
const rows = data?.data ?? []
out.push(...rows)
if (!data?.has_more || rows.length === 0) break
after = rows[rows.length - 1].id
await sleep(550) // Resend allows ~2 requests/second
}
return out
}
async function buildResendComparison() {
const resend = new Resend(process.env.RESEND_CONTACTS_API_KEY ?? process.env.RESEND_API_KEY)
const segmentId = process.env.RESEND_SEGMENT_ID || null
const appSubs = getAppSubscribers()
const resendContacts = await listAllResendContacts(resend)
let segmentEmails = null
if (segmentId) {
await sleep(550)
segmentEmails = new Set((await listAllResendContacts(resend, segmentId)).map(c => String(c.email).toLowerCase()))
}
const resendByEmail = new Map(resendContacts.map(c => [String(c.email).toLowerCase(), c]))
const nameOf = c => [c.first_name, c.last_name].filter(Boolean).join(' ')
const onlyInApp = []
const unsubscribedInResend = []
const notInSegment = []
let matched = 0
for (const sub of appSubs.values()) {
const contact = resendByEmail.get(sub.email)
if (!contact) { onlyInApp.push(sub); continue }
if (contact.unsubscribed) { unsubscribedInResend.push(sub); continue }
if (segmentEmails && !segmentEmails.has(sub.email)) { notInSegment.push(sub); continue }
matched += 1
}
const onlyInResend = resendContacts
.filter(c => !appSubs.has(String(c.email).toLowerCase()) && !c.unsubscribed)
.map(c => ({ name: nameOf(c), email: String(c.email).toLowerCase() }))
return {
appCount: appSubs.size,
resendCount: resendContacts.length,
resendUnsubscribedCount: resendContacts.filter(c => c.unsubscribed).length,
segmentConfigured: Boolean(segmentId),
segmentCount: segmentEmails ? segmentEmails.size : null,
matched,
onlyInApp,
onlyInResend,
unsubscribedInResend,
notInSegment,
}
}
app.get('/api/admin-subscribers/resend-compare', requireAdminAuth, async (_req, res) => {
if (!process.env.RESEND_API_KEY) {
res.status(500).json({ message: 'RESEND_API_KEY is not configured.' }); return
}
try {
res.json(await buildResendComparison())
} catch (err) {
console.error('[subscribers] resend compare failed:', err)
res.status(502).json({ message: 'Could not read contacts from Resend. Try again in a moment.' })
}
})
app.post('/api/admin-subscribers/resend-sync', requireAdminAuth, async (req, res) => {
const { action } = req.body ?? {}
if (!['add-to-resend', 'add-to-segment', 'apply-unsubscribes'].includes(action)) {
res.status(400).json({ message: 'Unknown action.' }); return
}
if (!process.env.RESEND_API_KEY) {
res.status(500).json({ message: 'RESEND_API_KEY is not configured.' }); return
}
try {
const diff = await buildResendComparison()
if (action === 'apply-unsubscribes') {
const emails = new Set(diff.unsubscribedInResend.map(s => s.email))
let changed = 0
state.contactSubmissions = state.contactSubmissions.map(entry => {
if (entry.subscribe === true && emails.has(String(entry.email ?? '').trim().toLowerCase())) {
changed += 1
return { ...entry, subscribe: false }
}
return entry
})
if (changed > 0) queueContactSubmissionsWrite()
res.json({ ok: true, done: emails.size, failed: 0 }); return
}
const resend = new Resend(process.env.RESEND_CONTACTS_API_KEY ?? process.env.RESEND_API_KEY)
const segmentId = process.env.RESEND_SEGMENT_ID || null
const targets = (action === 'add-to-resend' ? diff.onlyInApp : diff.notInSegment).slice(0, 100)
if (action === 'add-to-segment' && !segmentId) {
res.status(400).json({ message: 'RESEND_SEGMENT_ID is not configured.' }); return
}
let done = 0
let failed = 0
for (const target of targets) {
try {
if (action === 'add-to-resend') {
const { firstName, lastName } = splitName(target.name)
const { error } = await resend.contacts.create({
email: target.email, firstName, lastName, unsubscribed: false,
...(segmentId ? { segments: [{ id: segmentId }] } : {}),
})
if (error) throw new Error(error.message)
} else {
const { error } = await resend.contacts.segments.add({ email: target.email, segmentId })
if (error) throw new Error(error.message)
}
done += 1
} catch (err) {
failed += 1
console.error(`[subscribers] resend sync failed for ${target.email}:`, err?.message ?? err)
}
await sleep(550)
}
const total = action === 'add-to-resend' ? diff.onlyInApp.length : diff.notInSegment.length
res.json({ ok: true, done, failed, remaining: Math.max(total - targets.length, 0) })
} catch (err) {
console.error('[subscribers] resend sync failed:', err)
res.status(502).json({ message: 'Resend sync failed. Try again in a moment.' })
}
})
app.get('/api/admin-broadcast/templates', requireAdminAuth, async (_req, res) => {
if (!process.env.RESEND_API_KEY) {
res.status(500).json({ message: 'RESEND_API_KEY is not configured.' }); return