diff options
| author | sillylaird <sillyfanboy@gmail.com> | 2026-09-03 00:33:59 +0000 |
|---|---|---|
| committer | sillylaird <sillyfanboy@gmail.com> | 2026-09-03 00:33:59 +0000 |
| commit | 898b52edcb47bcb3e9d6106e74ca73e74ea01e70 (patch) | |
| tree | 85c6ee5ad58b860144551184d4cf86b560c62b91 /api/traffic-stats.php | |
| download | www-main.tar.gz www-main.zip | |
Diffstat (limited to 'api/traffic-stats.php')
| -rw-r--r-- | api/traffic-stats.php | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/api/traffic-stats.php b/api/traffic-stats.php new file mode 100644 index 0000000..f8fd70a --- /dev/null +++ b/api/traffic-stats.php @@ -0,0 +1,95 @@ +<?php +/** + * Admin-only live traffic stats (JSON). + * Used by /admin.php?section=traffic to poll and redraw charts in realtime. + * + * Requires the shared admin session. Never stores or returns IPs. + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../partials/session.php'; + +header('Content-Type: application/json; charset=utf-8'); +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('X-Content-Type-Options: nosniff'); +header('Referrer-Policy: no-referrer'); +header('X-Robots-Tag: noindex'); + +if (empty($_SESSION['admin'])) { + http_response_code(401); + echo json_encode(['ok' => false, 'err' => 'auth required']); + exit; +} + +if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'GET') { + http_response_code(405); + echo json_encode(['ok' => false, 'err' => 'GET only']); + exit; +} + +require_once __DIR__ . '/../partials/traffic.php'; + +try { + $db = traffic_db(); + $latestHitId = (int)$db->query('SELECT COALESCE(MAX(id), 0) FROM hits')->fetchColumn(); + $hitCount = (int)$db->query('SELECT COUNT(*) FROM hits')->fetchColumn(); + + $summary = traffic_summary(); + $analytics = traffic_analytics(); + $top = traffic_top_pages(80); + $recent = traffic_recent(150); + + // Normalize recent rows for the client (bool is_bot, formatted fields optional). + $recentOut = []; + foreach ($recent as $r) { + $isBot = !empty($r['is_bot']) || traffic_visitor_is_bot((string)($r['visitor'] ?? '')); + $recentOut[] = [ + 'id' => (int)($r['id'] ?? 0), + 'ts' => (int)($r['ts'] ?? 0), + 'host' => (string)($r['host'] ?? ''), + 'path' => (string)($r['path'] ?? ''), + 'visitor' => (string)($r['visitor'] ?? ''), + 'lang' => (string)($r['lang'] ?? ''), + 'ref_host' => (string)($r['ref_host'] ?? ''), + 'local_count' => (int)($r['local_count'] ?? 0), + 'is_new' => !empty($r['is_new']) ? 1 : 0, + 'is_bot' => $isBot ? 1 : 0, + ]; + } + + $topOut = []; + foreach ($top as $p) { + $topOut[] = [ + 'page_key' => (string)($p['page_key'] ?? ''), + 'host' => (string)($p['host'] ?? ''), + 'path' => (string)($p['path'] ?? ''), + 'hits' => (int)($p['hits'] ?? 0), + 'unique_sessions' => (int)($p['unique_sessions'] ?? 0), + 'bot_hits' => (int)($p['bot_hits'] ?? 0), + 'last_local_count' => (int)($p['last_local_count'] ?? 0), + 'last_visitor' => (string)($p['last_visitor'] ?? ''), + 'last_hit' => (int)($p['last_hit'] ?? 0), + ]; + } + + // Fingerprint so the client can skip redraw when nothing changed. + $rev = $latestHitId . ':' . $hitCount + . ':' . (int)($summary['total_hits'] ?? 0) + . ':' . (int)($summary['bot_hits'] ?? 0) + . ':' . (int)($summary['today'] ?? 0) + . ':' . (int)($summary['bot_today'] ?? 0); + + echo json_encode([ + 'ok' => true, + 'rev' => $rev, + 'server_time' => time(), + 'summary' => $summary, + 'analytics' => $analytics, + 'top' => $topOut, + 'recent' => $recentOut, + ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); +} catch (Throwable $e) { + http_response_code(500); + echo json_encode(['ok' => false, 'err' => 'store failed']); +} |
