Add webhook outbound, Contact→Calendar, checklist automation; v1.1.26
- Webhook CRUD (GET/POST/DELETE /api/admin-webhooks, POST .../test) with ⚡ Webhooks drawer in EmailPage; fires contact.new and reply.sent events - Contact→Calendar: history panel shows checklist episodes within ±30 days - Checklist automation: saveEdit auto-sets productionStatus (idea→scheduled for future dates, idea→published for past); ⚙ Tasks button generates Record (−14d) and Edit (−7d) calendar task events for scheduled episodes - Audit log captures webhook-added, webhook-removed, webhook-test events - Webhook infrastructure: state, disk persistence, fire-and-forget with AbortSignal.timeout(8000), loaded on startup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@ import {
|
||||
sanitizeReplyTemplates,
|
||||
sanitizeReplyHistory,
|
||||
appendAuditEntry,
|
||||
queueWebhooksWrite,
|
||||
fireWebhooks,
|
||||
} from '../data.js'
|
||||
import {
|
||||
noteContactEmailCooldown,
|
||||
@@ -154,6 +156,7 @@ export function register(app) {
|
||||
state.contactSubmissions.unshift(submission)
|
||||
state.contactSubmissions = state.contactSubmissions.slice(0, MAX_CONTACT_SUBMISSIONS)
|
||||
queueContactSubmissionsWrite()
|
||||
fireWebhooks('contact.new', { name: trimmedName, email: trimmedEmail, message: trimmedMessage, messageType: normalizedMessageType, submittedAt: submission.submittedAt })
|
||||
|
||||
if (normalizedMessageType === 'question') {
|
||||
const question = {
|
||||
@@ -693,6 +696,7 @@ export function register(app) {
|
||||
state.replyHistory = state.replyHistory.slice(0, 500)
|
||||
queueReplyHistoryWrite()
|
||||
appendAuditEntry('reply-sent', `To: ${submission.email} | Subject: ${subject}${scheduledAt ? ` | Scheduled: ${scheduledAt}` : ''}`)
|
||||
fireWebhooks('reply.sent', { toEmail: submission.email, toName: submission.name, subject, sentAt: new Date().toISOString() })
|
||||
|
||||
res.json({ ok: true, scheduled: Boolean(scheduledAt), scheduledAt: scheduledAt ?? null })
|
||||
} catch (err) {
|
||||
@@ -928,4 +932,52 @@ export function register(app) {
|
||||
res.status(502).json({ message: 'AI draft request failed.' })
|
||||
}
|
||||
})
|
||||
|
||||
// ── Webhook CRUD ───────────────────────────────────────────────────────────
|
||||
|
||||
app.get('/api/admin-webhooks', requireAdminAuth, (_req, res) => {
|
||||
res.json({ webhooks: state.webhooks })
|
||||
})
|
||||
|
||||
app.post('/api/admin-webhooks', requireAdminAuth, (req, res) => {
|
||||
const url = typeof req.body?.url === 'string' ? req.body.url.trim() : ''
|
||||
const events = Array.isArray(req.body?.events) ? req.body.events.filter(e => typeof e === 'string' && e.trim()) : ['*']
|
||||
const label = typeof req.body?.label === 'string' ? req.body.label.trim().slice(0, 100) : ''
|
||||
if (!url || !/^https?:\/\/./.test(url)) {
|
||||
res.status(400).json({ message: 'A valid http/https URL is required.' }); return
|
||||
}
|
||||
const webhook = { id: randomUUID(), url: url.slice(0, 500), label, events, createdAt: new Date().toISOString() }
|
||||
state.webhooks.push(webhook)
|
||||
state.webhooks = state.webhooks.slice(0, 50)
|
||||
queueWebhooksWrite()
|
||||
appendAuditEntry('webhook-added', url)
|
||||
res.json({ ok: true, webhook })
|
||||
})
|
||||
|
||||
app.delete('/api/admin-webhooks/:id', requireAdminAuth, (req, res) => {
|
||||
const { id } = req.params
|
||||
const before = state.webhooks.length
|
||||
state.webhooks = state.webhooks.filter(w => w.id !== id)
|
||||
if (state.webhooks.length === before) { res.status(404).json({ message: 'Webhook not found.' }); return }
|
||||
queueWebhooksWrite()
|
||||
appendAuditEntry('webhook-removed', `id: ${id}`)
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
app.post('/api/admin-webhooks/:id/test', requireAdminAuth, async (req, res) => {
|
||||
const wh = state.webhooks.find(w => w.id === req.params.id)
|
||||
if (!wh) { res.status(404).json({ message: 'Webhook not found.' }); return }
|
||||
try {
|
||||
const r = await fetch(wh.url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'User-Agent': 'Siteforge-Webhook/1.0' },
|
||||
body: JSON.stringify({ event: 'test', data: { message: 'Test event from Siteforge.' }, firedAt: new Date().toISOString() }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
})
|
||||
appendAuditEntry('webhook-test', `${wh.url} → ${r.status}`)
|
||||
res.json({ ok: true, status: r.status })
|
||||
} catch (err) {
|
||||
res.status(502).json({ message: String(err?.message ?? 'Test failed.') })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user