Add QR code tracker to admin panel

Each QR code gets a /qr/<slug> redirect that logs scans (IP, user agent,
timestamp) to disk. The admin QR Codes view lets you add, edit, enable/disable,
and delete codes, with per-code scan history inline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-06-16 09:33:35 -04:00
parent 37fc69680e
commit 48c2f81d00
6 changed files with 308 additions and 2 deletions
+6 -1
View File
@@ -1,5 +1,6 @@
import express from 'express'
import { hasVisitorConsent } from './server/helpers.js'
import { isValidAdminSession } from './server/auth.js'
import { BACKUP_INTERVAL_MS } from './server/config.js'
import { state } from './server/state.js'
import {
@@ -18,6 +19,7 @@ import {
loadEpisodeScriptsFromDisk,
migrateStudyNotesIfNeeded,
loadDownloadCountsFromDisk,
loadQrCodesFromDisk,
loadPodcastChecklistFromDisk,
createBackupSnapshot,
refreshContentCaches,
@@ -48,6 +50,7 @@ import { register as registerQuestions } from './server/routes/questions.js'
import { register as registerAnalytics } from './server/routes/analytics.js'
import { register as registerDownloads } from './server/routes/downloads.js'
import { register as registerEpisodes } from './server/routes/episodes.js'
import { register as registerQrCodes } from './server/routes/qr-codes.js'
import { register as registerPublic } from './server/routes/public.js'
const app = express()
@@ -70,6 +73,7 @@ registerQuestions(app)
registerAnalytics(app)
registerDownloads(app)
registerEpisodes(app)
registerQrCodes(app)
// Hit-counting middleware (must come before public routes)
app.use((req, res, next) => {
@@ -78,7 +82,7 @@ app.use((req, res, next) => {
const botDetection = detectBot(ua)
recordHit(req.path, botDetection.isBot, botDetection.reason)
queueHitStatsWrite()
if (hasVisitorConsent(req) && !botDetection.isBot) {
if (hasVisitorConsent(req) && !botDetection.isBot && !isValidAdminSession(req)) {
recordVisitor(req, res).catch(err => {
console.error('[visitor-stats] failed to record visitor:', err)
})
@@ -107,6 +111,7 @@ Promise.all([
loadEpisodeScriptsFromDisk(),
migrateStudyNotesIfNeeded(),
loadDownloadCountsFromDisk(),
loadQrCodesFromDisk(),
loadPodcastChecklistFromDisk(),
refreshContentCaches(),
])