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:
@@ -33,6 +33,7 @@ import {
|
||||
EMAIL_SETTINGS_FILE,
|
||||
CALENDAR_EVENTS_FILE,
|
||||
AUDIT_LOG_FILE,
|
||||
WEBHOOKS_FILE,
|
||||
EMPTY_HIT_STATS,
|
||||
EMPTY_VISITOR_STATS,
|
||||
DEFAULT_REPLY_TEMPLATES,
|
||||
@@ -475,6 +476,48 @@ export function loadAuditLogFromDisk() {
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// ── Webhooks ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function queueWebhooksWrite() {
|
||||
state.webhooksWritePromise = state.webhooksWritePromise
|
||||
.then(async () => {
|
||||
await mkdir(DATA_DIR, { recursive: true })
|
||||
await writeFile(WEBHOOKS_FILE, JSON.stringify({ webhooks: state.webhooks }, null, 2), 'utf8')
|
||||
})
|
||||
.catch(err => { console.error('[webhooks] write failed:', err) })
|
||||
}
|
||||
|
||||
export function loadWebhooksFromDisk() {
|
||||
return readFile(WEBHOOKS_FILE, 'utf8')
|
||||
.then(raw => {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (Array.isArray(parsed?.webhooks)) state.webhooks = parsed.webhooks.slice(0, 50)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
export async function fireWebhooks(eventType, payload) {
|
||||
const targets = state.webhooks.filter(w =>
|
||||
Array.isArray(w.events) && (w.events.includes(eventType) || w.events.includes('*'))
|
||||
)
|
||||
if (targets.length === 0) return
|
||||
const body = JSON.stringify({ event: eventType, data: payload, firedAt: new Date().toISOString() })
|
||||
for (const wh of targets) {
|
||||
fetch(wh.url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'User-Agent': 'Siteforge-Webhook/1.0' },
|
||||
body,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
})
|
||||
.then(r => {
|
||||
appendAuditEntry('webhook-fired', `${eventType} → ${wh.url} (${r.status})`)
|
||||
})
|
||||
.catch(err => {
|
||||
appendAuditEntry('webhook-error', `${eventType} → ${wh.url}: ${String(err?.message ?? err).slice(0, 200)}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Podcast checklist ──────────────────────────────────────────────────────
|
||||
|
||||
export function queuePodcastChecklistWrite() {
|
||||
|
||||
Reference in New Issue
Block a user