Enhance admin dashboard and scripture linking
This commit is contained in:
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user