Add Tier 2 remaining features: publishing pipeline, broadcasts, audit log, drip trigger, today banner; v1.1.25
- CalendarPage: productionStatus field on episodes (idea→recorded→edited→scheduled→published), color dot on chips, dropdown in edit popover; "publishing today" banner with draft announcement button - EmailPage: Broadcasts drawer (plain-text → Resend Broadcasts API); Audit Log drawer showing activity history; em-status-msg--ok style - ContactsPage: ▶ Drip button in edit mode syncs contact to Resend audience and triggers drip automation - server: appendAuditEntry() logged on reply-sent, email-sent, submission-deleted, contacts-merged, contacts-imported, broadcast-sent, drip-triggered; GET /api/admin-audit-log; POST /api/admin-broadcasts/send; POST /api/admin-contacts/trigger-drip; AUDIT_LOG_FILE persisted to data/audit-log.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -240,6 +240,10 @@ function ContactsClient() {
|
||||
const [mergeSearch, setMergeSearch] = useState('')
|
||||
const [mergeBusy, setMergeBusy] = useState(false)
|
||||
const [mergeMsg, setMergeMsg] = useState('')
|
||||
// Drip trigger
|
||||
const [dripBusyKey, setDripBusyKey] = useState<string | null>(null)
|
||||
const [dripMsgKey, setDripMsgKey] = useState<string | null>(null)
|
||||
const [dripMsgText, setDripMsgText] = useState('')
|
||||
|
||||
// History state
|
||||
const [expandedHistoryKey, setExpandedHistoryKey] = useState<string | null>(null)
|
||||
@@ -447,6 +451,20 @@ function ContactsClient() {
|
||||
|
||||
// ── Merge ──
|
||||
|
||||
async function triggerDrip(c: Contact) {
|
||||
setDripBusyKey(c.key); setDripMsgKey(c.key); setDripMsgText('')
|
||||
try {
|
||||
const res = await fetch('/api/admin-contacts/trigger-drip', {
|
||||
method: 'POST', credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: c.email }),
|
||||
})
|
||||
const d = await res.json() as { ok?: boolean; note?: string; message?: string }
|
||||
setDripMsgText(res.ok ? (d.note ?? 'Drip triggered.') : (d.message ?? 'Failed.'))
|
||||
} catch { setDripMsgText('Network error.') }
|
||||
setDripBusyKey(null)
|
||||
}
|
||||
|
||||
async function doMerge(keepContact: Contact, mergeContact: Contact) {
|
||||
if (!confirm(`Merge "${mergeContact.name || mergeContact.email}" into "${keepContact.name || keepContact.email}"? All messages from ${mergeContact.email} will be reassigned to ${keepContact.email}.`)) return
|
||||
setMergeBusy(true)
|
||||
@@ -735,8 +753,16 @@ function ContactsClient() {
|
||||
<button type="button" className="em-btn em-btn--ghost em-btn--sm" onClick={() => { setMergePickerKey(prev => prev === c.key ? null : c.key); setMergeSearch(''); setMergeMsg('') }}>
|
||||
{isMergeTarget ? 'Cancel merge' : 'Merge with…'}
|
||||
</button>
|
||||
{c.email && (
|
||||
<button type="button" className="em-btn em-btn--ghost em-btn--sm" title="Sync to Resend audience and trigger drip automation" onClick={() => triggerDrip(c)} disabled={dripBusyKey === c.key}>
|
||||
{dripBusyKey === c.key ? 'Triggering…' : '▶ Drip'}
|
||||
</button>
|
||||
)}
|
||||
<button type="button" className="em-btn em-btn--ghost em-btn--sm" onClick={cancelEdit}>Cancel</button>
|
||||
</div>
|
||||
{dripMsgKey === c.key && dripMsgText && (
|
||||
<p className="ct-drip-msg">{dripMsgText}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* ── Display mode ── */
|
||||
|
||||
Reference in New Issue
Block a user