From 900a3efee3ba2217cfd80cb88ce294508fbdc3c6 Mon Sep 17 00:00:00 2001 From: nmemmert Date: Mon, 12 Oct 2026 08:25:00 -0400 Subject: [PATCH] Podcast workflow: assign lanes by hand, match by season, unique keys for unseasoned episodes; drop old checklist data file; v2.16.0 Co-Authored-By: Claude Sonnet 5.5 --- .gitignore | 4 + data/podcast-checklist.json | 681 ------------------------------ package-lock.json | 4 +- package.json | 2 +- server/routes/podcast-workflow.js | 53 ++- src/AdminPage.tsx | 23 +- 6 files changed, 69 insertions(+), 698 deletions(-) delete mode 100644 data/podcast-checklist.json diff --git a/.gitignore b/.gitignore index 70bcd9c..d52a517 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,7 @@ data/study-community.json data/analytics-events.json data/admin-reply-history.json data/episode-plays.json + +# Episode workflow progress — lives only in the server data volume +data/podcast-workflow.json +data/nuggets-workflow.json diff --git a/data/podcast-checklist.json b/data/podcast-checklist.json deleted file mode 100644 index 8202a54..0000000 --- a/data/podcast-checklist.json +++ /dev/null @@ -1,681 +0,0 @@ -{ - "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" - } - ], - "episodes": [ - { - "id": "titus-11", - "series": "Titus", - "episodeNumber": 11, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "titus-12", - "series": "Titus", - "episodeNumber": 12, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "titus-13", - "series": "Titus", - "episodeNumber": 13, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "titus-14", - "series": "Titus", - "episodeNumber": 14, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "titus-15", - "series": "Titus", - "episodeNumber": 15, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-1", - "series": "Colossians", - "episodeNumber": 1, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-2", - "series": "Colossians", - "episodeNumber": 2, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-3", - "series": "Colossians", - "episodeNumber": 3, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-4", - "series": "Colossians", - "episodeNumber": 4, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-5", - "series": "Colossians", - "episodeNumber": 5, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-6", - "series": "Colossians", - "episodeNumber": 6, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-7", - "series": "Colossians", - "episodeNumber": 7, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-8", - "series": "Colossians", - "episodeNumber": 8, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-9", - "series": "Colossians", - "episodeNumber": 9, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-10", - "series": "Colossians", - "episodeNumber": 10, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-11", - "series": "Colossians", - "episodeNumber": 11, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-12", - "series": "Colossians", - "episodeNumber": 12, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-13", - "series": "Colossians", - "episodeNumber": 13, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-14", - "series": "Colossians", - "episodeNumber": 14, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-15", - "series": "Colossians", - "episodeNumber": 15, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-16", - "series": "Colossians", - "episodeNumber": 16, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-17", - "series": "Colossians", - "episodeNumber": 17, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-18", - "series": "Colossians", - "episodeNumber": 18, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-19", - "series": "Colossians", - "episodeNumber": 19, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-20", - "series": "Colossians", - "episodeNumber": 20, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-21", - "series": "Colossians", - "episodeNumber": 21, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-22", - "series": "Colossians", - "episodeNumber": 22, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-23", - "series": "Colossians", - "episodeNumber": 23, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-24", - "series": "Colossians", - "episodeNumber": 24, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-25", - "series": "Colossians", - "episodeNumber": 25, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-26", - "series": "Colossians", - "episodeNumber": 26, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "colossians-27", - "series": "Colossians", - "episodeNumber": 27, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - }, - { - "id": "episode-mpb66z1c", - "series": "Titus -10", - "episodeNumber": 0, - "title": "", - "datePublished": "", - "expanded": false, - "tasks": { - "verify_script": false, - "read_script": false, - "record": false, - "edit": false, - "mix": false, - "video_script": false, - "post_spotify": false, - "update_website": false, - "send_email": false - } - } - ] - }, - "updatedAt": "2026-05-18T12:18:35.130Z" -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e55d055..dc66749 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "siteforge", - "version": "2.15.2", + "version": "2.16.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "siteforge", - "version": "2.15.2", + "version": "2.16.0", "dependencies": { "@aws-sdk/client-s3": "^3.1135.0", "@dnd-kit/core": "^6.3.1", diff --git a/package.json b/package.json index 6300a5d..8c9e45e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.15.2", + "version": "2.16.0", "type": "module", "scripts": { "dev": "vite", diff --git a/server/routes/podcast-workflow.js b/server/routes/podcast-workflow.js index 2c7c333..68d7ae8 100644 --- a/server/routes/podcast-workflow.js +++ b/server/routes/podcast-workflow.js @@ -14,9 +14,9 @@ const STEPS = ['readScript', 'record', 'edit', 'upload'] async function load() { try { const data = JSON.parse(await readFile(WORKFLOW_FILE, 'utf8')) - return { planned: Array.isArray(data.planned) ? data.planned : [], items: data.items ?? {} } + return { planned: Array.isArray(data.planned) ? data.planned : [], items: data.items ?? {}, laneOverrides: data.laneOverrides ?? {} } } catch { - return { planned: [], items: {} } + return { planned: [], items: {}, laneOverrides: {} } } } @@ -37,6 +37,9 @@ function laneFor(season) { return 'other' } +const normTitle = t => String(t ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim() +const plannedKey = p => p.key ?? scriptKey(p.season, p.episodeNumber) + function toIsoDate(pubDate) { const d = new Date(pubDate) return Number.isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10) @@ -65,17 +68,25 @@ export function register(app) { }) } for (const p of wf.planned) { - const key = scriptKey(p.season, p.episodeNumber) + const key = plannedKey(p) const prev = byKey.get(key) - upsert(key, { season: p.season, episodeNumber: p.episodeNumber, title: prev?.title || p.title || '', planned: true }) + upsert(key, { season: p.season, episodeNumber: p.episodeNumber, title: prev?.title || p.title || '', planned: true, plannedLane: p.lane }) } for (const item of feed) { const hasNum = /^\d+$/.test(item.episode) - const key = hasNum ? scriptKey(item.season, item.episode) : `t:${item.title}` + let key + if (item.season && hasNum) { + key = scriptKey(item.season, item.episode) + } else { + // No season in the feed: pair with a script/planned episode of the same title, else keep it on its own. + const t = normTitle(item.title) + const match = [...byKey.values()].find(e => !e.released && normTitle(e.title) === t) + key = match ? match.key : `t:${t}` + } const prev = byKey.get(key) upsert(key, { - season: item.season, - episodeNumber: hasNum ? item.episode : '', + season: item.season || prev?.season || '', + episodeNumber: hasNum ? item.episode : (prev?.episodeNumber ?? ''), title: item.title || prev?.title || '', released: true, releaseDate: toIsoDate(item.pubDate), @@ -85,7 +96,8 @@ export function register(app) { const items = [...byKey.values()].map(e => ({ ...e, - lane: laneFor(e.season), + lane: wf.laneOverrides[e.key] ?? e.plannedLane ?? laneFor(e.season), + laneOverride: wf.laneOverrides[e.key] ?? '', released: Boolean(e.released), state: { ...emptyState(), ...(wf.items[e.key] ?? {}) }, })) @@ -101,10 +113,12 @@ export function register(app) { res.status(400).json({ message: 'Episode number must be a whole number.' }) return } + const lane = ['A', 'B'].includes(req.body?.lane) ? req.body.lane : 'other' const wf = await load() - const key = scriptKey(season, episodeNumber) - if (!wf.planned.some(p => scriptKey(p.season, p.episodeNumber) === key)) { - wf.planned.push({ season, episodeNumber, title }) + // Without a season number the key must still be unique per lane. + const key = season ? scriptKey(season, episodeNumber) : `${lane}-p${episodeNumber}` + if (!wf.planned.some(p => plannedKey(p) === key)) { + wf.planned.push({ key, season, episodeNumber, title, lane }) await save(wf) } res.json({ ok: true, key }) @@ -112,8 +126,23 @@ export function register(app) { app.delete('/api/admin/podcast-workflow/planned/:key', requireAdminAuth, async (req, res) => { const wf = await load() - wf.planned = wf.planned.filter(p => scriptKey(p.season, p.episodeNumber) !== req.params.key) + wf.planned = wf.planned.filter(p => plannedKey(p) !== req.params.key) delete wf.items[req.params.key] + delete wf.laneOverrides[req.params.key] + await save(wf) + res.json({ ok: true }) + }) + + // Assign an episode to a lane by hand ('auto' clears the override and goes back to season matching). + app.put('/api/admin/podcast-workflow/:key/lane', requireAdminAuth, async (req, res) => { + const lane = req.body?.lane + if (!['A', 'B', 'other', 'auto'].includes(lane)) { + res.status(400).json({ message: 'Lane must be A, B, other or auto.' }) + return + } + const wf = await load() + if (lane === 'auto') delete wf.laneOverrides[req.params.key] + else wf.laneOverrides[req.params.key] = lane await save(wf) res.json({ ok: true }) }) diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index 0dd506f..2da2ef6 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -1816,6 +1816,7 @@ interface PodcastWorkflowItem { episodeNumber: string title: string lane: 'A' | 'B' | 'other' + laneOverride?: '' | 'A' | 'B' | 'other' released: boolean releaseDate?: string spotifyUrl?: string @@ -1874,7 +1875,7 @@ function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string; season const r = await fetch('/api/admin/podcast-workflow/planned', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ season: laneSeason ? String(laneSeason) : '', episodeNumber: newNumber, title: newTitle }), + body: JSON.stringify({ season: laneSeason ? String(laneSeason) : '', episodeNumber: newNumber, title: newTitle, lane }), }) const d = await r.json() if (!r.ok) { setError(d.message ?? 'Could not add episode.'); return } @@ -1882,6 +1883,15 @@ function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string; season await reload() setSelectedKey(d.key) } + const setLaneFor = async (key: string, value: string) => { + await fetch(`/api/admin/podcast-workflow/${encodeURIComponent(key)}/lane`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ lane: value }), + }) + setSelectedKey('') + reload() + } const removePlanned = async (key: string) => { if (!confirm('Remove this planned episode and its progress?')) return await fetch(`/api/admin/podcast-workflow/planned/${encodeURIComponent(key)}`, { method: 'DELETE' }) @@ -1932,7 +1942,7 @@ function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string; season )} {lane !== 'other' && !laneSeason && ( -

Set this lane’s Podcast Season Number in Current Series so its episodes can be matched.

+

Set this lane’s Podcast Season Number in Current Series to match episodes automatically, or assign episodes to this lane by hand.

)} {loading ?

Loading…

: ( @@ -1970,6 +1980,15 @@ function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string; season {selected.hasScript ? 'Script uploaded.' : 'No script yet.'} This moves to Released on its own once it shows up in the feed.

)} +
+ + +
    {PODCAST_STEP_DEFS.map((s, idx) => { const completedAt = selected.state.steps[s.key]