diff --git a/package-lock.json b/package-lock.json index 38ab64f..5087a42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "siteforge", - "version": "2.20.1", + "version": "2.21.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "siteforge", - "version": "2.20.1", + "version": "2.21.0", "dependencies": { "@aws-sdk/client-s3": "^3.1135.0", "@dnd-kit/core": "^6.3.1", diff --git a/package.json b/package.json index cbe053e..7ec96af 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.20.1", + "version": "2.21.0", "type": "module", "scripts": { "dev": "vite", diff --git a/server/routes/admin-dashboard.js b/server/routes/admin-dashboard.js index f50bfdf..1261f04 100644 --- a/server/routes/admin-dashboard.js +++ b/server/routes/admin-dashboard.js @@ -2,6 +2,7 @@ import { Resend } from 'resend' import { readdir } from 'node:fs/promises' import { requireAdminAuth } from '../auth.js' import { buildSeasonCheck } from '../season-check.js' +import { countPendingUploads } from './nuggets-workflow.js' import { state } from '../state.js' import { getStorageStatus, loadUserProgress } from '../data.js' import { getStudyCatalog } from '../study-helpers.js' @@ -152,6 +153,7 @@ async function buildNuggets() { const weekAgo = Date.now() - 7 * DAY_MS return { uploadsThisWeek: entries.filter(e => new Date(e.uploadedAt).getTime() >= weekAgo).length, + pending: await countPendingUploads(), latest: entries.slice(0, 3).map(e => ({ hostName: e.hostName, episodeName: e.episodeName, uploadedAt: e.uploadedAt })), entries: entries.slice(0, 10), } @@ -236,7 +238,7 @@ export function register(app) { reengagement: buildReengagement(), studyProgress, latestEpisode, - nuggets: { uploadsThisWeek: nuggets.uploadsThisWeek, latest: nuggets.latest }, + nuggets: { uploadsThisWeek: nuggets.uploadsThisWeek, pending: nuggets.pending, latest: nuggets.latest }, health, lastBroadcast, comments: { diff --git a/server/routes/nuggets-workflow.js b/server/routes/nuggets-workflow.js index 11407b7..5d95a23 100644 --- a/server/routes/nuggets-workflow.js +++ b/server/routes/nuggets-workflow.js @@ -26,7 +26,7 @@ const AUDIO_TYPES = { export const WORKFLOW_STEPS = ['listen', 'coldOpen', 'recordColdOpen', 'edit', 'spotify'] -async function loadWorkflow() { +export async function loadWorkflow() { try { return JSON.parse(await readFile(WORKFLOW_FILE, 'utf8')) } catch { @@ -34,7 +34,7 @@ async function loadWorkflow() { } } -function entryId(entry) { +export function entryId(entry) { return createHash('sha1').update(`${entry.uploadedAt}|${entry.filename}`).digest('hex').slice(0, 16) } @@ -51,6 +51,15 @@ function seafileConfigured() { return Boolean(SEAFILE_URL && SEAFILE_TOKEN && SEAFILE_NUGGETS_REPO_ID) } +// Portal uploads whose workflow still has steps left to do (any age, so an old one keeps showing). +export async function countPendingUploads() { + const [history, wf] = await Promise.all([loadHistory(), loadWorkflow()]) + return history.entries.filter(entry => { + const steps = wf.items[entryId(entry)]?.steps ?? {} + return !WORKFLOW_STEPS.every(step => steps[step]) + }).length +} + export function register(app) { app.get('/api/admin/nuggets-workflow', requireAdminAuth, async (_req, res) => { const [history, wf] = await Promise.all([loadHistory(), loadWorkflow()]) diff --git a/src/components/AdminDashboardExtras.tsx b/src/components/AdminDashboardExtras.tsx index 06a0e31..f1b999d 100644 --- a/src/components/AdminDashboardExtras.tsx +++ b/src/components/AdminDashboardExtras.tsx @@ -14,7 +14,7 @@ export interface DashboardData { 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; latest: { hostName: string; episodeName: string; uploadedAt: string }[] } + 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 } @@ -93,7 +93,7 @@ export function AdminDashboardExtras({ publishedAt, draftUpdatedAt, isDirty, onN 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.uploadsThisWeek > 0) attention.push({ key: 'nuggets', label: `${data.nuggets.uploadsThisWeek} Nuggets upload${data.nuggets.uploadsThisWeek === 1 ? '' : 's'} in the last 7 days`, tone: 'alert', action: () => onNavigate('nuggets:history'), actionLabel: 'View uploads' }) + 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' })