db = Database::getInstance(); } /** * Update or create active viewer record */ public function heartbeat($userId, $data = []) { $sql = "INSERT OR REPLACE INTO active_viewers (user_id, nickname, ip_address, session_id, is_admin) VALUES (?, ?, ?, ?, ?)"; $params = [ $userId, $data['nickname'] ?? 'Anonymous', $data['ip_address'] ?? Security::getClientIP(), $data['session_id'] ?? session_id(), $data['is_admin'] ?? false ]; return $this->db->insert($sql, $params); } /** * Get count of active viewers (seen within last X seconds) */ public function getActiveCount($thresholdSeconds = 30) { return $this->db->fetchColumn( "SELECT COUNT(*) FROM active_viewers WHERE last_seen >= datetime('now', '-{$thresholdSeconds} seconds')" ); } /** * Get list of active viewers with details */ public function getActiveViewers($thresholdSeconds = 30) { return $this->db->fetchAll( "SELECT * FROM active_viewers WHERE last_seen >= datetime('now', '-{$thresholdSeconds} seconds') ORDER BY last_seen DESC" ); } /** * Clean up inactive viewers */ public function cleanupInactive($thresholdSeconds = 60) { return $this->db->delete( "DELETE FROM active_viewers WHERE last_seen < datetime('now', '-{$thresholdSeconds} seconds')" ); } /** * Update specific viewer's last seen time */ public function updateLastSeen($userId) { return $this->db->update( "UPDATE active_viewers SET last_seen = ? WHERE user_id = ?", [date('Y-m-d H:i:s'), $userId] ); } /** * Remove viewer from active list */ public function removeViewer($userId) { return $this->db->delete( "DELETE FROM active_viewers WHERE user_id = ?", [$userId] ); } /** * Get viewer by user ID */ public function getViewer($userId) { return $this->db->fetch( "SELECT * FROM active_viewers WHERE user_id = ?", [$userId] ); } /** * Bulk update multiple viewers (for mass heartbeat handling) */ public function bulkHeartbeat($viewersData) { $this->db->beginTransaction(); try { foreach ($viewersData as $viewerData) { $userId = $viewerData['user_id'] ?? ''; if (!empty($userId)) { $this->heartbeat($userId, $viewerData); } } $this->db->commit(); return true; } catch (Exception $e) { $this->db->rollback(); error_log("Bulk heartbeat failed: " . $e->getMessage()); return false; } } /** * Get viewer activity statistics */ public function getActivityStats() { $stats = []; // Current active count $stats['currently_active'] = $this->db->fetchColumn( "SELECT COUNT(*) FROM active_viewers WHERE last_seen >= datetime('now', '-30 seconds')" ); // Peak today $stats['peak_today'] = $this->db->fetchColumn( "SELECT COUNT(*) FROM active_viewers WHERE DATE(last_seen) = DATE('now')" ); // Viewer types $stats['admin_count'] = $this->db->fetchColumn( "SELECT COUNT(*) FROM active_viewers WHERE is_admin = 1 AND last_seen >= datetime('now', '-30 seconds')" ); // Recent joiners (joined in last 5 minutes) $stats['recent_joiners'] = $this->db->fetchColumn( "SELECT COUNT(*) FROM active_viewers WHERE last_seen >= datetime('now', '-5 minutes')" ); // Top user agents $stats['user_agents'] = $this->db->fetchAll( "SELECT user_agent, COUNT(*) as count FROM active_viewers WHERE last_seen >= datetime('now', '-1 hour') GROUP BY user_agent ORDER BY count DESC LIMIT 5" ); return $stats; } /** * Migrate old file-based viewer data if exists */ public function migrateFromFileIfNeeded() { $oldViewersFile = __DIR__ . '/../active_viewers.json'; if (!file_exists($oldViewersFile)) { return; } try { $oldData = json_decode(file_get_contents($oldViewersFile), true); if (is_array($oldData)) { $this->db->beginTransaction(); foreach ($oldData as $viewer) { if (!empty($viewer['user_id'])) { $this->heartbeat($viewer['user_id'], [ 'nickname' => $viewer['nickname'] ?? 'Anonymous', 'ip_address' => $viewer['ip_address'] ?? '', 'session_id' => $viewer['session_id'] ?? '', 'is_admin' => $viewer['is_admin'] ?? false ]); } } $this->db->commit(); // Backup old file and remove it rename($oldViewersFile, $oldViewersFile . '.migrated.' . date('Y-m-d_H-i-s')); error_log("Migrated " . count($oldData) . " viewers from file to database"); } } catch (Exception $e) { error_log("Migration from file failed: " . $e->getMessage()); } } }