Dashboard: Nuggets attention line counts uploads still open in the workflow and clears when finished; v2.21.0
Publish Container / build (push) Successful in 16m6s
Publish Container / build (push) Successful in 16m6s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
35d3c9ca33
commit
e6bef529e6
5 files changed
+19
-8
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"version": "2.20.1",
|
"version": "2.21.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"version": "2.20.1",
|
"version": "2.21.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.1135.0",
|
"@aws-sdk/client-s3": "^3.1135.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.20.1",
|
"version": "2.21.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Resend } from 'resend'
|
|||||||
import { readdir } from 'node:fs/promises'
|
import { readdir } from 'node:fs/promises'
|
||||||
import { requireAdminAuth } from '../auth.js'
|
import { requireAdminAuth } from '../auth.js'
|
||||||
import { buildSeasonCheck } from '../season-check.js'
|
import { buildSeasonCheck } from '../season-check.js'
|
||||||
|
import { countPendingUploads } from './nuggets-workflow.js'
|
||||||
import { state } from '../state.js'
|
import { state } from '../state.js'
|
||||||
import { getStorageStatus, loadUserProgress } from '../data.js'
|
import { getStorageStatus, loadUserProgress } from '../data.js'
|
||||||
import { getStudyCatalog } from '../study-helpers.js'
|
import { getStudyCatalog } from '../study-helpers.js'
|
||||||
@@ -152,6 +153,7 @@ async function buildNuggets() {
|
|||||||
const weekAgo = Date.now() - 7 * DAY_MS
|
const weekAgo = Date.now() - 7 * DAY_MS
|
||||||
return {
|
return {
|
||||||
uploadsThisWeek: entries.filter(e => new Date(e.uploadedAt).getTime() >= weekAgo).length,
|
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 })),
|
latest: entries.slice(0, 3).map(e => ({ hostName: e.hostName, episodeName: e.episodeName, uploadedAt: e.uploadedAt })),
|
||||||
entries: entries.slice(0, 10),
|
entries: entries.slice(0, 10),
|
||||||
}
|
}
|
||||||
@@ -236,7 +238,7 @@ export function register(app) {
|
|||||||
reengagement: buildReengagement(),
|
reengagement: buildReengagement(),
|
||||||
studyProgress,
|
studyProgress,
|
||||||
latestEpisode,
|
latestEpisode,
|
||||||
nuggets: { uploadsThisWeek: nuggets.uploadsThisWeek, latest: nuggets.latest },
|
nuggets: { uploadsThisWeek: nuggets.uploadsThisWeek, pending: nuggets.pending, latest: nuggets.latest },
|
||||||
health,
|
health,
|
||||||
lastBroadcast,
|
lastBroadcast,
|
||||||
comments: {
|
comments: {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const AUDIO_TYPES = {
|
|||||||
|
|
||||||
export const WORKFLOW_STEPS = ['listen', 'coldOpen', 'recordColdOpen', 'edit', 'spotify']
|
export const WORKFLOW_STEPS = ['listen', 'coldOpen', 'recordColdOpen', 'edit', 'spotify']
|
||||||
|
|
||||||
async function loadWorkflow() {
|
export async function loadWorkflow() {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(await readFile(WORKFLOW_FILE, 'utf8'))
|
return JSON.parse(await readFile(WORKFLOW_FILE, 'utf8'))
|
||||||
} catch {
|
} 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)
|
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)
|
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) {
|
export function register(app) {
|
||||||
app.get('/api/admin/nuggets-workflow', requireAdminAuth, async (_req, res) => {
|
app.get('/api/admin/nuggets-workflow', requireAdminAuth, async (_req, res) => {
|
||||||
const [history, wf] = await Promise.all([loadHistory(), loadWorkflow()])
|
const [history, wf] = await Promise.all([loadHistory(), loadWorkflow()])
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface DashboardData {
|
|||||||
lastActivityAt: string | null
|
lastActivityAt: string | null
|
||||||
}
|
}
|
||||||
latestEpisode: { available: boolean; title?: string; episode?: string; season?: string; publishedAt?: string | null; daysAgo?: number | null; stale?: boolean; staleAfterDays?: number }
|
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: {
|
health: {
|
||||||
resendConfigured: boolean
|
resendConfigured: boolean
|
||||||
lastBackup: { ok: boolean; at: string | null; error: string | null; file: string | null }
|
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) {
|
||||||
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.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.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.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.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' })
|
if (data.health.dataDirWritable === false) attention.push({ key: 'disk', label: 'Data directory is not writable', tone: 'alert' })
|
||||||
|
|||||||
Reference in new issue
Block a user