Total Hits
{stats.totalHits.toLocaleString()}
All page requestsimport { useState } from 'react' import { Line, Bar, Pie, Doughnut } from 'react-chartjs-2' import GeoMaps from './GeoMaps' import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, BarElement, Title, Tooltip, Legend, ArcElement, } from 'chart.js' import type { AdminStats } from '../AdminPage' ChartJS.register( CategoryScale, LinearScale, PointElement, LineElement, BarElement, Title, Tooltip, Legend, ArcElement, ) interface Props { stats: AdminStats | null statsStatus: 'loading' | 'error' | 'ready' opsStatus: { buildCommit: string | null; buildNumber: string | null; deployedAt: string | null; cachePurge: { ok: boolean; at: string | null; error: string | null }; deployHook: { ok: boolean; at: string | null; error: string | null } } | null formatDate: (date: string | null) => string maskIp: (ip: string) => string maintenanceMsg: string | null selectedBackup: string backupFiles: Array<{ filename: string; sizeBytes: number; createdAt: string | null; reason: string; adminUpdatedAt: string | null; totalHits: number; totalVisits: number }> selectedBackupPreview: { filename: string; sizeBytes: number; createdAt: string | null; reason: string; adminUpdatedAt: string | null; totalHits: number; totalVisits: number } | null onBackupSelect: (filename: string) => void onRestore: () => void onExport: () => void onBackupNow: () => void onPrune: () => void onClear: () => void onPurgeCache: () => void onDeployHook: () => void onRefreshStatus: () => void } export function AnalyticsPanel({ stats, statsStatus, opsStatus, formatDate, maskIp, maintenanceMsg, selectedBackup, backupFiles, selectedBackupPreview, onBackupSelect, onRestore, onExport, onBackupNow, onPrune, onClear, onPurgeCache, onDeployHook, onRefreshStatus, }: Props) { const [chartTab, setChartTab] = useState<'overview' | 'breakdown' | 'geographic'>('overview') if (statsStatus === 'loading') return
Loading analytics…
if (statsStatus === 'error') returnFailed to load analytics.
if (!stats) return null // Normalize fields that may be missing from older data const realHits = stats.realHits ?? 0 const botHits = stats.botHits ?? 0 const totalHits = stats.totalHits ?? 0 const last7DaysReal = stats.last7DaysReal ?? stats.last7Days.map((d: { day: string; hits: number }) => ({ day: d.day, hits: 0 })) const last7DaysBot = stats.last7DaysBot ?? stats.last7Days.map((d: { day: string; hits: number }) => ({ day: d.day, hits: 0 })) const last30DaysRealTotal = stats.last30DaysRealTotal ?? 0 const last30DaysBotTotal = stats.last30DaysBotTotal ?? 0 const topPathsReal = stats.topPathsReal ?? stats.topPaths const topPathsBot = stats.topPathsBot ?? [] const botReasons = stats.botReasons ?? [] // Chart data - Daily trend const dailyChartData = { labels: stats.last7Days.map((d: { day: string; hits: number }) => d.day), datasets: [ { label: 'Real Visitors', data: last7DaysReal.map((d: { day: string; hits: number }) => d.hits), borderColor: '#c9a84c', backgroundColor: 'rgba(201, 168, 76, 0.1)', tension: 0.4, fill: true, }, { label: 'Bot Traffic', data: last7DaysBot.map((d: { day: string; hits: number }) => d.hits), borderColor: '#666', backgroundColor: 'rgba(102, 102, 102, 0.1)', tension: 0.4, fill: true, }, ], } // Chart data - Bot breakdown (pie) const botPieData = { labels: botReasons.map((b: { reason: string; count: number }) => b.reason.replace(/-/g, ' ')), datasets: [{ data: botReasons.map((b: { reason: string; count: number }) => b.count), backgroundColor: [ '#c9a84c', '#a0853d', '#6d5a2e', '#8b7635', '#9e8843', '#bbb477', '#7a6930', '#5a4820', '#c0a060', '#746c36', ], borderColor: '#1a1a15', borderWidth: 2, }], } // Traffic ratio (doughnut) const trafficRatioData = { labels: ['Real Visitors', 'Bot Traffic'], datasets: [{ data: [realHits, botHits], backgroundColor: ['#c9a84c', '#999'], borderColor: '#1a1a15', borderWidth: 2, }], } // Top paths comparison const topPathsLabels = Array.from({ length: Math.max(topPathsReal.length, topPathsBot.length) }, (_, i: number) => { const realPath = topPathsReal[i]?.path || '' const botPath = topPathsBot[i]?.path || '' return realPath || botPath || `Path ${i + 1}` }).slice(0, 8) const topPathsChartData = { labels: topPathsLabels, datasets: [ { label: 'Real Hits', data: topPathsLabels.map((_label, i) => topPathsReal[i]?.hits || 0), backgroundColor: '#c9a84c', }, { label: 'Bot Hits', data: topPathsLabels.map((_label, i) => topPathsBot[i]?.hits || 0), backgroundColor: '#999', }, ], } const chartOptions = { responsive: true, maintainAspectRatio: true, plugins: { legend: { labels: { color: '#f0ead8', font: { size: 12 }, }, }, tooltip: { backgroundColor: '#2a2518', titleColor: '#f0ead8', bodyColor: '#f0ead8', borderColor: '#c9a84c', borderWidth: 1, }, }, scales: { y: { beginAtZero: true, ticks: { color: '#7a7060' }, grid: { color: '#2a2518' }, }, x: { ticks: { color: '#7a7060' }, grid: { color: '#2a2518' }, }, }, } return (Real-time insights into site traffic, bot detection, visitor behavior, and data management.
{stats.totalHits.toLocaleString()}
All page requests{realHits.toLocaleString()}
{totalHits > 0 ? ((realHits / totalHits) * 100).toFixed(1) : '0.0'}% of total{botHits.toLocaleString()}
{totalHits > 0 ? ((botHits / totalHits) * 100).toFixed(1) : '0.0'}% of total{last30DaysRealTotal.toLocaleString()}
Real hits • {last30DaysBotTotal.toLocaleString()} bots{stats.visitors.uniqueVisitors.toLocaleString()}
Tracked with consent{stats.visitors.totalVisits > 0 ? `${Math.round((stats.visitors.returningVisits / stats.visitors.totalVisits) * 100)}%` : '0%'}
{stats.visitors.returningVisits.toLocaleString()} return visitsNo bot traffic detected.
) : (No bots detected yet.
) : (Real-time tracking of visitor activity, geography, and returning behavior.
Privacy: visitor analytics only run after cookie consent. IPs below are masked.
{stats.visitors.totalVisits.toLocaleString()}
{stats.visitors.uniqueVisitors.toLocaleString()}
{stats.visitors.returningVisits.toLocaleString()}
{formatDate(stats.visitors.firstVisitAt)}
No visitor records yet.
) : (| Time | IP | Country | Path | Returning | Visit # |
|---|---|---|---|---|---|
| {formatDate(row.at)} | {maskIp(row.ip)} |
{row.country || '—'} | {row.path} | {row.returningVisitor ? 'Returning' : 'New'} | {row.visitCount} |
Submission totals from the contact form.
{stats.contactTotals.totalSubmissions.toLocaleString()}
{stats.contactTotals.totalQuestions.toLocaleString()}
Current deployment metadata and cache purge health for the live app.
{opsStatus?.buildCommit ?? 'Not available'}
{opsStatus?.buildNumber ?? 'Not available'}
{formatDate(opsStatus?.deployedAt ?? null)}
{opsStatus?.cachePurge.ok ? 'Healthy' : 'Needs setup'}
{formatDate(opsStatus?.cachePurge.at ?? null)}
Export, backup, or retain only recent analytics data.
{stats.writeStatus.hitStats.ok ? 'Healthy' : 'Error'}
{formatDate(stats.writeStatus.hitStats.at)}
{stats.writeStatus.visitorStats.ok ? 'Healthy' : 'Error'}
{formatDate(stats.writeStatus.visitorStats.at)}
{stats.writeStatus.backups.ok ? 'Healthy' : 'Error'}
{formatDate(stats.writeStatus.backups.at)}
Backup: {selectedBackupPreview.filename}
Created: {formatDate(selectedBackupPreview.createdAt)}
Size: {(selectedBackupPreview.sizeBytes / 1024).toFixed(1)} KB
Total Hits: {selectedBackupPreview.totalHits.toLocaleString()}
Total Visits: {selectedBackupPreview.totalVisits.toLocaleString()}
{maintenanceMsg}
}