Add admin podcast checklist UX and persistence updates
This commit is contained in:
@@ -57,6 +57,7 @@ const STUDY_USERS_FILE = path.join(DATA_DIR, 'study-users.json')
|
||||
const STUDY_NOTES_FILE = path.join(DATA_DIR, 'study-notes.json')
|
||||
const REPLY_TEMPLATES_FILE = path.join(DATA_DIR, 'admin-reply-templates.json')
|
||||
const REPLY_HISTORY_FILE = path.join(DATA_DIR, 'admin-reply-history.json')
|
||||
const PODCAST_CHECKLIST_FILE = path.join(DATA_DIR, 'podcast-checklist.json')
|
||||
const BACKUP_DIR = path.join(DATA_DIR, 'backups')
|
||||
const UPLOADS_DIR = path.join(DATA_DIR, 'uploads')
|
||||
const UPLOADS_META_FILE = path.join(DATA_DIR, 'uploads-meta.json')
|
||||
@@ -208,6 +209,122 @@ const DEFAULT_PUBLISH_STATE = {
|
||||
publishedAt: null,
|
||||
}
|
||||
|
||||
const DEFAULT_PODCAST_CHECKLIST_TASKS = [
|
||||
{ id: 'verify_script', label: 'Verify Script', phase: 'pre' },
|
||||
{ id: 'read_script', label: 'Read Script', phase: 'pre' },
|
||||
{ id: 'record', label: 'Record', phase: 'pre' },
|
||||
{ id: 'edit', label: 'Edit', phase: 'pre' },
|
||||
{ id: 'mix', label: 'Mix', phase: 'pre' },
|
||||
{ id: 'video_script', label: 'Run Video Conversion Script', phase: 'pre' },
|
||||
{ id: 'post_spotify', label: 'Post on Spotify', phase: 'pre' },
|
||||
{ id: 'update_website', label: 'Update Website', phase: 'post' },
|
||||
{ id: 'send_email', label: 'Send Email', phase: 'post' },
|
||||
]
|
||||
|
||||
function buildChecklistEpisode(series, number) {
|
||||
const tasks = {}
|
||||
for (const task of DEFAULT_PODCAST_CHECKLIST_TASKS) {
|
||||
tasks[task.id] = false
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${series.toLowerCase()}-${number}`,
|
||||
series,
|
||||
episodeNumber: number,
|
||||
title: '',
|
||||
datePublished: '',
|
||||
expanded: false,
|
||||
tasks,
|
||||
}
|
||||
}
|
||||
|
||||
function buildDefaultPodcastChecklist() {
|
||||
const titusEpisodes = [11, 12, 13, 14, 15].map(number => buildChecklistEpisode('Titus', number))
|
||||
const colossiansEpisodes = Array.from({ length: 27 }, (_, index) => buildChecklistEpisode('Colossians', index + 1))
|
||||
|
||||
return {
|
||||
tasks: DEFAULT_PODCAST_CHECKLIST_TASKS,
|
||||
episodes: [...titusEpisodes, ...colossiansEpisodes],
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeChecklistTask(task) {
|
||||
const label = typeof task?.label === 'string' ? task.label.trim().slice(0, 120) : ''
|
||||
if (!label) return null
|
||||
|
||||
const phase = task?.phase === 'post' ? 'post' : 'pre'
|
||||
const id = typeof task?.id === 'string' && task.id.trim() ? task.id.trim() : randomUUID()
|
||||
return { id, label, phase }
|
||||
}
|
||||
|
||||
function sanitizePodcastChecklist(value) {
|
||||
const fallback = buildDefaultPodcastChecklist()
|
||||
const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
||||
|
||||
const taskInput = Array.isArray(source.tasks) ? source.tasks : fallback.tasks
|
||||
const seenTaskIds = new Set()
|
||||
const tasks = []
|
||||
|
||||
for (const item of taskInput) {
|
||||
const safeTask = sanitizeChecklistTask(item)
|
||||
if (!safeTask) continue
|
||||
if (seenTaskIds.has(safeTask.id)) continue
|
||||
seenTaskIds.add(safeTask.id)
|
||||
tasks.push(safeTask)
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
for (const task of fallback.tasks) {
|
||||
tasks.push({ ...task })
|
||||
seenTaskIds.add(task.id)
|
||||
}
|
||||
}
|
||||
|
||||
const taskIds = tasks.map(task => task.id)
|
||||
const episodesInput = Array.isArray(source.episodes) ? source.episodes : fallback.episodes
|
||||
const episodes = []
|
||||
|
||||
for (const item of episodesInput) {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) continue
|
||||
|
||||
const id = typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID()
|
||||
const series = typeof item.series === 'string' ? item.series.trim().slice(0, 80) : ''
|
||||
const title = typeof item.title === 'string' ? item.title.trim().slice(0, 180) : ''
|
||||
const rawEpisodeNumber = Number(item.episodeNumber)
|
||||
const episodeNumber = Number.isFinite(rawEpisodeNumber) && rawEpisodeNumber >= 0
|
||||
? Math.round(rawEpisodeNumber)
|
||||
: null
|
||||
|
||||
const datePublished = typeof item.datePublished === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(item.datePublished.trim())
|
||||
? item.datePublished.trim()
|
||||
: ''
|
||||
|
||||
const sourceTasks = item.tasks && typeof item.tasks === 'object' && !Array.isArray(item.tasks)
|
||||
? item.tasks
|
||||
: {}
|
||||
const taskState = {}
|
||||
for (const taskId of taskIds) {
|
||||
taskState[taskId] = sourceTasks[taskId] === true
|
||||
}
|
||||
|
||||
episodes.push({
|
||||
id,
|
||||
series,
|
||||
episodeNumber,
|
||||
title,
|
||||
datePublished,
|
||||
expanded: item.expanded === true,
|
||||
tasks: taskState,
|
||||
})
|
||||
}
|
||||
|
||||
if (episodes.length === 0) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return { tasks, episodes }
|
||||
}
|
||||
|
||||
const DEFAULT_REPLY_TEMPLATES = [
|
||||
{
|
||||
id: 'thanks-for-reaching-out',
|
||||
@@ -238,6 +355,8 @@ let replyTemplates = [...DEFAULT_REPLY_TEMPLATES]
|
||||
let replyTemplatesWritePromise = Promise.resolve()
|
||||
let replyHistory = []
|
||||
let replyHistoryWritePromise = Promise.resolve()
|
||||
let podcastChecklist = buildDefaultPodcastChecklist()
|
||||
let podcastChecklistWritePromise = Promise.resolve()
|
||||
|
||||
async function loadSiteContentFile(filePath) {
|
||||
const raw = await readFile(filePath, 'utf8')
|
||||
@@ -620,6 +739,31 @@ function loadReplyHistoryFromDisk() {
|
||||
})
|
||||
}
|
||||
|
||||
function loadPodcastChecklistFromDisk() {
|
||||
return readFile(PODCAST_CHECKLIST_FILE, 'utf8')
|
||||
.then(raw => {
|
||||
const parsed = JSON.parse(raw)
|
||||
podcastChecklist = sanitizePodcastChecklist(parsed?.checklist)
|
||||
})
|
||||
.catch(() => {
|
||||
podcastChecklist = buildDefaultPodcastChecklist()
|
||||
})
|
||||
}
|
||||
|
||||
function queuePodcastChecklistWrite() {
|
||||
const updatedAt = new Date().toISOString()
|
||||
podcastChecklistWritePromise = podcastChecklistWritePromise.then(async () => {
|
||||
await mkdir(DATA_DIR, { recursive: true })
|
||||
await writeFile(
|
||||
PODCAST_CHECKLIST_FILE,
|
||||
JSON.stringify({ checklist: podcastChecklist, updatedAt }, null, 2),
|
||||
'utf8',
|
||||
)
|
||||
})
|
||||
|
||||
return podcastChecklistWritePromise
|
||||
}
|
||||
|
||||
function normalizeMessageType(value) {
|
||||
if (value === 'question' || value === 'testimony' || value === 'topic') return value
|
||||
return 'general'
|
||||
@@ -1027,6 +1171,7 @@ async function createBackupSnapshot(reason = 'scheduled') {
|
||||
reason,
|
||||
adminContent: null,
|
||||
draftContent: null,
|
||||
podcastChecklist,
|
||||
publishState,
|
||||
hitStats,
|
||||
visitorStats,
|
||||
@@ -1187,14 +1332,23 @@ async function restoreFromBackup(filename) {
|
||||
contactSubmissions = sanitizeLoadedContactSubmissions(parsed?.contactSubmissions)
|
||||
replyTemplates = sanitizeReplyTemplates(parsed?.replyTemplates)
|
||||
replyHistory = sanitizeReplyHistory(parsed?.replyHistory)
|
||||
podcastChecklist = sanitizePodcastChecklist(parsed?.podcastChecklist)
|
||||
|
||||
queueHitStatsWrite()
|
||||
queueVisitorStatsWrite()
|
||||
queueContactSubmissionsWrite()
|
||||
queueReplyTemplatesWrite()
|
||||
queueReplyHistoryWrite()
|
||||
queuePodcastChecklistWrite()
|
||||
|
||||
await Promise.all([hitStatsWritePromise, visitorStatsWritePromise, contactSubmissionsWritePromise, replyTemplatesWritePromise, replyHistoryWritePromise])
|
||||
await Promise.all([
|
||||
hitStatsWritePromise,
|
||||
visitorStatsWritePromise,
|
||||
contactSubmissionsWritePromise,
|
||||
replyTemplatesWritePromise,
|
||||
replyHistoryWritePromise,
|
||||
podcastChecklistWritePromise,
|
||||
])
|
||||
await refreshContentCaches()
|
||||
await createBackupSnapshot('post-restore')
|
||||
}
|
||||
@@ -1346,6 +1500,21 @@ app.get('/api/admin-content-state', requireAdminAuth, (_req, res) => {
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/api/admin-podcast-checklist', requireAdminAuth, (_req, res) => {
|
||||
res.json({ checklist: podcastChecklist })
|
||||
})
|
||||
|
||||
app.put('/api/admin-podcast-checklist', requireAdminAuth, async (req, res) => {
|
||||
try {
|
||||
const safeChecklist = sanitizePodcastChecklist(req.body?.checklist)
|
||||
podcastChecklist = safeChecklist
|
||||
await queuePodcastChecklistWrite()
|
||||
res.json({ ok: true, checklist: safeChecklist })
|
||||
} catch {
|
||||
res.status(500).json({ message: 'Failed to save podcast checklist.' })
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/site-config', async (_req, res) => {
|
||||
try {
|
||||
const parsed = await loadSiteContentFile(DATA_FILE)
|
||||
@@ -3447,6 +3616,7 @@ Promise.all([
|
||||
loadStudyUsersFromDisk(),
|
||||
loadStudyNotesFromDisk(),
|
||||
loadDownloadCountsFromDisk(),
|
||||
loadPodcastChecklistFromDisk(),
|
||||
refreshContentCaches(),
|
||||
])
|
||||
.catch(err => {
|
||||
|
||||
Reference in New Issue
Block a user