Episode scripts: file by season + episode so numbers can repeat per book; search labels for current series; v2.9.6
Publish Container / build (push) Successful in 16m51s

- Scripts keyed "<season>-<episode>" (legacy ones migrate on first start; unseasoned ones keep their plain key)
- Season backfill handles episode numbers shared across seasons (title match fallback)
- Fix: "Backfill seasons from RSS" was swallowed by the upload route
- Global search labels script results with the title of the series playing now (both lanes), not just finished books

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-08 15:31:11 -04:00
1 parent 4ddedf0f81
commit 752611d121
7 files changed
+114 -41

No files matched your search

+9 -9
View File
@@ -1670,7 +1670,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
const [commentSearch, setCommentSearch] = useState('')
// Episode scripts state
const [scriptsList, setScriptsList] = useState<Array<{ episodeNumber: string; title: string; filename: string; season: string | null; uploadedAt: string | null; wordCount: number }>>([])
const [scriptsList, setScriptsList] = useState<Array<{ key: string; episodeNumber: string; title: string; filename: string; season: string | null; uploadedAt: string | null; wordCount: number }>>([])
const [scriptsLoading, setScriptsLoading] = useState(false)
const [scriptUploadEpisode, setScriptUploadEpisode] = useState('')
const [scriptUploadTitle, setScriptUploadTitle] = useState('')
@@ -3148,7 +3148,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
})
const data = await res.json()
if (!res.ok) { setScriptUploadMsg(data.message ?? 'Upload failed.'); return }
setScriptUploadMsg(`✓ Episode ${scriptUploadEpisode} uploaded — ${data.wordCount?.toLocaleString()} words indexed.`)
setScriptUploadMsg(`✓ Episode ${scriptUploadEpisode}${scriptUploadSeason.trim() ? ` (Season ${scriptUploadSeason.trim()})` : ''} uploaded — ${data.wordCount?.toLocaleString()} words indexed.`)
setScriptUploadEpisode('')
setScriptUploadTitle('')
setScriptUploadSeason('')
@@ -3181,12 +3181,12 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
}
}
async function handleDeleteScript(episodeNumber: string) {
if (!confirm(`Remove script for Episode ${episodeNumber}?`)) return
async function handleDeleteScript(key: string, label: string) {
if (!confirm(`Remove script for ${label}?`)) return
try {
const res = await fetch(`/api/admin-episode-scripts/${encodeURIComponent(episodeNumber)}`, { method: 'DELETE' })
const res = await fetch(`/api/admin-episode-scripts/${encodeURIComponent(key)}`, { method: 'DELETE' })
if (!res.ok) { alert('Failed to delete script.'); return }
setScriptsList(prev => prev.filter(s => s.episodeNumber !== episodeNumber))
setScriptsList(prev => prev.filter(s => s.key !== key))
} catch { alert('Failed to delete script.') }
}
@@ -5684,7 +5684,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
/>
</div>
<div className="admin-field">
<label htmlFor="script-season">Season <span style={{ color: '#5a5440', fontWeight: 400 }}>(optional — matches the podcast season number)</span></label>
<label htmlFor="script-season">Season <span style={{ color: '#5a5440', fontWeight: 400 }}>(recommended — scripts are filed by season + episode, so episode numbers can repeat in each book)</span></label>
<input
id="script-season"
type="number"
@@ -5749,14 +5749,14 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
{scriptsList
.filter(script => scriptSeasonFilter === 'all' || script.season === scriptSeasonFilter)
.map(script => (
<div key={script.episodeNumber} className="admin-script-row">
<div key={script.key} className="admin-script-row">
<div className="admin-script-row-info">
<span className="admin-script-ep">Ep. {script.episodeNumber}</span>
{script.season && <span className="admin-script-ep">Season {script.season}</span>}
<span className="admin-script-title">{script.title}</span>
<span className="admin-script-meta">{script.wordCount.toLocaleString()} words · {script.filename}</span>
</div>
<button type="button" className="btn-admin-remove" onClick={() => handleDeleteScript(script.episodeNumber)}>Remove</button>
<button type="button" className="btn-admin-remove" onClick={() => handleDeleteScript(script.key, `Episode ${script.episodeNumber}${script.season ? ` (Season ${script.season})` : ''}`)}>Remove</button>
</div>
))}
</div>
+6 -2
View File
@@ -25,6 +25,7 @@ interface PublicQuestion {
}
interface ScriptSearchResult {
key?: string
episodeNumber: string
title: string
filename: string
@@ -121,8 +122,11 @@ export function useGlobalSearch(content: SiteContent) {
for (const book of content.finishedBooks ?? []) {
if (book.season != null && book.title) map.set(String(book.season), book.title)
}
// The series playing now (one or two at a time), so their scripts show a title too
if (content.seriesSeason != null && content.seriesTitle) map.set(String(content.seriesSeason), content.seriesTitle)
if (content.secondSeries?.season != null && content.secondSeries.title) map.set(String(content.secondSeries.season), content.secondSeries.title)
return map
}, [content.finishedBooks])
}, [content.finishedBooks, content.seriesSeason, content.seriesTitle, content.secondSeries])
// Debounced server-side script search — runs whenever query changes
useEffect(() => {
@@ -138,7 +142,7 @@ export function useGlobalSearch(content: SiteContent) {
const results = (data.results ?? []).map(r => {
const seasonLabel = r.season ? (seasonTitleMap.get(r.season) ?? `Season ${r.season}`) : null
return {
id: `script-${r.episodeNumber}`,
id: `script-${r.key ?? r.episodeNumber}`,
type: 'episode' as const,
title: r.title,
subtitle: [`Episode ${r.episodeNumber}`, seasonLabel, 'Script'].filter(Boolean).join(' · '),