Add per-episode play tracking with bar chart in admin analytics

- POST /api/analytics/play records a play event (title + date) on first
  play per player mount, skipping admin sessions
- Plays persisted to data/episode-plays.json with total + byDay buckets
- Admin stats response includes episodePlays sorted by total plays
- AnalyticsPanel shows horizontal bar chart + summary cards for all
  episodes dynamically — new episodes appear automatically as played

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-06-17 13:39:35 -04:00
parent 54bc7b28da
commit 8450d81e7f
9 changed files with 129 additions and 2 deletions
+35
View File
@@ -28,6 +28,7 @@ import {
UPLOADS_DIR,
UPLOADS_META_FILE,
DOWNLOAD_COUNTS_FILE,
EPISODE_PLAYS_FILE,
EMPTY_HIT_STATS,
EMPTY_VISITOR_STATS,
DEFAULT_REPLY_TEMPLATES,
@@ -675,6 +676,40 @@ export function incrementDownloadCount(resourceKey) {
queueDownloadCountsWrite()
}
// ── Episode play counts ────────────────────────────────────────────────────
export function queueEpisodePlaysWrite() {
state.episodePlaysWritePromise = state.episodePlaysWritePromise
.then(async () => {
await mkdir(DATA_DIR, { recursive: true })
await writeFile(EPISODE_PLAYS_FILE, JSON.stringify(state.episodePlays, null, 2), 'utf8')
})
.catch(err => {
console.error('[episode-plays] failed to write:', err)
})
}
export function loadEpisodePlaysFromDisk() {
return readFile(EPISODE_PLAYS_FILE, 'utf8')
.then(raw => {
const parsed = JSON.parse(raw)
state.episodePlays = (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {}
})
.catch(() => {
state.episodePlays = {}
})
}
export function recordEpisodePlay(title) {
const today = new Date().toISOString().slice(0, 10)
if (!state.episodePlays[title]) {
state.episodePlays[title] = { total: 0, byDay: {} }
}
state.episodePlays[title].total += 1
state.episodePlays[title].byDay[today] = (state.episodePlays[title].byDay[today] ?? 0) + 1
queueEpisodePlaysWrite()
}
// ── Uploads ────────────────────────────────────────────────────────────────
export async function readUploadsMetadata() {