import { useEffect, useState } from 'react' 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; 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 } studyProgress: { studies: { slug: string; title: string; enrolled: number; lessonsCompleted: number; certificates: number }[] certificatesTotal: number lastActivityAt: string | null } latestEpisode: { available: boolean; title?: string; episode?: string; season?: string; publishedAt?: string | null; daysAgo?: number | null; stale?: boolean; staleAfterDays?: number } nuggets: { uploadsThisWeek: number; pending: number; latest: { hostName: string; episodeName: string; uploadedAt: string }[] } health: { resendConfigured: boolean lastBackup: { ok: boolean; at: string | null; error: string | null; file: string | null } backupCount: number | null dataDirWritable: boolean | null writes: { hitStats: { ok: boolean; at: string | null }; visitorStats: { ok: boolean; at: string | null } } serverStartedAt: string } lastBroadcast: ResendStatus comments: { pending: number; thisWeek: number } activity: { at: string; kind: string; text: string }[] } interface Props { publishedAt: string | null draftUpdatedAt: string | null isDirty: boolean onNavigate: (view: string) => void onRequestPublish: () => void } function Sparkline({ values }: { values: number[] }) { const max = Math.max(1, ...values) const step = 100 / Math.max(1, values.length - 1) const points = values.map((v, i) => `${(i * step).toFixed(1)},${(22 - (v / max) * 20).toFixed(1)}`).join(' ') return ( ) } function TrendCard({ label, trend, onClick, actionLabel }: { label: string; trend: Trend; onClick?: () => void; actionLabel?: string }) { const { thisWeek, lastWeek } = trend const diff = thisWeek - lastWeek const pct = lastWeek > 0 ? Math.round((diff / lastWeek) * 100) : null const tone = diff > 0 ? 'up' : diff < 0 ? 'down' : 'flat' const arrow = diff > 0 ? '▲' : diff < 0 ? '▼' : '–' const delta = pct !== null ? `${arrow} ${Math.abs(pct)}%` : diff > 0 ? `${arrow} new` : '–' return (
{thisWeek.toLocaleString()}
{label}
{delta} vs {lastWeek.toLocaleString()} previous 7 days
{onClick && }
) } function formatWhen(value: string | null | undefined) { if (!value) return '—' const d = new Date(value) return Number.isNaN(d.getTime()) ? '—' : d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) } export function AdminDashboardExtras({ publishedAt, draftUpdatedAt, isDirty, onNavigate, onRequestPublish }: Props) { const [data, setData] = useState(null) const [failed, setFailed] = useState(false) useEffect(() => { let cancelled = false fetch('/api/admin-dashboard') .then(r => (r.ok ? r.json() : Promise.reject(new Error('Failed to load dashboard data')))) .then(json => { if (!cancelled) setData(json as DashboardData) }) .catch(() => { if (!cancelled) setFailed(true) }) return () => { cancelled = true } }, []) const draftNewer = !!draftUpdatedAt && (!publishedAt || new Date(draftUpdatedAt).getTime() > new Date(publishedAt).getTime()) const unpublished = isDirty || draftNewer const attention: { key: string; label: string; tone: 'alert' | 'ok'; action?: () => void; actionLabel?: string }[] = [] if (data) { if (data.seasonCheck && data.seasonCheck.total > 0) attention.push({ key: 'season-check', label: `Season check: ${data.seasonCheck.total} thing${data.seasonCheck.total === 1 ? '' : 's'} to fix`, tone: 'alert', action: () => onNavigate('podcast:season-check'), actionLabel: 'Open Season Check' }) if (data.comments.pending > 0) attention.push({ key: 'comments', label: `${data.comments.pending} study comment${data.comments.pending === 1 ? '' : 's'} awaiting approval`, tone: 'alert', action: () => onNavigate('study-comments'), actionLabel: 'Review' }) if (data.nuggets.pending > 0) attention.push({ key: 'nuggets', label: `${data.nuggets.pending} Nuggets upload${data.nuggets.pending === 1 ? '' : 's'} waiting in the workflow`, tone: 'alert', action: () => onNavigate('nuggets:workflow'), actionLabel: 'Open workflow' }) if (data.latestEpisode.available && data.latestEpisode.stale) attention.push({ key: 'feed', label: `Podcast feed has no new episode in ${data.latestEpisode.daysAgo} days`, tone: 'alert', action: () => onNavigate('podcast:rss'), actionLabel: 'Check feed' }) if (!data.health.lastBackup.ok) attention.push({ key: 'backup', label: 'Last data backup failed', tone: 'alert' }) if (data.health.dataDirWritable === false) attention.push({ key: 'disk', label: 'Data directory is not writable', tone: 'alert' }) } return ( <> {unpublished && (
{isDirty ? 'You have unsaved edits.' : 'Draft changes are not live yet.'} Draft updated {formatWhen(draftUpdatedAt)} · Live version published {formatWhen(publishedAt)}.
)}
Quick actions
{failed &&

Couldn't load the extra dashboard insights.

} {!data && !failed &&

Loading insights…

} {data && ( <> {attention.length > 0 && (

Needs attention

)} {data.seasonCheck && data.seasonCheck.lanes.length > 0 && ( <>

Season check

{data.seasonCheck.lanes.map(l => { const issues = l.errors + l.warnings return (
{issues === 0 ? '✓' : issues}
Lane {l.lane}{l.title ? ` — ${l.title}` : ''}
{l.season ? `Season ${l.season}` : 'No season number set'} · {issues === 0 ? 'all set' : `${issues} to fix`}
) })}
{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).

)} )}

Trends

onNavigate('analytics')} actionLabel="Analytics" /> onNavigate('subscribers')} actionLabel="Subscribers" /> onNavigate('study-users')} actionLabel="Study users" />
{data.comments.thisWeek}
Study comments (7 days)
{data.comments.pending} awaiting approval

Top pages (7 days)

{data.traffic.topPages.length === 0 ?

No visits recorded yet.

: (
{data.traffic.topPages.map(p => (
{p.path}{p.hits}
))}
)}

Top referrers (7 days)

{data.traffic.topReferrers.length === 0 ?

No referrers recorded yet.

: (
{data.traffic.topReferrers.map(r => (
{r.referrer}{r.count}
))}
)}

Last broadcast

{!data.lastBroadcast.available ?

{data.lastBroadcast.reason ?? 'Resend is unavailable.'}

: !data.lastBroadcast.broadcast ?

No broadcasts sent yet.

: (
{data.lastBroadcast.broadcast.subject || data.lastBroadcast.broadcast.name}
Sent {formatWhen(data.lastBroadcast.broadcast.sentAt)}
Opens, clicks & unsubscribes in Resend →
)}

Study re-engagement

Inactive 7–13 days{data.reengagement.t1}
Inactive 14–29 days{data.reengagement.t2}
Inactive 30+ days{data.reengagement.t3}
Reminders sent (30 days){data.reengagement.recentlySent}
{data.reengagement.remindersEnabled} students have reminders on

Study progress

{data.studyProgress.studies.length === 0 ?

No study activity yet.

: (
{data.studyProgress.studies.map(s => (
{s.title}
{s.enrolled} enrolled · {s.lessonsCompleted} lessons completed · {s.certificates} certificate{s.certificates === 1 ? '' : 's'}
))}
Last study activity: {formatWhen(data.studyProgress.lastActivityAt)}
)}

Latest podcast episode

{!data.latestEpisode.available ?

Couldn't read the podcast feed.

: (
{data.latestEpisode.title}
{data.latestEpisode.season ? `S${data.latestEpisode.season} ` : ''}{data.latestEpisode.episode ? `E${data.latestEpisode.episode} · ` : ''} {formatWhen(data.latestEpisode.publishedAt)}{data.latestEpisode.daysAgo != null ? ` (${data.latestEpisode.daysAgo} days ago)` : ''}
{data.latestEpisode.stale &&
No new episode in over {data.latestEpisode.staleAfterDays} days.
}
)}

System health

Resend API key{data.health.resendConfigured ? 'Configured' : 'Missing'}
Last backup{data.health.lastBackup.at ? `${data.health.lastBackup.ok ? '✓' : '✗'} ${formatWhen(data.health.lastBackup.at)}` : 'None this run'}
Backups on disk{data.health.backupCount ?? '—'}
Data directory{data.health.dataDirWritable === null ? '—' : data.health.dataDirWritable ? 'Writable' : 'Not writable'}
Server started{formatWhen(data.health.serverStartedAt)}

Recent activity

{data.activity.length === 0 ?

No activity yet.

: (
{data.activity.map((item, i) => (
{item.text}{formatWhen(item.at)}
))}
)}
)} ) }