Add email reminders to calendar episodes; v1.1.15

- Per-episode reminder select (1d/2d/3d/1wk/2wk before publish date)
- Server checks hourly; fires Resend email to admin on reminder day
- reminderSentAt persisted so reminders don't re-fire; cleared if date or setting changes
- Bell indicator on calendar chips; sent confirmation in edit popover

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-07-28 11:12:17 -04:00
parent 0161a0dacd
commit de201ff356
5 changed files with 150 additions and 5 deletions
+88
View File
@@ -0,0 +1,88 @@
import { Resend } from 'resend'
import { state } from './state.js'
import { queuePodcastChecklistWrite } from './data.js'
import { DEFAULT_RESEND_FROM, DEFAULT_RESEND_TO } from './config.js'
function toDateKey(date) {
return date.toISOString().slice(0, 10)
}
export function startReminderScheduler() {
checkReminders()
setInterval(checkReminders, 60 * 60 * 1000)
}
async function checkReminders() {
const episodes = state.podcastChecklist?.episodes
if (!Array.isArray(episodes)) return
const todayKey = toDateKey(new Date())
let changed = false
for (const ep of episodes) {
if (!ep.datePublished || !(ep.reminderDays > 0) || ep.reminderSentAt) continue
const publish = new Date(ep.datePublished + 'T12:00:00Z')
if (isNaN(publish.getTime())) continue
const reminderDate = new Date(publish)
reminderDate.setUTCDate(reminderDate.getUTCDate() - ep.reminderDays)
const reminderKey = toDateKey(reminderDate)
if (todayKey >= reminderKey && todayKey <= toDateKey(publish)) {
const sent = await sendReminderEmail(ep)
if (sent) {
ep.reminderSentAt = new Date().toISOString()
changed = true
}
}
}
if (changed) queuePodcastChecklistWrite()
}
async function sendReminderEmail(ep) {
if (!process.env.RESEND_API_KEY) return false
try {
const resend = new Resend(process.env.RESEND_API_KEY)
const from = process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM
const to = process.env.RESEND_TO ?? DEFAULT_RESEND_TO
const publishDate = new Date(ep.datePublished + 'T12:00:00Z')
const msLeft = publishDate.getTime() - Date.now()
const daysLeft = Math.max(0, Math.ceil(msLeft / (1000 * 60 * 60 * 24)))
const label = ep.episodeNumber ? `Episode ${ep.episodeNumber}` : 'Episode'
const title = ep.title || 'Untitled'
const series = ep.series ? ` (${ep.series})` : ''
const daysText = daysLeft === 0 ? 'today' : daysLeft === 1 ? 'in 1 day' : `in ${daysLeft} days`
const subject = `Reminder: "${label}: ${title}" publishes ${daysText}`
const html = `
<div style="font-family:system-ui,sans-serif;max-width:540px;margin:0 auto;color:#222">
<h2 style="color:#c8860a;margin-bottom:4px">📅 Release Reminder</h2>
<p style="font-size:1.1rem;margin-bottom:16px">
<strong>${label}: ${title}</strong>${series}<br>
<span style="color:#555">Publishes <strong>${ep.datePublished}</strong> — ${daysText}</span>
</p>
<hr style="border:none;border-top:1px solid #eee;margin:16px 0">
<p style="color:#777;font-size:0.85rem">
This reminder was set ${ep.reminderDays} day${ep.reminderDays === 1 ? '' : 's'} before the publish date.
To change or remove it, open the Calendar in your admin panel.
</p>
</div>
`
const { error } = await resend.emails.send({ from, to, subject, html })
if (error) {
console.error('[reminder] send error:', error)
return false
}
console.log(`[reminder] sent for episode "${title}" (${ep.datePublished})`)
return true
} catch (err) {
console.error('[reminder] send exception:', err)
return false
}
}