Season Check: series card, study guide, next-season handover and unseasoned-script checks; v2.20.1
Publish Container / build (push) Has been cancelled
Publish Container / build (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
50b0e83c2b
commit
35d3c9ca33
6 files changed
+66
-7
No files matched your search
Generated
+2
-2
@@ -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",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "siteforge",
|
||||
"private": true,
|
||||
"version": "2.20.0",
|
||||
"version": "2.20.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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)
|
||||
|
||||
+43
-2
@@ -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) {
|
||||
|
||||
+15
-1
@@ -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
|
||||
<button type="button" className="btn-admin-ghost" onClick={() => { setLoading(true); fetchReport() }} disabled={loading}>{loading ? 'Checking…' : 'Check again'}</button>
|
||||
{error && <p style={{ color: '#c0392b' }}>{error}</p>}
|
||||
{report && report.lanes.length === 0 && <p className="admin-stats-note">No lanes are set up yet.</p>}
|
||||
{report && report.general.length > 0 && (
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<h3 style={{ marginBottom: 4 }}>All lanes</h3>
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: '8px 0 12px' }}>
|
||||
{report.general.map((p, i) => (
|
||||
<li key={i} style={{ display: 'flex', gap: 10, alignItems: 'flex-start', padding: '5px 0' }}>
|
||||
<span style={{ color: p.severity === 'error' ? '#c0392b' : '#b8860b' }}>{p.severity === 'error' ? '●' : '▲'}</span>
|
||||
<span style={{ flex: 1 }}>{p.message}</span>
|
||||
<button type="button" className="btn-admin-ghost" onClick={() => onNavigate(p.view as AdminView)}>Fix →</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{report?.lanes.map(l => (
|
||||
<div key={l.lane} style={{ marginTop: 20 }}>
|
||||
<h3 style={{ marginBottom: 4 }}>
|
||||
|
||||
@@ -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
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{data.seasonCheck.general > 0 && (
|
||||
<p className="admin-stats-note">Plus {data.seasonCheck.general} thing{data.seasonCheck.general === 1 ? '' : 's'} that apply to every lane (such as scripts with no season number).</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user