import { 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') return

Failed 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 (

Analytics Dashboard

Real-time insights into site traffic, bot detection, visitor behavior, and data management.

{/* KPI Grid */}

Total Hits

{stats.totalHits.toLocaleString()}

All page requests

Real Traffic

{realHits.toLocaleString()}

{totalHits > 0 ? ((realHits / totalHits) * 100).toFixed(1) : '0.0'}% of total

Bot Traffic

{botHits.toLocaleString()}

{totalHits > 0 ? ((botHits / totalHits) * 100).toFixed(1) : '0.0'}% of total

Last 30 Days

{last30DaysRealTotal.toLocaleString()}

Real hits • {last30DaysBotTotal.toLocaleString()} bots

Unique Visitors

{stats.visitors.uniqueVisitors.toLocaleString()}

Tracked with consent

Returning Rate

{stats.visitors.totalVisits > 0 ? `${Math.round((stats.visitors.returningVisits / stats.visitors.totalVisits) * 100)}%` : '0%'}

{stats.visitors.returningVisits.toLocaleString()} return visits
{/* Chart Tabs */}
{/* Overview Charts */} {chartTab === 'overview' && (

7-Day Trend (Real vs Bot)

Traffic Composition

)} {/* Bot Breakdown Charts */} {chartTab === 'breakdown' && (

Bot Sources

{botReasons.length === 0 ? (

No bot traffic detected.

) : ( )}

Bot Detection Details

{botReasons.length === 0 ? (

No bots detected yet.

) : (
    {botReasons.map((reason: { reason: string; count: number }) => (
  • {reason.reason.replace(/-/g, ' ')} {reason.count.toLocaleString()}
  • ))}
)}
)} {/* Geographic Charts */} {chartTab === 'geographic' && (

Top Pages

)} {/* Recent Visitors Table */}

Visitor Details

Real-time tracking of visitor activity, geography, and returning behavior.

Privacy: visitor analytics only run after cookie consent. IPs below are masked.

Total Visits

{stats.visitors.totalVisits.toLocaleString()}

Unique Visitors

{stats.visitors.uniqueVisitors.toLocaleString()}

Returning Visits

{stats.visitors.returningVisits.toLocaleString()}

First Visit

{formatDate(stats.visitors.firstVisitAt)}

Top Countries

    {stats.visitors.topCountries.map((item: { name: string; hits: number }) => (
  • {item.name}{item.hits.toLocaleString()}
  • ))}

Top States

    {stats.visitors.topStates.map((item: { name: string; hits: number }) => (
  • {item.name}{item.hits.toLocaleString()}
  • ))}

Recent Visitor Log

{stats.visitors.recentVisits.length === 0 ? (

No visitor records yet.

) : (
{stats.visitors.recentVisits.map(row => ( ))}
Time IP Country Path Returning Visit #
{formatDate(row.at)} {maskIp(row.ip)} {row.country || '—'} {row.path} {row.returningVisitor ? 'Returning' : 'New'} {row.visitCount}
)}
{/* Contact Summary */}

Contact Summary

Submission totals from the contact form.

Total Contact Messages

{stats.contactTotals.totalSubmissions.toLocaleString()}

Total Bible Questions

{stats.contactTotals.totalQuestions.toLocaleString()}

{/* Deployment & Cache Status */}

Deployment & Cache Status

Current deployment metadata and cache purge health for the live app.

Build Commit

{opsStatus?.buildCommit ?? 'Not available'}

Build Number

{opsStatus?.buildNumber ?? 'Not available'}

Deployed At

{formatDate(opsStatus?.deployedAt ?? null)}

Cache Purge

{opsStatus?.cachePurge.ok ? 'Healthy' : 'Needs setup'}

{formatDate(opsStatus?.cachePurge.at ?? null)}

{/* Data Management */}

Data Management

Export, backup, or retain only recent analytics data.

Hit Stats Write

{stats.writeStatus.hitStats.ok ? 'Healthy' : 'Error'}

{formatDate(stats.writeStatus.hitStats.at)}

Visitor Stats Write

{stats.writeStatus.visitorStats.ok ? 'Healthy' : 'Error'}

{formatDate(stats.writeStatus.visitorStats.at)}

Backup Status

{stats.writeStatus.backups.ok ? 'Healthy' : 'Error'}

{formatDate(stats.writeStatus.backups.at)}

{selectedBackupPreview && (

Restore Preview

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 &&

{maintenanceMsg}

}
) }