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.
)}
+
+ Lane
+ setLaneFor(selected.key, e.target.value)}>
+ Automatic (by season number{selected.season ? ` ${selected.season}` : ''})
+ Lane A — {laneA.title || 'Primary series'}
+ Lane B — {laneB.title || 'Second series'}
+ Other
+
+
{PODCAST_STEP_DEFS.map((s, idx) => {
const completedAt = selected.state.steps[s.key]