v1.2.0: Broadcast emails, webhook admin UI, dynamic sitemap, scripture index
Publish Container / docker (push) Successful in 14m11s

- Admin > Audience > Broadcast Email: compose and send one-off HTML emails
  to all subscribers via Resend batch API; {{name}} personalization; confirm step
- Admin > Configure > Webhooks: CRUD UI for managing POST event webhooks;
  backend GET/POST/DELETE /api/admin-webhooks routes wired to existing state.webhooks
- Sitemap: dynamically appends released study lesson URLs (/study/:slug/:id)
  and episode highlight URLs (/episodes/:id) to /sitemap.xml
- Scripture Index: public /scripture page listing every released lesson grouped
  by study with passage reference links; served by /api/public/scripture-index

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-09-03 09:54:43 -04:00
1 parent 0bf2d92b1f
commit 63d4f9e316
7 files changed
+348 -3

No files matched your search

+36
View File
@@ -25,6 +25,7 @@ import {
queueReplyTemplatesWrite,
queueReplyHistoryWrite,
queuePodcastChecklistWrite,
queueWebhooksWrite,
createBackupSnapshot,
listBackupPreviews,
readBackupPreview,
@@ -331,4 +332,39 @@ export function register(app) {
res.status(400).json({ message: err instanceof Error ? err.message : 'Restore failed.' })
}
})
// ── Webhooks ──────────────────────────────────────────────────────────────
app.get('/api/admin-webhooks', requireAdminAuth, (_req, res) => {
res.json({ webhooks: state.webhooks })
})
app.post('/api/admin-webhooks', requireAdminAuth, (req, res) => {
const { url, events } = req.body ?? {}
if (!url || typeof url !== 'string' || !/^https:\/\//i.test(url.trim())) {
res.status(400).json({ message: 'A valid HTTPS URL is required.' })
return
}
if (state.webhooks.length >= 50) {
res.status(400).json({ message: 'Maximum of 50 webhooks reached.' })
return
}
const webhook = {
id: crypto.randomUUID(),
url: url.trim().slice(0, 500),
events: Array.isArray(events) ? events.map(String).slice(0, 20) : (typeof events === 'string' && events.trim() ? events.split(',').map(e => e.trim()).filter(Boolean) : ['*']),
createdAt: new Date().toISOString(),
}
state.webhooks.push(webhook)
queueWebhooksWrite()
res.status(201).json({ webhook })
})
app.delete('/api/admin-webhooks/:id', requireAdminAuth, (req, res) => {
const idx = state.webhooks.findIndex(w => w.id === req.params.id)
if (idx === -1) { res.status(404).json({ message: 'Not found.' }); return }
state.webhooks.splice(idx, 1)
queueWebhooksWrite()
res.json({ ok: true })
})
}