'Access denied']); // No peeking, strangers exit; } $date = isset($_GET['date']) ? $_GET['date'] : date('Y-m-d'); $endDate = isset($_GET['endDate']) ? $_GET['endDate'] : null; $dataDir = __DIR__ . '/../data/analytics'; $reactionsDir = __DIR__ . '/../data/reactions'; // Date range wizard - conjures dates out of thin air function getDatesInRange($startDate, $endDate) { $dates = []; $current = strtotime($startDate); $end = strtotime($endDate); while ($current <= $end) { $dates[] = date('Y-m-d', $current); $current = strtotime('+1 day', $current); } return $dates; } // Load summary data - aggregate if date range is provided $data = [ 'total' => 0, 'new' => 0, 'returning' => 0, 'byHour' => array_fill(0, 24, 0), 'shares' => ['mastodon' => 0, 'bluesky' => 0, 'copy' => 0], 'rss' => 0 ]; if ($endDate && $endDate >= $date) { // Date range mode - aggregate data from multiple days $dates = getDatesInRange($date, $endDate); $allVisits = []; foreach ($dates as $dayDate) { $filename = $dataDir . '/summary_' . $dayDate . '.json'; if (file_exists($filename)) { $dayData = json_decode(file_get_contents($filename), true); if ($dayData) { // Aggregate totals $data['total'] += $dayData['total'] ?? 0; $data['new'] += $dayData['new'] ?? 0; $data['returning'] += $dayData['returning'] ?? 0; // Aggregate shares if (isset($dayData['shares'])) { $data['shares']['mastodon'] += $dayData['shares']['mastodon'] ?? 0; $data['shares']['bluesky'] += $dayData['shares']['bluesky'] ?? 0; $data['shares']['copy'] += $dayData['shares']['copy'] ?? 0; } // Aggregate hourly data if (isset($dayData['byHour']) && is_array($dayData['byHour'])) { for ($i = 0; $i < 24; $i++) { $data['byHour'][$i] += $dayData['byHour'][$i] ?? 0; } } // Collect visits for recent visitors $visitsFile = $dataDir . '/visits_' . $dayDate . '.json'; if (file_exists($visitsFile)) { $dayVisits = json_decode(file_get_contents($visitsFile), true) ?: []; $allVisits = array_merge($allVisits, $dayVisits); } } } else { // Load visits even if summary doesn't exist $visitsFile = $dataDir . '/visits_' . $dayDate . '.json'; if (file_exists($visitsFile)) { $dayVisits = json_decode(file_get_contents($visitsFile), true) ?: []; $allVisits = array_merge($allVisits, $dayVisits); } } } // Recalculate byHour from actual timestamps for the range $recalculatedByHour = array_fill(0, 24, 0); foreach ($allVisits as $visit) { if (isset($visit['type']) && $visit['type'] === 'pageview' && isset($visit['timestamp'])) { $hour = (int)date('H', $visit['timestamp']); if ($hour >= 0 && $hour < 24) { $recalculatedByHour[$hour]++; } } } $data['byHour'] = $recalculatedByHour; // Use all visits for recent visitors $visits = $allVisits; } else { // Single date mode $filename = $dataDir . '/summary_' . $date . '.json'; if (file_exists($filename)) { $data = json_decode(file_get_contents($filename), true); } // Load visits file for this date $visitsFile = $dataDir . '/visits_' . $date . '.json'; $visits = []; if (file_exists($visitsFile)) { $visits = json_decode(file_get_contents($visitsFile), true) ?: []; } // Recalculate byHour from actual timestamps $recalculatedByHour = array_fill(0, 24, 0); foreach ($visits as $visit) { if (isset($visit['type']) && $visit['type'] === 'pageview' && isset($visit['timestamp'])) { $hour = (int)date('H', $visit['timestamp']); if ($hour >= 0 && $hour < 24) { $recalculatedByHour[$hour]++; } } } $data['byHour'] = $recalculatedByHour; } // Ensure RSS tracking exists if (!isset($data['rss'])) { $data['rss'] = 0; } // Calculate active RSS subscribers (readers who fetched in last 7 days) $readersFile = $dataDir . '/rss_readers.json'; $activeRssSubscribers = 0; if (file_exists($readersFile)) { $readers = json_decode(file_get_contents($readersFile), true) ?: []; $sevenDaysAgo = time() - (7 * 24 * 60 * 60); foreach ($readers as $reader) { if (isset($reader['lastFetch']) && $reader['lastFetch'] >= $sevenDaysAgo) { $activeRssSubscribers++; } } } $data['activeRssSubscribers'] = $activeRssSubscribers; // Get 10 most recent unique IP addresses with geolocation function getGeolocation($ip) { // Skip private/local IPs if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) { return ['country' => 'Local', 'city' => 'Private Network', 'isp' => '']; } // Use ip-api.com (free, no API key needed, 45 requests/minute limit) $url = "http://ip-api.com/json/{$ip}?fields=status,country,countryCode,city,region,isp,query"; // Use file_get_contents with stream context $context = stream_context_create([ 'http' => [ 'timeout' => 2, 'method' => 'GET', 'header' => 'User-Agent: Analytics/1.0' ] ]); $response = @file_get_contents($url, false, $context); if ($response) { $geo = json_decode($response, true); if ($geo && isset($geo['status']) && $geo['status'] === 'success') { return [ 'country' => $geo['country'] ?? 'Unknown', 'countryCode' => $geo['countryCode'] ?? '', 'city' => $geo['city'] ?? 'Unknown', 'region' => $geo['region'] ?? '', 'isp' => $geo['isp'] ?? 'Unknown', 'ip' => $geo['query'] ?? $ip ]; } } return ['country' => 'Unknown', 'city' => 'Unknown', 'isp' => 'Unknown', 'ip' => $ip]; } // Get unique IPs with their most recent visit timestamp $ipVisits = []; foreach ($visits as $visit) { $ip = $visit['ip'] ?? ''; if ($ip && !isset($ipVisits[$ip])) { $ipVisits[$ip] = [ 'ip' => $ip, 'timestamp' => $visit['timestamp'] ?? 0, 'page' => $visit['page'] ?? '', 'userAgent' => $visit['userAgent'] ?? '' ]; } elseif ($ip && isset($ipVisits[$ip])) { // Keep the most recent timestamp if (($visit['timestamp'] ?? 0) > $ipVisits[$ip]['timestamp']) { $ipVisits[$ip]['timestamp'] = $visit['timestamp'] ?? 0; $ipVisits[$ip]['page'] = $visit['page'] ?? ''; } } } // Sort by timestamp (most recent first) and get top 10 usort($ipVisits, function($a, $b) { return $b['timestamp'] - $a['timestamp']; }); $recentIPs = array_slice($ipVisits, 0, 10); // Get geolocation for each IP (with caching to avoid rate limits) $geoCacheFile = $dataDir . '/geo_cache.json'; $geoCache = []; if (file_exists($geoCacheFile)) { $geoCache = json_decode(file_get_contents($geoCacheFile), true) ?: []; } $recentVisitors = []; foreach ($recentIPs as $ipVisit) { $ip = $ipVisit['ip']; // Check cache first (cache expires after 24 hours) $geo = null; if (isset($geoCache[$ip]) && (time() - $geoCache[$ip]['cached_at']) < 86400) { $geo = $geoCache[$ip]['data']; } else { $geo = getGeolocation($ip); $geoCache[$ip] = ['data' => $geo, 'cached_at' => time()]; // Small delay to respect API rate limits usleep(200000); // 0.2 second delay } $recentVisitors[] = [ 'ip' => $ip, 'timestamp' => $ipVisit['timestamp'], 'time' => gmdate('Y-m-d\TH:i:s\Z', $ipVisit['timestamp']), // ISO 8601 UTC format for frontend conversion 'page' => $ipVisit['page'], 'country' => $geo['country'] ?? 'Unknown', 'countryCode' => $geo['countryCode'] ?? '', 'city' => $geo['city'] ?? 'Unknown', 'region' => $geo['region'] ?? '', 'isp' => $geo['isp'] ?? 'Unknown' ]; } // Save updated cache if (count($geoCache) > 0) { file_put_contents($geoCacheFile, json_encode($geoCache, JSON_PRETTY_PRINT)); } $data['recentVisitors'] = $recentVisitors; // Ensure shares structure exists and normalize old platform names if (!isset($data['shares'])) { $data['shares'] = ['mastodon' => 0, 'bluesky' => 0, 'copy' => 0]; } else { // Normalize old platform names to new ones $normalizedShares = ['mastodon' => 0, 'bluesky' => 0, 'copy' => 0]; foreach ($data['shares'] as $platform => $count) { if ($platform === 'twitter' || $platform === 'mastodon') { $normalizedShares['mastodon'] += $count; } else if ($platform === 'facebook' || $platform === 'bluesky') { $normalizedShares['bluesky'] += $count; } else if ($platform === 'copy') { $normalizedShares['copy'] += $count; } } $data['shares'] = $normalizedShares; } // Load blog posts and their stats $postsFile = __DIR__ . '/../blog/data/posts.json'; $posts = []; if (file_exists($postsFile)) { $posts = json_decode(file_get_contents($postsFile), true) ?: []; } // Get reactions and shares for each post $blogStats = []; foreach ($posts as $post) { $postId = $post['id']; $reactionFile = $reactionsDir . '/post_' . $postId . '.json'; $reactions = ['like' => 0, 'love' => 0, 'helpful' => 0]; if (file_exists($reactionFile)) { $reactionData = json_decode(file_get_contents($reactionFile), true); if (isset($reactionData['counts'])) { $reactions = $reactionData['counts']; } } // Get shares for this post from visits data (already loaded, includes date range) $postShares = ['mastodon' => 0, 'bluesky' => 0, 'copy' => 0]; foreach ($visits as $visit) { if (isset($visit['type']) && $visit['type'] === 'share' && isset($visit['page']) && strpos($visit['page'], '#post-' . $postId) !== false) { $platform = $visit['platform'] ?? 'copy'; if (isset($postShares[$platform])) { $postShares[$platform]++; } } } $blogStats[] = [ 'id' => $postId, 'title' => $post['title'], 'reactions' => $reactions, 'shares' => $postShares, 'totalReactions' => array_sum($reactions), 'totalShares' => array_sum($postShares) ]; } $data['blogPosts'] = $blogStats; echo json_encode(['success' => true, 'data' => $data]); ?>