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:
nmemmert
2026-07-29 08:03:30 -04:00
parent f5c4557e9f
commit 111b559865
10 changed files with 322 additions and 25 deletions
+43
View File
@@ -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() {