Analytics overhaul: privacy fixes, All Time range, readability improvements; v2.0.29
Publish Container / docker (push) Successful in 14m17s

- Remove raw IP storage; store only SHA-256 hash prefix in visitor records
- Non-blocking geo lookup: recordVisitor now synchronous, geo resolved in background
- UTM capture race condition fixed via sessionStorage pre-consent capture
- Add consent + bot checks to play endpoint (was missing)
- Add rate limiter to pageview endpoint (60 req/min per IP)
- Add 'All time' range option alongside 7d/30d/90d with trendData aggregation
- AnalyticsPanel: tabbed layout (Traffic/Engagement/System), dynamic chart titles,
  range buttons with active state, visitor log uses ipHash instead of raw IP,
  trendData drives main chart with weekly bucketing for all-time view
- Re-add prop-types dependency (required by react-simple-maps bundler)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-09-17 16:27:53 -04:00
1 parent da5c1c5078
commit 4002f1ee93
6 files changed
+333 -231

No files matched your search

+4 -4
View File
@@ -546,7 +546,7 @@ export interface AdminStats {
recentVisits: Array<{
at: string
visitorId: string
ip: string
ipHash?: string
path: string
referrer?: string
device?: string
@@ -1531,7 +1531,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
const [navOpenGroups, setNavOpenGroups] = useState<Set<string>>(new Set())
const [stats, setStats] = useState<AdminStats | null>(null)
const [statsStatus, setStatsStatus] = useState<'loading' | 'ready' | 'error'>('loading')
const [statsRange, setStatsRange] = useState<'7d' | '30d' | '90d'>('30d')
const [statsRange, setStatsRange] = useState<'7d' | '30d' | '90d' | 'all'>('30d')
const [maintenanceMsg, setMaintenanceMsg] = useState('')
const [opsMsg, setOpsMsg] = useState('')
const [backupFiles, setBackupFiles] = useState<BackupPreview[]>([])
@@ -1883,7 +1883,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
})
}, [selectedBackup])
async function reloadStats(range?: '7d' | '30d' | '90d') {
async function reloadStats(range?: '7d' | '30d' | '90d' | 'all') {
const r = await fetch(`/api/admin-stats?range=${range ?? statsRange}`)
if (!r.ok) throw new Error('Could not refresh stats')
const data = await r.json()
@@ -5592,7 +5592,7 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
stats={stats}
statsStatus={statsStatus}
statsRange={statsRange}
onRangeChange={async (range) => {
onRangeChange={async (range: '7d' | '30d' | '90d' | 'all') => {
setStatsRange(range)
setStatsStatus('loading')
try { await reloadStats(range) } catch { setStatsStatus('error') }