first commit
This commit is contained in:
Executable
+336
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
// Timezone: the eternal struggle between server time and human sanity
|
||||
date_default_timezone_set('UTC'); // Adjust to your server's actual timezone
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Paranoid? Maybe. Secure? Definitely.
|
||||
// Nginx filters IPs, but we're not taking chances with your data
|
||||
$allowedIPs = ['127.0.0.1', '::1'];
|
||||
$clientIP = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
|
||||
// Math! It's what separates us from the script kiddies
|
||||
function ipInRange($ip, $range) {
|
||||
list($subnet, $mask) = explode('/', $range);
|
||||
$ipLong = ip2long($ip);
|
||||
$subnetLong = ip2long($subnet);
|
||||
$maskLong = -1 << (32 - (int)$mask);
|
||||
return ($ipLong & $maskLong) === ($subnetLong & $maskLong);
|
||||
}
|
||||
|
||||
$isLocal = in_array($clientIP, $allowedIPs) ||
|
||||
(filter_var($clientIP, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) ||
|
||||
ipInRange($clientIP, '10.0.0.0/8') ||
|
||||
ipInRange($clientIP, '172.16.0.0/12') ||
|
||||
ipInRange($clientIP, '192.168.0.0/16');
|
||||
|
||||
if (!$isLocal) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => '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]);
|
||||
?>
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, GET');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
$dataDir = __DIR__ . '/../data/reactions';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!isset($data['postId']) || !isset($data['reaction'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing required fields']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$postId = $data['postId'];
|
||||
$reaction = $data['reaction'];
|
||||
$visitorId = isset($data['visitorId']) ? $data['visitorId'] : md5($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']);
|
||||
|
||||
$filename = $dataDir . '/post_' . $postId . '.json';
|
||||
|
||||
// Load existing reactions
|
||||
$reactions = [];
|
||||
if (file_exists($filename)) {
|
||||
$reactions = json_decode(file_get_contents($filename), true) ?: [];
|
||||
}
|
||||
|
||||
// Initialize reaction counts
|
||||
if (!isset($reactions['counts'])) {
|
||||
$reactions['counts'] = ['like' => 0, 'love' => 0, 'helpful' => 0];
|
||||
}
|
||||
|
||||
// Initialize user reactions
|
||||
if (!isset($reactions['users'])) {
|
||||
$reactions['users'] = [];
|
||||
}
|
||||
|
||||
// Check if user already reacted
|
||||
$userIndex = -1;
|
||||
foreach ($reactions['users'] as $index => $user) {
|
||||
if ($user['visitorId'] === $visitorId) {
|
||||
$userIndex = $index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($userIndex >= 0) {
|
||||
// User already reacted, update their reaction
|
||||
$oldReaction = $reactions['users'][$userIndex]['reaction'];
|
||||
if ($oldReaction !== $reaction) {
|
||||
// Change reaction - decrement old, increment new
|
||||
if (isset($reactions['counts'][$oldReaction]) && $reactions['counts'][$oldReaction] > 0) {
|
||||
$reactions['counts'][$oldReaction]--;
|
||||
}
|
||||
if (!isset($reactions['counts'][$reaction])) {
|
||||
$reactions['counts'][$reaction] = 0;
|
||||
}
|
||||
$reactions['counts'][$reaction]++;
|
||||
$reactions['users'][$userIndex]['reaction'] = $reaction;
|
||||
} else {
|
||||
// Same reaction, remove it (toggle off)
|
||||
if (isset($reactions['counts'][$reaction]) && $reactions['counts'][$reaction] > 0) {
|
||||
$reactions['counts'][$reaction]--;
|
||||
}
|
||||
unset($reactions['users'][$userIndex]);
|
||||
$reactions['users'] = array_values($reactions['users']);
|
||||
}
|
||||
} else {
|
||||
// New reaction
|
||||
if (!isset($reactions['counts'][$reaction])) {
|
||||
$reactions['counts'][$reaction] = 0;
|
||||
}
|
||||
$reactions['counts'][$reaction]++;
|
||||
$reactions['users'][] = [
|
||||
'visitorId' => $visitorId,
|
||||
'reaction' => $reaction,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
// Ensure all reaction types exist
|
||||
if (!isset($reactions['counts']['like'])) $reactions['counts']['like'] = 0;
|
||||
if (!isset($reactions['counts']['love'])) $reactions['counts']['love'] = 0;
|
||||
if (!isset($reactions['counts']['helpful'])) $reactions['counts']['helpful'] = 0;
|
||||
|
||||
// Save reactions
|
||||
file_put_contents($filename, json_encode($reactions, JSON_PRETTY_PRINT));
|
||||
|
||||
echo json_encode(['success' => true, 'counts' => $reactions['counts']]);
|
||||
|
||||
} else if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
// Get reactions for a post
|
||||
if (!isset($_GET['postId']) || $_GET['postId'] === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing postId']);
|
||||
exit;
|
||||
}
|
||||
$postId = $_GET['postId'];
|
||||
|
||||
$filename = $dataDir . '/post_' . $postId . '.json';
|
||||
|
||||
if (file_exists($filename)) {
|
||||
$reactions = json_decode(file_get_contents($filename), true);
|
||||
echo json_encode(['success' => true, 'counts' => $reactions['counts'] ?? []]);
|
||||
} else {
|
||||
echo json_encode(['success' => true, 'counts' => ['like' => 0, 'love' => 0, 'helpful' => 0]]);
|
||||
}
|
||||
} else {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method not allowed']);
|
||||
}
|
||||
?>
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
// Time is an illusion, but timestamps are real - use your server's timezone
|
||||
date_default_timezone_set('UTC'); // Adjust to your server's actual timezone
|
||||
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!isset($data['type']) || !isset($data['page'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing required fields']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$dataDir = __DIR__ . '/../data/analytics';
|
||||
// Calculate date and hour from current timestamp to ensure timezone consistency
|
||||
$now = time();
|
||||
$date = date('Y-m-d', $now);
|
||||
$hour = (int)date('H', $now);
|
||||
$filename = $dataDir . '/visits_' . $date . '.json';
|
||||
|
||||
// Load existing data - because we're not starting from scratch every time
|
||||
$visits = [];
|
||||
if (file_exists($filename)) {
|
||||
$visits = json_decode(file_get_contents($filename), true) ?: [];
|
||||
}
|
||||
|
||||
// Generate visitor ID - a simple fingerprint that's probably unique enough
|
||||
// Privacy advocates hate this one simple trick
|
||||
$visitorId = isset($data['visitorId']) ? $data['visitorId'] : md5($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']);
|
||||
|
||||
// Check if new or returning visitor - because we care about your browsing habits
|
||||
$isNewVisitor = true;
|
||||
foreach ($visits as $visit) {
|
||||
if ($visit['visitorId'] === $visitorId) {
|
||||
$isNewVisitor = false;
|
||||
break; // Found you, you can't hide
|
||||
}
|
||||
}
|
||||
|
||||
// Add visit record - we're watching, always watching
|
||||
// Timestamps are the only truth in this digital wasteland
|
||||
$timestamp = time();
|
||||
$visitRecord = [
|
||||
'timestamp' => $timestamp,
|
||||
'hour' => (int)date('H', $timestamp),
|
||||
'visitorId' => $visitorId,
|
||||
'page' => $data['page'],
|
||||
'type' => $data['type'],
|
||||
'isNew' => $isNewVisitor,
|
||||
'userAgent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||
'ip' => $_SERVER['REMOTE_ADDR'] ?? ''
|
||||
];
|
||||
|
||||
// Add platform if it's a share
|
||||
if ($data['type'] === 'share' && isset($data['platform'])) {
|
||||
$visitRecord['platform'] = $data['platform'];
|
||||
}
|
||||
|
||||
$visits[] = $visitRecord;
|
||||
|
||||
// Save data
|
||||
file_put_contents($filename, json_encode($visits, JSON_PRETTY_PRINT));
|
||||
|
||||
// Also update daily summary - because raw data is messy and we like order
|
||||
$summaryFile = $dataDir . '/summary_' . $date . '.json';
|
||||
$summary = [];
|
||||
if (file_exists($summaryFile)) {
|
||||
$summary = json_decode(file_get_contents($summaryFile), true) ?: [];
|
||||
}
|
||||
|
||||
if (!isset($summary['total'])) $summary['total'] = 0;
|
||||
if (!isset($summary['new'])) $summary['new'] = 0;
|
||||
if (!isset($summary['returning'])) $summary['returning'] = 0;
|
||||
if (!isset($summary['byHour'])) $summary['byHour'] = array_fill(0, 24, 0);
|
||||
if (!isset($summary['shares'])) $summary['shares'] = ['mastodon' => 0, 'bluesky' => 0, 'copy' => 0];
|
||||
if (!isset($summary['rss'])) $summary['rss'] = 0;
|
||||
|
||||
if ($data['type'] === 'share' && isset($data['platform'])) {
|
||||
// Track share
|
||||
$platform = $data['platform'];
|
||||
if (isset($summary['shares'][$platform])) {
|
||||
$summary['shares'][$platform]++;
|
||||
}
|
||||
} else if ($data['type'] === 'rss_click') {
|
||||
// Track RSS button click
|
||||
$summary['rss']++;
|
||||
} else {
|
||||
// Track pageview
|
||||
$summary['total']++;
|
||||
if ($isNewVisitor) {
|
||||
$summary['new']++;
|
||||
} else {
|
||||
$summary['returning']++;
|
||||
}
|
||||
// Use hour calculated from timestamp for consistency
|
||||
$hourFromTimestamp = (int)date('H', $timestamp);
|
||||
$summary['byHour'][$hourFromTimestamp]++;
|
||||
}
|
||||
|
||||
file_put_contents($summaryFile, json_encode($summary, JSON_PRETTY_PRINT));
|
||||
|
||||
echo json_encode(['success' => true, 'visitorId' => $visitorId, 'isNew' => $isNewVisitor]);
|
||||
?>
|
||||
Reference in New Issue
Block a user