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:
+31
-1
@@ -32,6 +32,7 @@ import {
|
||||
ANALYTICS_EVENTS_FILE,
|
||||
EMAIL_SETTINGS_FILE,
|
||||
CALENDAR_EVENTS_FILE,
|
||||
AUDIT_LOG_FILE,
|
||||
EMPTY_HIT_STATS,
|
||||
EMPTY_VISITOR_STATS,
|
||||
DEFAULT_REPLY_TEMPLATES,
|
||||
@@ -447,6 +448,33 @@ export function loadCalendarEventsFromDisk() {
|
||||
})
|
||||
}
|
||||
|
||||
// ── Audit log ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function appendAuditEntry(action, details) {
|
||||
state.auditLog.unshift({
|
||||
id: randomUUID(),
|
||||
action: String(action).slice(0, 80),
|
||||
details: typeof details === 'string' ? details.slice(0, 400) : JSON.stringify(details ?? {}).slice(0, 400),
|
||||
at: new Date().toISOString(),
|
||||
})
|
||||
if (state.auditLog.length > 500) state.auditLog = state.auditLog.slice(0, 500)
|
||||
state.auditLogWritePromise = state.auditLogWritePromise
|
||||
.then(async () => {
|
||||
await mkdir(DATA_DIR, { recursive: true })
|
||||
await writeFile(AUDIT_LOG_FILE, JSON.stringify({ entries: state.auditLog }, null, 2), 'utf8')
|
||||
})
|
||||
.catch(err => { console.error('[audit-log] write failed:', err) })
|
||||
}
|
||||
|
||||
export function loadAuditLogFromDisk() {
|
||||
return readFile(AUDIT_LOG_FILE, 'utf8')
|
||||
.then(raw => {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (Array.isArray(parsed?.entries)) state.auditLog = parsed.entries.slice(0, 500)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// ── Podcast checklist ──────────────────────────────────────────────────────
|
||||
|
||||
export function queuePodcastChecklistWrite() {
|
||||
@@ -1263,7 +1291,9 @@ export function sanitizePodcastChecklist(value) {
|
||||
const reminderDays = Number.isFinite(Number(item.reminderDays)) && Number(item.reminderDays) >= 0 ? Number(item.reminderDays) : 0
|
||||
const reminderSentAt = typeof item.reminderSentAt === 'string' && item.reminderSentAt ? item.reminderSentAt : undefined
|
||||
const startTime = typeof item.startTime === 'string' && /^\d{2}:\d{2}$/.test(item.startTime) ? item.startTime : undefined
|
||||
episodes.push({ id, series, episodeNumber, title, datePublished, expanded: item.expanded === true, tasks: taskState, reminderDays, reminderSentAt, ...(startTime ? { startTime } : {}) })
|
||||
const PROD_STATUSES = ['idea', 'recorded', 'edited', 'scheduled', 'published']
|
||||
const productionStatus = PROD_STATUSES.includes(item.productionStatus) ? item.productionStatus : undefined
|
||||
episodes.push({ id, series, episodeNumber, title, datePublished, expanded: item.expanded === true, tasks: taskState, reminderDays, reminderSentAt, ...(startTime ? { startTime } : {}), ...(productionStatus ? { productionStatus } : {}) })
|
||||
}
|
||||
|
||||
if (episodes.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user