= ?;", [$since]); } /** Recent lines in the PHP error log that came from a plugin hook. */ function acp_analytics_plugin_errors(int $sinceDays): int { $path = trim((string)(@ini_get('error_log') ?: '')); if ($path === '' || !is_file($path) || !is_readable($path)) { return 0; } $handle = @fopen($path, 'rb'); if ($handle === false) { return 0; } $cutoff = time() - ($sinceDays * 86400); $count = 0; $pattern = '/\[(\d{2}-\w{3}-\d{4} \d{2}:\d{2}:\d{2})[^\]]*\].*\[ZnoteX plugin\]/'; while (($line = fgets($handle)) !== false) { if (strpos($line, '[ZnoteX plugin]') === false) { continue; } if (preg_match($pattern, $line, $m)) { $ts = strtotime($m[1]); if ($ts !== false && $ts < $cutoff) { continue; } } $count++; } fclose($handle); return $count; } function znote_analytics_snapshot(): array { $now = time(); $day = 86400; $data = array(); // -------------------------------------------------------------- Accounts $data['accounts_24h'] = acp_analytics_period_count('znote_accounts', 'created', $now - $day); $data['accounts_7d'] = acp_analytics_period_count('znote_accounts', 'created', $now - 7 * $day); $data['accounts_30d'] = acp_analytics_period_count('znote_accounts', 'created', $now - 30 * $day); $data['accounts_daily'] = array(); if (znote_table_exists('znote_accounts')) { $rows = db()->fetchAll(" SELECT FLOOR((? - `created`) / {$day}) AS `days_ago`, COUNT(*) AS `c` FROM `znote_accounts` WHERE `created` >= ? GROUP BY `days_ago`; ", [$now, $now - 14 * $day]); $byDay = array(); foreach ((is_array($rows) ? $rows : array()) as $row) { $byDay[(int)$row['days_ago']] = (int)$row['c']; } for ($i = 13; $i >= 0; $i--) { $data['accounts_daily'][] = array('label' => date('M j', $now - $i * $day), 'count' => $byDay[$i] ?? 0); } } // ---------------------------------------------------------- Active players $data['active_24h'] = acp_analytics_period_count('players', 'lastlogin', $now - $day); $data['active_7d'] = acp_analytics_period_count('players', 'lastlogin', $now - 7 * $day); $data['active_30d'] = acp_analytics_period_count('players', 'lastlogin', $now - 30 * $day); // New vs returning, last 30 days: a returning player logged in during the // window but their account existed before it started. $data['new_players_30d'] = 0; $data['returning_players_30d'] = 0; if (znote_table_exists('players') && znote_column_exists('players', 'lastlogin') && znote_table_exists('accounts') && znote_column_exists('accounts', 'created')) { $windowStart = $now - 30 * $day; $data['new_players_30d'] = acp_count(" SELECT COUNT(DISTINCT `p`.`id`) AS `c` FROM `players` `p` INNER JOIN `accounts` `a` ON `a`.`id` = `p`.`account_id` WHERE `p`.`lastlogin` >= ? AND `a`.`created` >= ?; ", [$windowStart, $windowStart]); $data['returning_players_30d'] = acp_count(" SELECT COUNT(DISTINCT `p`.`id`) AS `c` FROM `players` `p` INNER JOIN `accounts` `a` ON `a`.`id` = `p`.`account_id` WHERE `p`.`lastlogin` >= ? AND `a`.`created` < ?; ", [$windowStart, $windowStart]); } // ------------------------------------------------------------- Peak online $record = znote_record_get(); $data['peak_online'] = $record['players']; $data['peak_online_at'] = $record['time']; $data['online_now'] = znote_server_adapter()->onlineCount(); // --------------------------------------------------------- Shop / revenue $data['orders_30d'] = acp_analytics_period_count('znote_shop_orders', 'time', $now - 30 * $day); $data['revenue_30d'] = 0.0; $data['revenue_currency'] = ''; $data['payments_failed_30d'] = 0; $data['payments_pending_30d'] = 0; if (znote_table_exists('znote_payment_transactions')) { $since = $now - 30 * $day; $paid = db()->fetchOne(" SELECT COALESCE(SUM(`price`), 0) AS `total`, MAX(`currency`) AS `currency`, COUNT(*) AS `c` FROM `znote_payment_transactions` WHERE `credited` = 1 AND `created_at` >= ?; ", [$since]); if (is_array($paid)) { $data['revenue_30d'] = (float)$paid['total']; $data['revenue_currency'] = (string)($paid['currency'] ?? ''); } $data['payments_failed_30d'] = acp_count(" SELECT COUNT(*) AS `c` FROM `znote_payment_transactions` WHERE `credited` = 0 AND `created_at` >= ? AND `status` IN ('failed', 'declined', 'cancelled', 'expired', 'error', 'not_paid', 'not_approved'); ", [$since]); $data['payments_pending_30d'] = acp_count(" SELECT COUNT(*) AS `c` FROM `znote_payment_transactions` WHERE `credited` = 0 AND `created_at` >= ? AND `status` NOT IN ('failed', 'declined', 'cancelled', 'expired', 'error', 'not_paid', 'not_approved'); ", [$since]); } // -------------------------------------------------------------- Countries $data['top_countries'] = array(); if (znote_table_exists('znote_accounts') && znote_column_exists('znote_accounts', 'flag')) { $rows = db()->fetchAll(" SELECT `flag`, COUNT(*) AS `c` FROM `znote_accounts` WHERE `flag` <> '' GROUP BY `flag` ORDER BY `c` DESC LIMIT 10; "); $data['top_countries'] = is_array($rows) ? $rows : array(); } // ---------------------------------------------------------------- Tickets $data['tickets_open'] = acp_badge_helpdesk(); $data['tickets_30d'] = acp_analytics_period_count('znote_tickets', 'creation', $now - 30 * $day); // ---------------------------------------------------------- Plugin errors $data['plugin_errors_7d'] = acp_analytics_plugin_errors(7); return $data; } $acp_analytics_cache = new Cache('engine/cache/acp_analytics'); $acp_analytics_cache->setExpiration(300); if (isset($_GET['refresh'])) { $acp_analytics_cache->delete(); acp_redirect('analytics'); } if (!$acp_analytics_cache->hasExpired() && is_array($acp_analytics_cache->load())) { $stats = $acp_analytics_cache->load(); } else { $stats = znote_analytics_snapshot(); $acp_analytics_cache->setContent($stats); $acp_analytics_cache->save(); } $maxDaily = max(1, ...array_map(static fn($d) => (int)$d['count'], $stats['accounts_daily'] ?: array(array('count' => 0)))); ?>