diff --git a/package-lock.json b/package-lock.json index ff1ec0d..38ab64f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "siteforge", - "version": "2.20.0", + "version": "2.20.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "siteforge", - "version": "2.20.0", + "version": "2.20.1", "dependencies": { "@aws-sdk/client-s3": "^3.1135.0", "@dnd-kit/core": "^6.3.1", diff --git a/package.json b/package.json index 044e4a7..cbe053e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.20.0", + "version": "2.20.1", "type": "module", "scripts": { "dev": "vite", diff --git a/server/routes/admin-dashboard.js b/server/routes/admin-dashboard.js index ae5c47d..f50bfdf 100644 --- a/server/routes/admin-dashboard.js +++ b/server/routes/admin-dashboard.js @@ -225,6 +225,7 @@ export function register(app) { total: c.total, errors: c.errors, warnings: c.warnings, + general: c.general.length, lanes: c.lanes.map(l => ({ lane: l.lane, title: l.title, season: l.season, errors: l.errors, warnings: l.warnings })), })) .catch(() => null) diff --git a/server/season-check.js b/server/season-check.js index 52fd271..b38e6bc 100644 --- a/server/season-check.js +++ b/server/season-check.js @@ -17,6 +17,11 @@ function laneConfig(content, lane) { title: String(content.seriesTitle ?? '').trim(), studySlug: String(content.seriesStudySlug ?? '').trim(), highlightLane: 'primary', + description: String(content.seriesDescription ?? '').trim(), + imageUrl: String(content.seriesImageUrl ?? '').trim(), + listenUrl: String(content.seriesListenUrl ?? '').trim(), + hasGuide: Boolean(String(content.studyGuideTitle ?? '').trim() && (content.studyGuideDownloadUrl || content.studyGuideUrl || content.studyGuideAppleBooksUrl)), + transition: content.scheduledSeriesTransition, } } const second = content.secondSeries ?? {} @@ -24,6 +29,11 @@ function laneConfig(content, lane) { title: String(second.title ?? '').trim(), studySlug: String(second.studySlug ?? '').trim(), highlightLane: 'second', + description: String(second.description ?? '').trim(), + imageUrl: String(second.imageUrl ?? '').trim(), + listenUrl: String(second.listenUrl ?? '').trim(), + hasGuide: Boolean(String(second.guide?.title ?? '').trim() && (second.guide?.downloadUrl || second.guide?.amazonUrl || second.guide?.appleBooksUrl)), + transition: content.scheduledSecondSeriesTransition, } } @@ -68,6 +78,24 @@ export async function buildSeasonCheck() { problems.push(warn(`${noHighlightNumber.length} highlight${noHighlightNumber.length === 1 ? '' : 's'} in Lane ${lane} ${noHighlightNumber.length === 1 ? 'has' : 'have'} no episode number.`, 'podcast:highlights')) } + // Series card + if (!cfg.imageUrl) problems.push(warn(`Lane ${lane} has no cover image.`, 'podcast:current-series')) + if (!cfg.description) problems.push(warn(`Lane ${lane} has no series description.`, 'podcast:current-series')) + if (!cfg.listenUrl) problems.push(warn(`Lane ${lane} has no listen link.`, 'podcast:current-series')) + if (!cfg.hasGuide) problems.push(warn(`Lane ${lane} has no companion study guide (title plus a download or purchase link).`, 'podcast:current-series')) + + // Handover to the next season + const t = cfg.transition + if (t) { + if (!String(t.newTitle ?? '').trim() || !t.newSeason) { + problems.push(warn(`The next-season handover for Lane ${lane} is scheduled but is missing its title or season number.`, 'podcast:current-series')) + } else if (season && String(t.newSeason) === season) { + problems.push(warn(`The next-season handover for Lane ${lane} uses the same season number (${season}) as the current one.`, 'podcast:current-series')) + } + } else if (study && sections.length > 0 && sections.every(x => x.releasedAt && new Date(x.releasedAt) <= new Date())) { + problems.push(warn(`Every lesson in "${study.title}" is released, but no next season is scheduled for Lane ${lane}.`, 'podcast:current-series')) + } + // Episode numbers to show: every number any part of the season mentions, plus 1..lesson count const numbers = new Set() for (let n = 1; n <= sections.length; n++) numbers.add(n) @@ -117,8 +145,21 @@ export async function buildSeasonCheck() { }) } - const total = lanes.reduce((n, l) => n + l.problems.length, 0) - return { lanes, total, errors: lanes.reduce((n, l) => n + l.errors, 0), warnings: lanes.reduce((n, l) => n + l.warnings, 0) } + // Scripts are filed by season + episode, so one with no season can't be matched to an episode + const general = [] + const unseasoned = Object.entries(state.episodeScripts ?? {}).filter(([, entry]) => !String(entry?.season ?? '').trim()) + if (unseasoned.length > 0) { + general.push(warn(`${unseasoned.length} episode script${unseasoned.length === 1 ? ' has' : 's have'} no season number, so ${unseasoned.length === 1 ? 'it' : 'they'} can't be matched to an episode. Use the season backfill in Episode Scripts.`, 'podcast:scripts')) + } + + const all = [...lanes.flatMap(l => l.problems), ...general] + return { + lanes, + general, + total: all.length, + errors: all.filter(p => p.severity === 'error').length, + warnings: all.filter(p => p.severity === 'warn').length, + } } export function register(app) { diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index d122a27..46af67c 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -2148,7 +2148,7 @@ interface SeasonCheckLane { } function SeasonCheckAdmin({ onNavigate }: { onNavigate: (view: AdminView) => void }) { - const [report, setReport] = useState<{ lanes: SeasonCheckLane[]; total: number } | null>(null) + const [report, setReport] = useState<{ lanes: SeasonCheckLane[]; general: SeasonCheckProblem[]; total: number } | null>(null) const [error, setError] = useState('') const [loading, setLoading] = useState(true) @@ -2173,6 +2173,20 @@ function SeasonCheckAdmin({ onNavigate }: { onNavigate: (view: AdminView) => voi {error &&

{error}

} {report && report.lanes.length === 0 &&

No lanes are set up yet.

} + {report && report.general.length > 0 && ( +
+

All lanes

+ +
+ )} {report?.lanes.map(l => (

diff --git a/src/components/AdminDashboardExtras.tsx b/src/components/AdminDashboardExtras.tsx index cc7e4bf..06a0e31 100644 --- a/src/components/AdminDashboardExtras.tsx +++ b/src/components/AdminDashboardExtras.tsx @@ -4,7 +4,7 @@ interface Trend { thisWeek: number; lastWeek: number; series: number[] } interface ResendStatus { available: boolean; reason?: string; broadcast?: { id: string; name: string; subject: string; sentAt: string | null; dashboardUrl: string } | null } export interface DashboardData { - seasonCheck: { total: number; errors: number; warnings: number; lanes: { lane: string; title: string; season: string; errors: number; warnings: number }[] } | null + seasonCheck: { total: number; errors: number; warnings: number; general: number; lanes: { lane: string; title: string; season: string; errors: number; warnings: number }[] } | null trends: { visits: Trend; subscribers: Trend; studySignups: Trend } traffic: { topPages: { path: string; hits: number }[]; topReferrers: { referrer: string; count: number }[] } reengagement: { t1: number; t2: number; t3: number; recentlySent: number; remindersEnabled: number } @@ -157,6 +157,9 @@ export function AdminDashboardExtras({ publishedAt, draftUpdatedAt, isDirty, onN ) })}

+ {data.seasonCheck.general > 0 && ( +

Plus {data.seasonCheck.general} thing{data.seasonCheck.general === 1 ? '' : 's'} that apply to every lane (such as scripts with no season number).

+ )} )}