Fix analytics data-loss bugs and clean up the admin dashboard; v2.2.4
Publish Container / build (push) Successful in 16m3s

- Self-heal consent desync: if the server's consent cookie and the
  client's localStorage flag fall out of sync, the client now notices
  and stops silently sending events into a void.
- Stop double-counting hard page loads in hit/visitor stats.
- Flush pending disk writes on SIGTERM/SIGINT so redeploys don't drop
  the last few events.
- Log rate-limiter drops once per window (diagnoses proxy misconfig).
- Widen the bot-detection allowlist to avoid flagging real browsers
  (Chrome-iOS, Samsung Browser, etc.) as bots.
- Stop merging distinct visitors who share an IP when a real session
  cookie is already present — only fall back to IP correlation when no
  cookie was sent at all.
- Include analyticsEvents/episodePlays in backup snapshots and restore
  them, so a restore no longer wipes engagement analytics.
- Regroup admin dashboard KPI cards by time scope (range vs all-time)
  and remove a duplicated Top Countries/States block.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5 committed 2026-09-24 14:06:02 -04:00
1 parent 8496e227b3
commit d1a68754a1
9 files changed
+183 -45

No files matched your search

+60 -1
View File
@@ -232,8 +232,67 @@ Promise.all([
console.error('[series-scheduler] error:', err)
})
app.listen(PORT, () => {
const server = app.listen(PORT, () => {
logResendEmailAlignmentWarnings()
console.log(`Siteforge listening on http://localhost:${PORT}`)
})
// On every deploy the running container gets SIGTERM'd and replaced.
// All the queueXWrite() helpers in data.js debounce disk writes onto
// per-resource promise chains — without draining those here, a redeploy
// can kill the process mid-write and silently lose the last few
// analytics events (or any other pending write) of that container's life.
function collectPendingWrites() {
return [
state.hitStatsWritePromise,
state.visitorStatsWritePromise,
state.contactSubmissionsWritePromise,
state.questionsWritePromise,
state.draftQuestionsWritePromise,
state.replyTemplatesWritePromise,
state.replyHistoryWritePromise,
state.podcastChecklistWritePromise,
state.studyUsersWritePromise,
state.studyCommunityWritePromise,
state.studyRemindersWritePromise,
state.studyCommentsWritePromise,
state.studyCertificatesWritePromise,
state.episodeScriptsWritePromise,
state.downloadCountsWritePromise,
state.episodePlaysWritePromise,
state.analyticsEventsWritePromise,
state.qrCodesWritePromise,
state.emailSettingsWritePromise,
state.calendarEventsWritePromise,
state.auditLogWritePromise,
state.webhooksWritePromise,
state.studySessionsWritePromise,
...state.studyNotesWriteQueues.values(),
...state.studyProgressWriteQueues.values(),
]
}
let shuttingDown = false
async function gracefulShutdown(signal) {
if (shuttingDown) return
shuttingDown = true
console.log(`[shutdown] ${signal} received — closing server and flushing pending writes...`)
server.close()
const forceExit = setTimeout(() => {
console.error('[shutdown] timed out waiting for pending writes; exiting anyway')
process.exit(1)
}, 10_000)
try {
await Promise.all(collectPendingWrites())
console.log('[shutdown] all pending writes flushed')
} catch (err) {
console.error('[shutdown] error while flushing writes:', err)
} finally {
clearTimeout(forceExit)
process.exit(0)
}
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
process.on('SIGINT', () => gracefulShutdown('SIGINT'))
})