Enhance admin dashboard and scripture linking

This commit is contained in:
nmemmert
2026-05-12 09:12:55 -04:00
parent 0703a802ff
commit becc1b5b27
4 changed files with 976 additions and 21 deletions
+68
View File
@@ -58,6 +58,7 @@ const REPLY_HISTORY_FILE = path.join(DATA_DIR, 'admin-reply-history.json')
const BACKUP_DIR = path.join(DATA_DIR, 'backups')
const UPLOADS_DIR = path.join(DATA_DIR, 'uploads')
const UPLOADS_META_FILE = path.join(DATA_DIR, 'uploads-meta.json')
const DOWNLOAD_COUNTS_FILE = path.join(DATA_DIR, 'download-counts.json')
const DIST_DIR = path.join(__dirname, 'dist')
const INDEX_FILE = path.join(DIST_DIR, 'index.html')
const DIST_IMAGES_DIR = path.join(DIST_DIR, 'images')
@@ -258,6 +259,33 @@ async function readUploadsMetadata() {
}
}
function loadDownloadCountsFromDisk() {
return readFile(DOWNLOAD_COUNTS_FILE, 'utf8')
.then(raw => {
const parsed = JSON.parse(raw)
downloadCounts = (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {}
})
.catch(() => {
downloadCounts = {}
})
}
function queueDownloadCountsWrite() {
downloadCountsWritePromise = downloadCountsWritePromise
.then(async () => {
await mkdir(DATA_DIR, { recursive: true })
await writeFile(DOWNLOAD_COUNTS_FILE, JSON.stringify(downloadCounts, null, 2), 'utf8')
})
.catch(err => {
console.error('[download-counts] failed to write:', err)
})
}
function incrementDownloadCount(resourceKey) {
downloadCounts[resourceKey] = (downloadCounts[resourceKey] ?? 0) + 1
queueDownloadCountsWrite()
}
async function writeUploadsMetadata(metadata) {
await mkdir(DATA_DIR, { recursive: true })
await writeFile(UPLOADS_META_FILE, JSON.stringify(metadata, null, 2), 'utf8')
@@ -359,6 +387,8 @@ let contactSubmissions = []
let contactSubmissionsWritePromise = Promise.resolve()
let questions = []
let questionsWritePromise = Promise.resolve()
let downloadCounts = {}
let downloadCountsWritePromise = Promise.resolve()
let lastVisitorStatsWrite = { ok: true, at: null, error: null }
let lastHitStatsWrite = { ok: true, at: null, error: null }
let lastBackupStatus = { ok: true, at: null, error: null, file: null }
@@ -1918,6 +1948,40 @@ app.post('/api/admin-contact-submissions/:id/reply', requireAdminAuth, async (re
}
})
app.get('/api/admin-download-stats', requireAdminAuth, (_req, res) => {
res.json({ counts: downloadCounts })
})
app.get('/api/admin-subscribers', requireAdminAuth, (_req, res) => {
const seen = new Set()
const subscribers = contactSubmissions
.filter(entry => entry.subscribe === true && entry.email && !seen.has(entry.email) && seen.add(entry.email))
.map(entry => ({
name: entry.name,
email: entry.email,
subscribedAt: entry.submittedAt,
source: entry.message?.startsWith('Requested') ? 'download' : 'contact-form',
}))
.sort((a, b) => new Date(b.subscribedAt).getTime() - new Date(a.subscribedAt).getTime())
res.json({ subscribers, total: subscribers.length })
})
app.post('/api/admin-subscribers/export', requireAdminAuth, (_req, res) => {
const seen = new Set()
const rows = [['Name', 'Email', 'Subscribed At', 'Source']]
contactSubmissions
.filter(entry => entry.subscribe === true && entry.email && !seen.has(entry.email) && seen.add(entry.email))
.sort((a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime())
.forEach(entry => {
const source = entry.message?.startsWith('Requested') ? 'download' : 'contact-form'
rows.push([entry.name, entry.email, entry.submittedAt, source])
})
const csv = rows.map(row => row.map(cell => `"${String(cell ?? '').replace(/"/g, '""')}"`).join(',')).join('\n')
res.setHeader('Content-Type', 'text/csv')
res.setHeader('Content-Disposition', `attachment; filename="subscribers-${new Date().toISOString().slice(0, 10)}.csv"`)
res.send(csv)
})
app.get('/api/admin-stats/export', requireAdminAuth, async (_req, res) => {
let adminContent = null
let draftContent = null
@@ -2105,6 +2169,8 @@ app.post('/api/study-downloads/titus', studyDownloadRateLimit, async (req, res)
await syncContactToResend(trimmedName, trimmedEmail)
}
incrementDownloadCount('titus-study')
if (configuredDownloadUrl) {
res.json({ ok: true, downloadUrl: configuredDownloadUrl })
return
@@ -2212,6 +2278,7 @@ app.post('/api/resource-download', studyDownloadRateLimit, async (req, res) => {
await syncContactToResend(trimmedName, trimmedEmail)
}
incrementDownloadCount(`resource:${resourceId}`)
res.json({ ok: true, downloadUrl: resource.url.trim() })
} catch (err) {
console.error('[resource-download] request error:', err)
@@ -2991,6 +3058,7 @@ Promise.all([
loadReplyHistoryFromDisk(),
loadQuestionsFromDisk(),
loadDraftQuestionsFromDisk(),
loadDownloadCountsFromDisk(),
refreshContentCaches(),
])
.catch(err => {