diff --git a/package-lock.json b/package-lock.json index dc66749..d792a0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "siteforge", - "version": "2.16.0", + "version": "2.16.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "siteforge", - "version": "2.16.0", + "version": "2.16.1", "dependencies": { "@aws-sdk/client-s3": "^3.1135.0", "@dnd-kit/core": "^6.3.1", diff --git a/package.json b/package.json index 8c9e45e..67b0628 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.16.0", + "version": "2.16.1", "type": "module", "scripts": { "dev": "vite", diff --git a/server/routes/podcast-workflow.js b/server/routes/podcast-workflow.js index 68d7ae8..36ed703 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 ?? {}, laneOverrides: data.laneOverrides ?? {} } + return { planned: Array.isArray(data.planned) ? data.planned : [], items: data.items ?? {}, laneOverrides: data.laneOverrides ?? {}, laneSeasons: data.laneSeasons ?? {} } } catch { - return { planned: [], items: {}, laneOverrides: {} } + return { planned: [], items: {}, laneOverrides: {}, laneSeasons: {} } } } @@ -29,11 +29,19 @@ function emptyState() { } } -function laneFor(season) { +// A lane's season comes from the workflow page's own setting, falling back to Current Series. +function laneSeasonOf(lane, wf) { + const saved = wf.laneSeasons?.[lane] + if (/^\d+$/.test(String(saved ?? ''))) return String(saved) const content = state.cachedSiteContent ?? {} + const fallback = lane === 'A' ? content.seriesSeason : content.secondSeries?.season + return fallback ? String(fallback) : '' +} + +function laneFor(season, wf) { const s = String(season ?? '') - if (s && s === String(content.seriesSeason ?? '')) return 'A' - if (s && s === String(content.secondSeries?.season ?? '')) return 'B' + if (s && s === laneSeasonOf('A', wf)) return 'A' + if (s && s === laneSeasonOf('B', wf)) return 'B' return 'other' } @@ -96,13 +104,13 @@ export function register(app) { const items = [...byKey.values()].map(e => ({ ...e, - lane: wf.laneOverrides[e.key] ?? e.plannedLane ?? laneFor(e.season), + lane: wf.laneOverrides[e.key] ?? e.plannedLane ?? laneFor(e.season, wf), laneOverride: wf.laneOverrides[e.key] ?? '', released: Boolean(e.released), state: { ...emptyState(), ...(wf.items[e.key] ?? {}) }, })) items.sort((a, b) => (Number(a.season) || 0) - (Number(b.season) || 0) || (Number(a.episodeNumber) || 0) - (Number(b.episodeNumber) || 0)) - res.json({ items, feedOk }) + res.json({ items, feedOk, laneSeasons: { A: laneSeasonOf('A', wf), B: laneSeasonOf('B', wf) } }) }) app.post('/api/admin/podcast-workflow/planned', requireAdminAuth, async (req, res) => { @@ -133,6 +141,21 @@ export function register(app) { res.json({ ok: true }) }) + // Set the season number each lane looks for ('' goes back to the Current Series value). + app.put('/api/admin/podcast-workflow-lane-seasons', requireAdminAuth, async (req, res) => { + const wf = await load() + for (const lane of ['A', 'B']) { + const v = req.body?.[lane] + if (v === undefined) continue + const str = String(v).trim() + if (str === '') delete wf.laneSeasons[lane] + else if (/^\d+$/.test(str)) wf.laneSeasons[lane] = str + else { res.status(400).json({ message: 'Season must be a whole number.' }); return } + } + 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 diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index 2da2ef6..2e68ace 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -1825,7 +1825,8 @@ interface PodcastWorkflowItem { state: PodcastWorkflowState } -function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string; season?: number }; laneB: { title: string; season?: number } }) { +function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string }; laneB: { title: string } }) { + const [laneSeasons, setLaneSeasons] = useState<{ A: string; B: string }>({ A: '', B: '' }) const [items, setItems] = useState([]) const [feedOk, setFeedOk] = useState(true) const [loading, setLoading] = useState(true) @@ -1839,13 +1840,25 @@ function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string; season const reload = () => fetch('/api/admin/podcast-workflow') .then(r => r.json()) - .then(d => { setItems(d.items ?? []); setFeedOk(d.feedOk !== false) }) + .then(d => { setItems(d.items ?? []); setFeedOk(d.feedOk !== false); setLaneSeasons(d.laneSeasons ?? { A: '', B: '' }) }) .catch(() => setError('Could not load episodes.')) .finally(() => setLoading(false)) useEffect(() => { reload() }, []) - const laneSeason = lane === 'A' ? laneA.season : lane === 'B' ? laneB.season : undefined + const laneSeason = lane === 'A' || lane === 'B' ? laneSeasons[lane] : '' + const saveLaneSeason = async (value: string) => { + if (lane === 'other') return + setError('') + const r = await fetch('/api/admin/podcast-workflow-lane-seasons', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ [lane]: value }), + }) + if (!r.ok) { const d = await r.json(); setError(d.message ?? 'Could not save season.'); return } + setSelectedKey('') + reload() + } const hasOther = items.some(i => i.lane === 'other') const inLane = items.filter(i => i.lane === lane) const upcoming = inLane.filter(i => !i.released) @@ -1875,7 +1888,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, lane }), + body: JSON.stringify({ season: laneSeason, episodeNumber: newNumber, title: newTitle, lane }), }) const d = await r.json() if (!r.ok) { setError(d.message ?? 'Could not add episode.'); return } @@ -1899,7 +1912,7 @@ function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string; season reload() } - const label = (i: PodcastWorkflowItem) => `${i.episodeNumber ? `Ep ${i.episodeNumber}` : 'Ep'}${i.title ? ` — ${i.title}` : ''}` + const label = (i: PodcastWorkflowItem) => `${i.season ? `S${i.season} ` : ''}${i.episodeNumber ? `Ep ${i.episodeNumber}` : 'Ep'}${i.title ? ` — ${i.title}` : ''}` const doneCount = (i: PodcastWorkflowItem) => (i.released ? PODCAST_STEP_DEFS.length : PODCAST_STEP_DEFS.filter(s => i.state.steps[s.key]).length) const row = (i: PodcastWorkflowItem) => ( @@ -1937,12 +1950,28 @@ function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string; season ))} {hasOther && ( )} - {lane !== 'other' && !laneSeason && ( -

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

+ {lane !== 'other' ? ( +
+ + { if (e.target.value !== laneSeason) saveLaneSeason(e.target.value) }} + /> + + {laneSeason ? `Only season ${laneSeason} episodes are shown here.` : 'Set a season number so this lane picks up its episodes, or assign episodes by hand.'} + +
+ ) : ( +

Episodes from other seasons (finished or not assigned to a lane).

)} {loading ?

Loading…

: ( @@ -6230,7 +6259,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date()) )} {/* EPISODE SCRIPTS */} - {adminView === 'podcast:workflow' && } + {adminView === 'podcast:workflow' && } {adminView === 'podcast:scripts' && (