Podcast workflow: per-lane season number set on the page, only that season shown; v2.16.1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
900a3efee3
commit
3a69e03e60
4 files changed
+71
-19
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"version": "2.16.0",
|
"version": "2.16.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"version": "2.16.0",
|
"version": "2.16.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.1135.0",
|
"@aws-sdk/client-s3": "^3.1135.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.16.0",
|
"version": "2.16.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ const STEPS = ['readScript', 'record', 'edit', 'upload']
|
|||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(await readFile(WORKFLOW_FILE, 'utf8'))
|
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 {
|
} 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 content = state.cachedSiteContent ?? {}
|
||||||
|
const fallback = lane === 'A' ? content.seriesSeason : content.secondSeries?.season
|
||||||
|
return fallback ? String(fallback) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function laneFor(season, wf) {
|
||||||
const s = String(season ?? '')
|
const s = String(season ?? '')
|
||||||
if (s && s === String(content.seriesSeason ?? '')) return 'A'
|
if (s && s === laneSeasonOf('A', wf)) return 'A'
|
||||||
if (s && s === String(content.secondSeries?.season ?? '')) return 'B'
|
if (s && s === laneSeasonOf('B', wf)) return 'B'
|
||||||
return 'other'
|
return 'other'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,13 +104,13 @@ export function register(app) {
|
|||||||
|
|
||||||
const items = [...byKey.values()].map(e => ({
|
const items = [...byKey.values()].map(e => ({
|
||||||
...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] ?? '',
|
laneOverride: wf.laneOverrides[e.key] ?? '',
|
||||||
released: Boolean(e.released),
|
released: Boolean(e.released),
|
||||||
state: { ...emptyState(), ...(wf.items[e.key] ?? {}) },
|
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))
|
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) => {
|
app.post('/api/admin/podcast-workflow/planned', requireAdminAuth, async (req, res) => {
|
||||||
@@ -133,6 +141,21 @@ export function register(app) {
|
|||||||
res.json({ ok: true })
|
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).
|
// 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) => {
|
app.put('/api/admin/podcast-workflow/:key/lane', requireAdminAuth, async (req, res) => {
|
||||||
const lane = req.body?.lane
|
const lane = req.body?.lane
|
||||||
|
|||||||
+38
-9
@@ -1825,7 +1825,8 @@ interface PodcastWorkflowItem {
|
|||||||
state: PodcastWorkflowState
|
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<PodcastWorkflowItem[]>([])
|
const [items, setItems] = useState<PodcastWorkflowItem[]>([])
|
||||||
const [feedOk, setFeedOk] = useState(true)
|
const [feedOk, setFeedOk] = useState(true)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@@ -1839,13 +1840,25 @@ function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string; season
|
|||||||
const reload = () =>
|
const reload = () =>
|
||||||
fetch('/api/admin/podcast-workflow')
|
fetch('/api/admin/podcast-workflow')
|
||||||
.then(r => r.json())
|
.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.'))
|
.catch(() => setError('Could not load episodes.'))
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
|
|
||||||
useEffect(() => { reload() }, [])
|
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 hasOther = items.some(i => i.lane === 'other')
|
||||||
const inLane = items.filter(i => i.lane === lane)
|
const inLane = items.filter(i => i.lane === lane)
|
||||||
const upcoming = inLane.filter(i => !i.released)
|
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', {
|
const r = await fetch('/api/admin/podcast-workflow/planned', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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()
|
const d = await r.json()
|
||||||
if (!r.ok) { setError(d.message ?? 'Could not add episode.'); return }
|
if (!r.ok) { setError(d.message ?? 'Could not add episode.'); return }
|
||||||
@@ -1899,7 +1912,7 @@ function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string; season
|
|||||||
reload()
|
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 doneCount = (i: PodcastWorkflowItem) => (i.released ? PODCAST_STEP_DEFS.length : PODCAST_STEP_DEFS.filter(s => i.state.steps[s.key]).length)
|
||||||
|
|
||||||
const row = (i: PodcastWorkflowItem) => (
|
const row = (i: PodcastWorkflowItem) => (
|
||||||
@@ -1937,12 +1950,28 @@ function PodcastWorkflowAdmin({ laneA, laneB }: { laneA: { title: string; season
|
|||||||
))}
|
))}
|
||||||
{hasOther && (
|
{hasOther && (
|
||||||
<button type="button" className={lane === 'other' ? 'btn-admin-save' : 'btn-admin-ghost'} onClick={() => { setLane('other'); setSelectedKey('') }}>
|
<button type="button" className={lane === 'other' ? 'btn-admin-save' : 'btn-admin-ghost'} onClick={() => { setLane('other'); setSelectedKey('') }}>
|
||||||
Other ({items.filter(i => i.lane === 'other' && !i.released).length} in progress)
|
Other seasons ({items.filter(i => i.lane === 'other' && !i.released).length} in progress)
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{lane !== 'other' && !laneSeason && (
|
{lane !== 'other' ? (
|
||||||
<p className="admin-stats-note">Set this lane’s Podcast Season Number in Current Series to match episodes automatically, or assign episodes to this lane by hand.</p>
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center', margin: '0 0 12px', flexWrap: 'wrap' }}>
|
||||||
|
<label htmlFor="pw-season">Season number for Lane {lane}</label>
|
||||||
|
<input
|
||||||
|
key={`${lane}-${laneSeason}`}
|
||||||
|
id="pw-season"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
defaultValue={laneSeason}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
onBlur={e => { if (e.target.value !== laneSeason) saveLaneSeason(e.target.value) }}
|
||||||
|
/>
|
||||||
|
<span className="admin-stats-note">
|
||||||
|
{laneSeason ? `Only season ${laneSeason} episodes are shown here.` : 'Set a season number so this lane picks up its episodes, or assign episodes by hand.'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="admin-stats-note">Episodes from other seasons (finished or not assigned to a lane).</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading ? <p className="admin-stats-note">Loading…</p> : (
|
{loading ? <p className="admin-stats-note">Loading…</p> : (
|
||||||
@@ -6230,7 +6259,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* EPISODE SCRIPTS */}
|
{/* EPISODE SCRIPTS */}
|
||||||
{adminView === 'podcast:workflow' && <PodcastWorkflowAdmin laneA={{ title: form.seriesTitle, season: form.seriesSeason }} laneB={{ title: form.secondSeries?.title ?? '', season: form.secondSeries?.season }} />}
|
{adminView === 'podcast:workflow' && <PodcastWorkflowAdmin laneA={{ title: form.seriesTitle }} laneB={{ title: form.secondSeries?.title ?? '' }} />}
|
||||||
{adminView === 'podcast:scripts' && (
|
{adminView === 'podcast:scripts' && (
|
||||||
<section className="admin-panel-section">
|
<section className="admin-panel-section">
|
||||||
<div className="admin-panel-head">
|
<div className="admin-panel-head">
|
||||||
|
|||||||
Reference in new issue
Block a user