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:
+44
-1
@@ -294,6 +294,7 @@ function CalendarClient() {
|
||||
|
||||
// Drag state
|
||||
const [dragOverKey, setDragOverKey] = useState<string | null>(null)
|
||||
const [calFlash, setCalFlash] = useState('')
|
||||
const dragDataRef = useRef<{ type: 'episode' | 'event'; id: string } | null>(null)
|
||||
|
||||
// ── Load ──
|
||||
@@ -475,6 +476,16 @@ function CalendarClient() {
|
||||
const newReminder = Number(editForm.reminderDays)
|
||||
const dateChanged = newDate !== (editEp.datePublished?.trim() ?? '')
|
||||
const reminderChanged = newReminder !== (editEp.reminderDays ?? 0)
|
||||
|
||||
let nextStatus = (editForm.productionStatus as ProductionStatus) || undefined
|
||||
if (dateChanged && newDate) {
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
const isPast = newDate <= today
|
||||
if (!nextStatus || nextStatus === 'idea') {
|
||||
nextStatus = isPast ? 'published' : 'scheduled'
|
||||
}
|
||||
}
|
||||
|
||||
const updated = checklist.episodes.map(ep =>
|
||||
ep.id === editEp.id
|
||||
? {
|
||||
@@ -486,7 +497,7 @@ function CalendarClient() {
|
||||
startTime: editForm.startTime || undefined,
|
||||
reminderDays: newReminder,
|
||||
reminderSentAt: dateChanged || reminderChanged ? undefined : ep.reminderSentAt,
|
||||
productionStatus: (editForm.productionStatus as ProductionStatus) || undefined,
|
||||
productionStatus: nextStatus,
|
||||
}
|
||||
: ep
|
||||
)
|
||||
@@ -494,6 +505,36 @@ function CalendarClient() {
|
||||
setEditEp(null)
|
||||
}
|
||||
|
||||
async function generateTasks(ep: PodcastChecklistEpisode) {
|
||||
if (!ep.datePublished) return
|
||||
const epDate = new Date(ep.datePublished + 'T12:00:00')
|
||||
const label = ep.title || [ep.series, ep.episodeNumber != null ? `Ep. ${ep.episodeNumber}` : null].filter(Boolean).join(' ')
|
||||
const tasks = [
|
||||
{ offsetDays: -14, title: `Record: ${label}` },
|
||||
{ offsetDays: -7, title: `Edit: ${label}` },
|
||||
]
|
||||
for (const t of tasks) {
|
||||
const d = new Date(epDate)
|
||||
d.setDate(d.getDate() + t.offsetDays)
|
||||
const dateKey = d.toISOString().slice(0, 10)
|
||||
try {
|
||||
const res = await fetch('/api/admin-calendar-events', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ type: 'task', title: t.title, date: dateKey, notes: '', reminderDays: 0 }),
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json() as { event: CalendarEvent }
|
||||
setEvents(prev => [data.event, ...prev])
|
||||
}
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
setCalFlash(`Tasks generated for "${label}"`)
|
||||
setTimeout(() => setCalFlash(''), 3000)
|
||||
setEditEp(null)
|
||||
}
|
||||
|
||||
function unschedule(ep: PodcastChecklistEpisode) {
|
||||
if (!checklist) return
|
||||
const updated = checklist.episodes.map(e => e.id === ep.id ? { ...e, datePublished: '' } : e)
|
||||
@@ -782,6 +823,7 @@ function CalendarClient() {
|
||||
<button type="button" className="cal-nav-btn" onClick={nextPeriod} aria-label="Next">›</button>
|
||||
<button type="button" className="em-btn em-btn--ghost em-btn--sm" onClick={gotoToday}>Today</button>
|
||||
{saving && <span className="cal-saving">Saving…</span>}
|
||||
{calFlash && <span className="cal-flash-msg">{calFlash}</span>}
|
||||
</div>
|
||||
<div className="cal-header-center">
|
||||
<div className="cal-view-toggle">
|
||||
@@ -977,6 +1019,7 @@ function CalendarClient() {
|
||||
<div className="cal-popover-actions">
|
||||
<button type="button" className="em-btn em-btn--primary em-btn--sm" onClick={saveEdit}>Save</button>
|
||||
<button type="button" className="em-btn em-btn--ghost em-btn--sm" onClick={() => announceEpisode(editEp)} title="Open email compose pre-filled to announce this episode">✉ Announce</button>
|
||||
{editEp.datePublished && <button type="button" className="em-btn em-btn--ghost em-btn--sm" title="Create Record (−14d) and Edit (−7d) task events" onClick={() => generateTasks(editEp)}>⚙ Tasks</button>}
|
||||
<button type="button" className="em-btn em-btn--ghost em-btn--sm" onClick={() => unschedule(editEp)}>Unschedule</button>
|
||||
<button type="button" className="em-btn em-btn--ghost em-btn--sm" onClick={() => setEditEp(null)}>Cancel</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user