Add Mumble/stats.php
This commit is contained in:
		
							parent
							
								
									8360e06473
								
							
						
					
					
						commit
						6faaeec92f
					
				
					 1 changed files with 809 additions and 0 deletions
				
			
		
							
								
								
									
										809
									
								
								Mumble/stats.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										809
									
								
								Mumble/stats.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,809 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					// Minimal working PHP logic for live Mumble stats, no error handling
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require_once "/usr/share/php/Ice.php";
 | 
				
			||||||
 | 
					if (!class_exists("Ice\\Value")) {
 | 
				
			||||||
 | 
					    eval("namespace Ice; class Value {}");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					if (!class_exists("Ice\\UserException")) {
 | 
				
			||||||
 | 
					    eval("namespace Ice; class UserException extends \\Exception {}");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					require_once __DIR__ . "/MumbleServer.php";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- CONFIGURATION ---
 | 
				
			||||||
 | 
					$iceConnectionString = "Meta:tcp -h 127.0.0.1 -p 6502"; // Change host/port as needed
 | 
				
			||||||
 | 
					$serverId = 1; // Change to your Mumble server ID
 | 
				
			||||||
 | 
					$iceSecret = "PASSWORD123"; // Your icesecretread value
 | 
				
			||||||
 | 
					$serverExpectedName = "Claytonia Gaming"; // Expected server name from configuration
 | 
				
			||||||
 | 
					$serverWebsite = "https://claytonia.net/"; // Server website
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// For displaying resolved IP
 | 
				
			||||||
 | 
					$displayHostname = "voice.claytonia.net";
 | 
				
			||||||
 | 
					$displayIp = gethostbyname($displayHostname);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- DATA FETCHING ---
 | 
				
			||||||
 | 
					$serverName = $serverVersion = $uptime = $maxUsers = $currentUsers = null;
 | 
				
			||||||
 | 
					$serverHostname = null;
 | 
				
			||||||
 | 
					$serverPort = 64738; // Default Mumble port
 | 
				
			||||||
 | 
					$channels = [];
 | 
				
			||||||
 | 
					$users = [];
 | 
				
			||||||
 | 
					$userStats = [];
 | 
				
			||||||
 | 
					$userStatsFile = __DIR__ . "/mumble_users.json"; // File to store user statistics
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Main block for Ice and server operations
 | 
				
			||||||
 | 
					$ICE = null;
 | 
				
			||||||
 | 
					$meta = null;
 | 
				
			||||||
 | 
					$serverHostname = null;
 | 
				
			||||||
 | 
					$users = [];
 | 
				
			||||||
 | 
					$channels = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try {
 | 
				
			||||||
 | 
					    $ICE = Ice\initialize();
 | 
				
			||||||
 | 
					    $proxy = $ICE->stringToProxy($iceConnectionString);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (class_exists("\\MumbleServer\\MetaPrxHelper")) {
 | 
				
			||||||
 | 
					        $meta = \MumbleServer\MetaPrxHelper::checkedCast($proxy);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $context = ["secret" => $iceSecret];
 | 
				
			||||||
 | 
					    $servers = $meta->getAllServers($context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Get server by ID or use first server in the list
 | 
				
			||||||
 | 
					    $server = null;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        $server = $meta->getServer($serverId, $context);
 | 
				
			||||||
 | 
					    } catch (Exception $e) {
 | 
				
			||||||
 | 
					        if (count($servers) > 0) {
 | 
				
			||||||
 | 
					            $server = $servers[0];
 | 
				
			||||||
 | 
					            $serverId = $server->id($context);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if ($server) {
 | 
				
			||||||
 | 
					        // Get server information
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $serverName = $server->getConf("registerName", $context);
 | 
				
			||||||
 | 
					            if (empty($serverName)) {
 | 
				
			||||||
 | 
					                $serverName =
 | 
				
			||||||
 | 
					                    $serverExpectedName ?: "Mumble Server #" . $serverId;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (Exception $e) {
 | 
				
			||||||
 | 
					            $serverName = $serverExpectedName ?: "Mumble Server #" . $serverId;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get server hostname
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $serverHostname = $server->getConf("registerHostname", $context);
 | 
				
			||||||
 | 
					        } catch (Exception $e) {
 | 
				
			||||||
 | 
					            $serverHostname = "voice.claytonia.net";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Try to get port if available (default to 64738)
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $confPort = $server->getConf("registerPort", $context);
 | 
				
			||||||
 | 
					            if (!empty($confPort) && is_numeric($confPort)) {
 | 
				
			||||||
 | 
					                $serverPort = (int) $confPort;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (Exception $e) {
 | 
				
			||||||
 | 
					            $serverPort = 64738;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get version - note: getVersion doesn't work in this implementation
 | 
				
			||||||
 | 
					        $serverVersion = "Mumble Server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get uptime
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $uptimeSeconds = $server->getUptime($context);
 | 
				
			||||||
 | 
					            $days = floor($uptimeSeconds / 86400);
 | 
				
			||||||
 | 
					            $hours = floor(($uptimeSeconds % 86400) / 3600);
 | 
				
			||||||
 | 
					            $minutes = floor(($uptimeSeconds % 3600) / 60);
 | 
				
			||||||
 | 
					            $uptime = "";
 | 
				
			||||||
 | 
					            if ($days > 0) {
 | 
				
			||||||
 | 
					                $uptime .= "$days days, ";
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if ($hours > 0 || $days > 0) {
 | 
				
			||||||
 | 
					                $uptime .= "$hours hours, ";
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            $uptime .= "$minutes minutes";
 | 
				
			||||||
 | 
					        } catch (Exception $e) {
 | 
				
			||||||
 | 
					            $uptime = "Unknown";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get configuration values and users
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $maxUsers = $server->getConf("users", $context);
 | 
				
			||||||
 | 
					        } catch (Exception $e) {
 | 
				
			||||||
 | 
					            $maxUsers = "?";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $rawUsers = $server->getUsers($context);
 | 
				
			||||||
 | 
					            $currentUsers = count($rawUsers);
 | 
				
			||||||
 | 
					        } catch (Exception $e) {
 | 
				
			||||||
 | 
					            $rawUsers = [];
 | 
				
			||||||
 | 
					            $currentUsers = 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get channels
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $rawChannels = $server->getChannels($context);
 | 
				
			||||||
 | 
					            foreach ($rawChannels as $id => $channel) {
 | 
				
			||||||
 | 
					                // Get users in this channel
 | 
				
			||||||
 | 
					                $channelUsers = [];
 | 
				
			||||||
 | 
					                foreach ($rawUsers as $uid => $user) {
 | 
				
			||||||
 | 
					                    $userChannel = null;
 | 
				
			||||||
 | 
					                    if (is_object($user) && isset($user->channel)) {
 | 
				
			||||||
 | 
					                        $userChannel = $user->channel;
 | 
				
			||||||
 | 
					                    } elseif (is_array($user) && isset($user["channel"])) {
 | 
				
			||||||
 | 
					                        $userChannel = $user["channel"];
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    if ($userChannel == $id) {
 | 
				
			||||||
 | 
					                        $channelUsers[] = $uid;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                $channelName = "";
 | 
				
			||||||
 | 
					                if (is_object($channel) && isset($channel->name)) {
 | 
				
			||||||
 | 
					                    $channelName = $channel->name;
 | 
				
			||||||
 | 
					                } elseif (is_array($channel) && isset($channel["name"])) {
 | 
				
			||||||
 | 
					                    $channelName = $channel["name"];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                $channels[] = [
 | 
				
			||||||
 | 
					                    "name" => $channelName,
 | 
				
			||||||
 | 
					                    "users" => count($channelUsers),
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (Exception $e) {
 | 
				
			||||||
 | 
					            $rawChannels = [];
 | 
				
			||||||
 | 
					            $channels = [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Process users and update stats
 | 
				
			||||||
 | 
					        if (isset($userStatsFile) && file_exists($userStatsFile)) {
 | 
				
			||||||
 | 
					            $userStats =
 | 
				
			||||||
 | 
					                json_decode(file_get_contents($userStatsFile), true) ?: [];
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            $userStats = [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $now = time();
 | 
				
			||||||
 | 
					        $today = date("Y-m-d");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($rawUsers as $id => $user) {
 | 
				
			||||||
 | 
					            $channelName = "Unknown";
 | 
				
			||||||
 | 
					            $userName = "";
 | 
				
			||||||
 | 
					            $userChannelId = null;
 | 
				
			||||||
 | 
					            $userSession = null;
 | 
				
			||||||
 | 
					            $userIdHash = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (is_object($user)) {
 | 
				
			||||||
 | 
					                $userName = isset($user->name) ? $user->name : "Unknown User";
 | 
				
			||||||
 | 
					                $userChannelId = isset($user->channel) ? $user->channel : null;
 | 
				
			||||||
 | 
					                $userSession = isset($user->session) ? $user->session : $id;
 | 
				
			||||||
 | 
					                $userIdHash = isset($user->hash) ? $user->hash : md5($userName);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                $userName = isset($user["name"])
 | 
				
			||||||
 | 
					                    ? $user["name"]
 | 
				
			||||||
 | 
					                    : "Unknown User";
 | 
				
			||||||
 | 
					                $userChannelId = isset($user["channel"])
 | 
				
			||||||
 | 
					                    ? $user["channel"]
 | 
				
			||||||
 | 
					                    : null;
 | 
				
			||||||
 | 
					                $userSession = isset($user["session"]) ? $user["session"] : $id;
 | 
				
			||||||
 | 
					                $userIdHash = isset($user["hash"])
 | 
				
			||||||
 | 
					                    ? $user["hash"]
 | 
				
			||||||
 | 
					                    : md5($userName);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                $userChannelId !== null &&
 | 
				
			||||||
 | 
					                isset($rawChannels[$userChannelId])
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                $channel = $rawChannels[$userChannelId];
 | 
				
			||||||
 | 
					                if (is_object($channel) && isset($channel->name)) {
 | 
				
			||||||
 | 
					                    $channelName = $channel->name;
 | 
				
			||||||
 | 
					                } elseif (is_array($channel) && isset($channel["name"])) {
 | 
				
			||||||
 | 
					                    $channelName = $channel["name"];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $users[] = [
 | 
				
			||||||
 | 
					                "name" => $userName,
 | 
				
			||||||
 | 
					                "channel" => $channelName,
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Update and save user stats
 | 
				
			||||||
 | 
					        if (isset($userStatsFile)) {
 | 
				
			||||||
 | 
					            foreach ($users as $user) {
 | 
				
			||||||
 | 
					                $userName = $user["name"];
 | 
				
			||||||
 | 
					                $userHash = md5($userName);
 | 
				
			||||||
 | 
					                $userChannel = $user["channel"] ?? "Unknown";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (!isset($userStats[$userHash])) {
 | 
				
			||||||
 | 
					                    $userStats[$userHash] = [
 | 
				
			||||||
 | 
					                        "name" => $userName,
 | 
				
			||||||
 | 
					                        "first_seen" => $now,
 | 
				
			||||||
 | 
					                        "last_seen" => $now,
 | 
				
			||||||
 | 
					                        "connect_count" => 1,
 | 
				
			||||||
 | 
					                        "days" => [
 | 
				
			||||||
 | 
					                            $today => 1,
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        // "channels" => [ $userChannel => 1 ],
 | 
				
			||||||
 | 
					                        "total_time" => 0,
 | 
				
			||||||
 | 
					                        // "sessions" => [[ "start" => $now, "end" => null, "channel" => $userChannel ]],
 | 
				
			||||||
 | 
					                    ];
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $userStats[$userHash]["last_seen"] = $now;
 | 
				
			||||||
 | 
					                $userStats[$userHash]["days"][$today] = 1;
 | 
				
			||||||
 | 
					                // Removed all channels/session management from stats page
 | 
				
			||||||
 | 
					                $hasActiveSession = false;
 | 
				
			||||||
 | 
					                // Removed all session/channel logic from stats page
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            // file_put_contents removed: stats page should never write to mumble_users.json
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					} catch (Exception $e) {
 | 
				
			||||||
 | 
					    // No error output
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					?>
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					    <head>
 | 
				
			||||||
 | 
					        <meta charset="UTF-8">
 | 
				
			||||||
 | 
					        <title><?= htmlspecialchars(
 | 
				
			||||||
 | 
					            $serverName ?: "Mumble Server"
 | 
				
			||||||
 | 
					        ) ?> - Mumble Stats</title>
 | 
				
			||||||
 | 
					        <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
				
			||||||
 | 
					        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <style>
 | 
				
			||||||
 | 
					            body {
 | 
				
			||||||
 | 
					                background-color: #121212;
 | 
				
			||||||
 | 
					                color: #ffffff;
 | 
				
			||||||
 | 
					                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 | 
				
			||||||
 | 
					                font-size: 16px;
 | 
				
			||||||
 | 
					                line-height: 1.6;
 | 
				
			||||||
 | 
					                letter-spacing: 0.01em;
 | 
				
			||||||
 | 
					                font-weight: 500;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /* User stats table custom styling */
 | 
				
			||||||
 | 
					            .user-stats-table {
 | 
				
			||||||
 | 
					                background-color: #232323 !important;
 | 
				
			||||||
 | 
					                color: #ffffff !important;
 | 
				
			||||||
 | 
					                border-radius: 8px;
 | 
				
			||||||
 | 
					                overflow: hidden;
 | 
				
			||||||
 | 
					                box-shadow: 0 2px 8px rgba(0,0,0,0.18);
 | 
				
			||||||
 | 
					                margin-bottom: 0;
 | 
				
			||||||
 | 
					                width: 100%;
 | 
				
			||||||
 | 
					                border-collapse: separate;
 | 
				
			||||||
 | 
					                border-spacing: 0;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            .user-stats-table th, .user-stats-table td {
 | 
				
			||||||
 | 
					                border: none !important;
 | 
				
			||||||
 | 
					                color: #ffffff !important;
 | 
				
			||||||
 | 
					                background: #232323 !important;
 | 
				
			||||||
 | 
					                vertical-align: middle;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            .user-stats-table th {
 | 
				
			||||||
 | 
					                background: #1e1e1e !important;
 | 
				
			||||||
 | 
					                color: #d9c6ff !important;
 | 
				
			||||||
 | 
					                font-weight: 600;
 | 
				
			||||||
 | 
					                text-shadow: 0 0 1px rgba(140,82,255,0.5);
 | 
				
			||||||
 | 
					                font-size: 1.05rem;
 | 
				
			||||||
 | 
					                border-bottom: 2px solid #2a2a2a !important;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            .user-stats-table tbody tr {
 | 
				
			||||||
 | 
					                background-color: #232323 !important;
 | 
				
			||||||
 | 
					                transition: background-color 0.2s;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            .user-stats-table tbody tr:hover {
 | 
				
			||||||
 | 
					                background-color: #2a2a2a !important;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            .user-stats-table td {
 | 
				
			||||||
 | 
					                font-size: 1.01rem;
 | 
				
			||||||
 | 
					                font-weight: 600;
 | 
				
			||||||
 | 
					                text-shadow: 0 0 1px rgba(255,255,255,0.3);
 | 
				
			||||||
 | 
					                padding: 0.75rem 1.25rem;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            .user-stats-table .badge {
 | 
				
			||||||
 | 
					                background-color: #8c52ff;
 | 
				
			||||||
 | 
					                font-weight: 500;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .container {
 | 
				
			||||||
 | 
					                max-width: 1200px;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .card {
 | 
				
			||||||
 | 
					                background-color: #1e1e1e;
 | 
				
			||||||
 | 
					                border: none;
 | 
				
			||||||
 | 
					                border-left: 3px solid #8c52ff;
 | 
				
			||||||
 | 
					                border-radius: 6px;
 | 
				
			||||||
 | 
					                box-shadow: 0 4px 8px rgba(0,0,0,0.2);
 | 
				
			||||||
 | 
					                transition: transform 0.2s, box-shadow 0.2s;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .card:hover {
 | 
				
			||||||
 | 
					                transform: translateY(-5px);
 | 
				
			||||||
 | 
					                box-shadow: 0 8px 16px rgba(0,0,0,0.3);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            h1, h4, h5 {
 | 
				
			||||||
 | 
					                color: #ffffff;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            h1 .text-info {
 | 
				
			||||||
 | 
					                color: #8c52ff !important;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .card h5 {
 | 
				
			||||||
 | 
					                margin-bottom: 15px;
 | 
				
			||||||
 | 
					                font-weight: 600;
 | 
				
			||||||
 | 
					                color: #d9c6ff;
 | 
				
			||||||
 | 
					                text-shadow: 0 0 1px rgba(140,82,255,0.5);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .list-group-item {
 | 
				
			||||||
 | 
					                background-color: #2a2a2a;
 | 
				
			||||||
 | 
					                color: #ffffff;
 | 
				
			||||||
 | 
					                border-color: #333;
 | 
				
			||||||
 | 
					                transition: background-color 0.2s;
 | 
				
			||||||
 | 
					                font-size: 1.05rem;
 | 
				
			||||||
 | 
					                padding: 0.75rem 1.25rem;
 | 
				
			||||||
 | 
					                font-weight: 600;
 | 
				
			||||||
 | 
					                text-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .list-group-item:hover {
 | 
				
			||||||
 | 
					                background-color: #333;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .badge {
 | 
				
			||||||
 | 
					                background-color: #8c52ff;
 | 
				
			||||||
 | 
					                font-weight: 500;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .text-info {
 | 
				
			||||||
 | 
					                color: #ffffff !important;
 | 
				
			||||||
 | 
					                font-weight: 600;
 | 
				
			||||||
 | 
					                text-shadow: 0 0 1px rgba(255,255,255,0.5);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .btn-primary {
 | 
				
			||||||
 | 
					                background-color: #8c52ff;
 | 
				
			||||||
 | 
					                border-color: #8c52ff;
 | 
				
			||||||
 | 
					                transition: background-color 0.3s;
 | 
				
			||||||
 | 
					                font-weight: 600;
 | 
				
			||||||
 | 
					                color: #ffffff;
 | 
				
			||||||
 | 
					                text-shadow: 0 1px 2px rgba(0,0,0,0.3);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .btn-primary:hover {
 | 
				
			||||||
 | 
					                background-color: #7140d1;
 | 
				
			||||||
 | 
					                border-color: #7140d1;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .alert-danger {
 | 
				
			||||||
 | 
					                background-color: #3d1a1a;
 | 
				
			||||||
 | 
					                color: #ff9e9e;
 | 
				
			||||||
 | 
					                border-color: #5c2626;
 | 
				
			||||||
 | 
					                font-weight: 600;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /* Add animation */
 | 
				
			||||||
 | 
					            @keyframes fadeIn {
 | 
				
			||||||
 | 
					                from { opacity: 0; transform: translateY(15px); }
 | 
				
			||||||
 | 
					                to { opacity: 1; transform: translateY(0); }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .row > div {
 | 
				
			||||||
 | 
					                animation: fadeIn 0.5s ease-out forwards;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .row > div:nth-child(2) {
 | 
				
			||||||
 | 
					                animation-delay: 0.1s;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .row > div:nth-child(3) {
 | 
				
			||||||
 | 
					                animation-delay: 0.2s;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .row > div:nth-child(4) {
 | 
				
			||||||
 | 
					                animation-delay: 0.3s;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /* Channel and user list styling improvements */
 | 
				
			||||||
 | 
					               .list-group {
 | 
				
			||||||
 | 
					                   border-radius: 6px;
 | 
				
			||||||
 | 
					                   overflow: hidden;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               .list-group-item:first-child {
 | 
				
			||||||
 | 
					                   border-top-left-radius: 6px;
 | 
				
			||||||
 | 
					                   border-top-right-radius: 6px;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               .list-group-item:last-child {
 | 
				
			||||||
 | 
					                   border-bottom-left-radius: 6px;
 | 
				
			||||||
 | 
					                   border-bottom-right-radius: 6px;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					               /* Connection details section */
 | 
				
			||||||
 | 
					               .connection-details {
 | 
				
			||||||
 | 
					                   margin-top: 2rem;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               .connection-details .card {
 | 
				
			||||||
 | 
					                   border-left-color: #3498db;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               .connection-details h4 {
 | 
				
			||||||
 | 
					                   color: #3498db;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               /* Stats boxes */
 | 
				
			||||||
 | 
					               .stats-box {
 | 
				
			||||||
 | 
					                   text-align: center;
 | 
				
			||||||
 | 
					                   padding: 1.5rem 1rem;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               .stats-box p {
 | 
				
			||||||
 | 
					                   font-size: 1.5rem;
 | 
				
			||||||
 | 
					                   font-weight: 700;
 | 
				
			||||||
 | 
					                   color: #ffffff;
 | 
				
			||||||
 | 
					                   text-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               /* Server details */
 | 
				
			||||||
 | 
					               .server-name {
 | 
				
			||||||
 | 
					                   display: inline-block;
 | 
				
			||||||
 | 
					                   background: linear-gradient(45deg, #8c52ff, #5c9ce6);
 | 
				
			||||||
 | 
					                   -webkit-background-clip: text;
 | 
				
			||||||
 | 
					                   background-clip: text;
 | 
				
			||||||
 | 
					                   color: transparent;
 | 
				
			||||||
 | 
					                   font-weight: 700;
 | 
				
			||||||
 | 
					                   text-transform: uppercase;
 | 
				
			||||||
 | 
					                   letter-spacing: 0.05em;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               /* Empty state styling */
 | 
				
			||||||
 | 
					               .empty-state {
 | 
				
			||||||
 | 
					                   text-align: center;
 | 
				
			||||||
 | 
					                   padding: 2rem;
 | 
				
			||||||
 | 
					                   color: #ffffff;
 | 
				
			||||||
 | 
					                   font-style: italic;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               /* Footer */
 | 
				
			||||||
 | 
					               .footer {
 | 
				
			||||||
 | 
					                   margin-top: 2rem;
 | 
				
			||||||
 | 
					                   padding-top: 1rem;
 | 
				
			||||||
 | 
					                   border-top: 1px solid #333;
 | 
				
			||||||
 | 
					                   font-size: 0.9rem;
 | 
				
			||||||
 | 
					                   color: #888;
 | 
				
			||||||
 | 
					                   text-align: center;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               /* Dark scrollbar */
 | 
				
			||||||
 | 
					               ::-webkit-scrollbar {
 | 
				
			||||||
 | 
					                   width: 8px;
 | 
				
			||||||
 | 
					                   height: 8px;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               ::-webkit-scrollbar-track {
 | 
				
			||||||
 | 
					                   background: #1a1a1a;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               ::-webkit-scrollbar-thumb {
 | 
				
			||||||
 | 
					                   background: #3a3a3a;
 | 
				
			||||||
 | 
					                   border-radius: 4px;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               ::-webkit-scrollbar-thumb:hover {
 | 
				
			||||||
 | 
					                   background: #4a4a4a;
 | 
				
			||||||
 | 
					               }
 | 
				
			||||||
 | 
					           </style>
 | 
				
			||||||
 | 
					    </head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					<div class="container py-5">
 | 
				
			||||||
 | 
					   <h1 class="mb-4"><?= htmlspecialchars(
 | 
				
			||||||
 | 
					       isset($serverName) ? $serverName : "Mumble Server"
 | 
				
			||||||
 | 
					   ) ?> <small class="text-info">Stats</small></h1>
 | 
				
			||||||
 | 
					   <div class="row mb-4">
 | 
				
			||||||
 | 
					       <div class="col-md-3">
 | 
				
			||||||
 | 
					           <div class="card p-3 mb-3">
 | 
				
			||||||
 | 
					               <h5>Server Info</h5>
 | 
				
			||||||
 | 
					               <p style="color: #ffffff; font-size: 1.1rem; font-weight: 600; text-shadow: 0 0 1px rgba(255,255,255,0.8);"><?= htmlspecialchars(
 | 
				
			||||||
 | 
					                   isset($serverVersion) ? $serverVersion : "Unknown"
 | 
				
			||||||
 | 
					               ) ?><br>
 | 
				
			||||||
 | 
					               <?php if (!empty($serverWebsite)): ?>
 | 
				
			||||||
 | 
					               <a href="<?= htmlspecialchars(
 | 
				
			||||||
 | 
					                   $serverWebsite
 | 
				
			||||||
 | 
					               ) ?>" target="_blank">Visit Website</a>
 | 
				
			||||||
 | 
					               <?php endif; ?>
 | 
				
			||||||
 | 
					               </p>
 | 
				
			||||||
 | 
					           </div>
 | 
				
			||||||
 | 
					       </div>
 | 
				
			||||||
 | 
					       <div class="col-md-3">
 | 
				
			||||||
 | 
					           <div class="card p-3 mb-3">
 | 
				
			||||||
 | 
					               <h5>Uptime</h5>
 | 
				
			||||||
 | 
					               <p style="color: #ffffff; font-size: 1.1rem; font-weight: 600; text-shadow: 0 0 1px rgba(255,255,255,0.8);"><?= htmlspecialchars(
 | 
				
			||||||
 | 
					                   isset($uptime) ? $uptime : "Unknown"
 | 
				
			||||||
 | 
					               ) ?></p>
 | 
				
			||||||
 | 
					           </div>
 | 
				
			||||||
 | 
					       </div>
 | 
				
			||||||
 | 
					       <div class="col-md-3">
 | 
				
			||||||
 | 
					           <div class="card p-3 mb-3">
 | 
				
			||||||
 | 
					               <h5>Users</h5>
 | 
				
			||||||
 | 
					               <p style="color: #ffffff; font-size: 1.1rem; font-weight: 600; text-shadow: 0 0 1px rgba(255,255,255,0.8);"><?= htmlspecialchars(
 | 
				
			||||||
 | 
					                   isset($currentUsers) ? $currentUsers : "0"
 | 
				
			||||||
 | 
					               ) . " / ∞" ?></p>
 | 
				
			||||||
 | 
					           </div>
 | 
				
			||||||
 | 
					       </div>
 | 
				
			||||||
 | 
					       <div class="col-md-3">
 | 
				
			||||||
 | 
					           <div class="card p-3 mb-3">
 | 
				
			||||||
 | 
					               <h5>Channels</h5>
 | 
				
			||||||
 | 
					               <p style="color: #ffffff; font-size: 1.1rem; font-weight: 600; text-shadow: 0 0 1px rgba(255,255,255,0.8);"><?= htmlspecialchars(
 | 
				
			||||||
 | 
					                   (string) (isset($channels) && is_array($channels)
 | 
				
			||||||
 | 
					                       ? count($channels)
 | 
				
			||||||
 | 
					                       : 0)
 | 
				
			||||||
 | 
					               ) ?></p>
 | 
				
			||||||
 | 
					           </div>
 | 
				
			||||||
 | 
					       </div>
 | 
				
			||||||
 | 
					   </div>
 | 
				
			||||||
 | 
					   <div class="row">
 | 
				
			||||||
 | 
					       <div class="col-12 mb-4">
 | 
				
			||||||
 | 
					           <div class="card p-3">
 | 
				
			||||||
 | 
					               <h5>Connection Details</h5>
 | 
				
			||||||
 | 
					               <div style="margin-bottom: 15px;">
 | 
				
			||||||
 | 
					                   <p style="color: #ffffff; font-size: 1.1rem; font-weight: 600; text-shadow: 0 0 1px rgba(255,255,255,0.8); margin-bottom: 10px;">
 | 
				
			||||||
 | 
					                       <strong style="color: #d9c6ff; display: inline-block; width: 80px;">Server:</strong>
 | 
				
			||||||
 | 
					                       <span style="color: #ffffff; letter-spacing: 0.03em; font-weight: 700; text-shadow: 0 0 1px rgba(255,255,255,0.8);"><?= htmlspecialchars(
 | 
				
			||||||
 | 
					                           $displayHostname
 | 
				
			||||||
 | 
					                       ) ?></span>
 | 
				
			||||||
 | 
					                   </p>
 | 
				
			||||||
 | 
					                   <p style="color: #ffffff; font-size: 1.1rem; font-weight: 600; text-shadow: 0 0 1px rgba(255,255,255,0.8); margin-bottom: 15px;">
 | 
				
			||||||
 | 
					                       <strong style="color: #d9c6ff; display: inline-block; width: 80px;">Port:</strong>
 | 
				
			||||||
 | 
					                       <span style="color: #ffffff; letter-spacing: 0.03em; font-weight: 700; text-shadow: 0 0 1px rgba(255,255,255,0.8);"><?= htmlspecialchars(
 | 
				
			||||||
 | 
					                           $serverPort
 | 
				
			||||||
 | 
					                       ) ?></span>
 | 
				
			||||||
 | 
					                   </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					               </div>
 | 
				
			||||||
 | 
					               <p class="text-center">
 | 
				
			||||||
 | 
					                   <a href="mumble://<?= htmlspecialchars(
 | 
				
			||||||
 | 
					                       $displayHostname
 | 
				
			||||||
 | 
					                   ) ?>:<?= htmlspecialchars(
 | 
				
			||||||
 | 
					    $serverPort
 | 
				
			||||||
 | 
					) ?>" class="btn btn-primary" style="font-size: 1.1rem; padding: 0.7rem 2rem; font-weight: 700; letter-spacing: 0.05em; box-shadow: 0 4px 15px rgba(140, 82, 255, 0.3);">
 | 
				
			||||||
 | 
					                       Connect to Claytonia Mumble
 | 
				
			||||||
 | 
					                   </a>
 | 
				
			||||||
 | 
					               </p>
 | 
				
			||||||
 | 
					           </div>
 | 
				
			||||||
 | 
					       </div>
 | 
				
			||||||
 | 
					   </div>
 | 
				
			||||||
 | 
					   <div class="row">
 | 
				
			||||||
 | 
					       <div class="col-md-6">
 | 
				
			||||||
 | 
					           <div class="card p-3 mb-3">
 | 
				
			||||||
 | 
					               <h4>Channels</h4>
 | 
				
			||||||
 | 
					               <ul class="list-group">
 | 
				
			||||||
 | 
					                   <?php if (empty($channels)): ?>
 | 
				
			||||||
 | 
					                       <li class="list-group-item">No channels available</li>
 | 
				
			||||||
 | 
					                   <?php else: ?>
 | 
				
			||||||
 | 
					                   <?php foreach ((array) $channels as $ch): ?>
 | 
				
			||||||
 | 
					                       <li class="list-group-item d-flex justify-content-between align-items-center">
 | 
				
			||||||
 | 
					                           <span><?= htmlspecialchars($ch["name"]) ?></span>
 | 
				
			||||||
 | 
					                           <span class="badge bg-info"><?= $ch[
 | 
				
			||||||
 | 
					                               "users"
 | 
				
			||||||
 | 
					                           ] ?> users</span>
 | 
				
			||||||
 | 
					                       </li>
 | 
				
			||||||
 | 
					                   <?php endforeach; ?>
 | 
				
			||||||
 | 
					                   <?php endif; ?>
 | 
				
			||||||
 | 
					               </ul>
 | 
				
			||||||
 | 
					           </div>
 | 
				
			||||||
 | 
					       </div>
 | 
				
			||||||
 | 
					       <div class="col-md-6">
 | 
				
			||||||
 | 
					           <div class="card p-3 mb-3">
 | 
				
			||||||
 | 
					               <h4>Online Users</h4>
 | 
				
			||||||
 | 
					               <ul class="list-group">
 | 
				
			||||||
 | 
					                   <?php if (empty($users)): ?>
 | 
				
			||||||
 | 
					                       <li class="list-group-item">No users online</li>
 | 
				
			||||||
 | 
					                   <?php else: ?>
 | 
				
			||||||
 | 
					                   <?php foreach ((array) $users as $user): ?>
 | 
				
			||||||
 | 
					                       <li class="list-group-item d-flex justify-content-between align-items-center">
 | 
				
			||||||
 | 
					                           <span><?= htmlspecialchars($user["name"]) ?></span>
 | 
				
			||||||
 | 
					                           <span class="text-info"><?= htmlspecialchars(
 | 
				
			||||||
 | 
					                               $user["channel"]
 | 
				
			||||||
 | 
					                           ) ?></span>
 | 
				
			||||||
 | 
					                       </li>
 | 
				
			||||||
 | 
					                   <?php endforeach; ?>
 | 
				
			||||||
 | 
					                   <?php endif; ?>
 | 
				
			||||||
 | 
					               </ul>
 | 
				
			||||||
 | 
					           </div>
 | 
				
			||||||
 | 
					       </div>
 | 
				
			||||||
 | 
					   </div>
 | 
				
			||||||
 | 
					   <!-- User Statistics -->
 | 
				
			||||||
 | 
					   <div class="row">
 | 
				
			||||||
 | 
					       <div class="col-12 mb-4">
 | 
				
			||||||
 | 
					           <div class="card p-3">
 | 
				
			||||||
 | 
					               <h5>User Statistics <small style="font-size: 0.7rem; color: #a175ff; font-weight: normal;"><?= file_exists(
 | 
				
			||||||
 | 
					                   __DIR__ . "/mumble_users.json"
 | 
				
			||||||
 | 
					               )
 | 
				
			||||||
 | 
					                   ? "Updated " .
 | 
				
			||||||
 | 
					                       date(
 | 
				
			||||||
 | 
					                           "Y-m-d H:i:s",
 | 
				
			||||||
 | 
					                           filemtime(__DIR__ . "/mumble_users.json")
 | 
				
			||||||
 | 
					                       )
 | 
				
			||||||
 | 
					                   : "No stats file" ?></small></h5>
 | 
				
			||||||
 | 
					               <div class="table-responsive">
 | 
				
			||||||
 | 
					                   <table class="user-stats-table mb-0">
 | 
				
			||||||
 | 
					                       <thead>
 | 
				
			||||||
 | 
					                           <tr>
 | 
				
			||||||
 | 
					                               <th>User</th>
 | 
				
			||||||
 | 
					                               <th>First Seen</th>
 | 
				
			||||||
 | 
					                               <th>Last Seen</th>
 | 
				
			||||||
 | 
					                               <th>Connections</th>
 | 
				
			||||||
 | 
					                               <th>Days Active</th>
 | 
				
			||||||
 | 
					                               <th>Total Time</th>
 | 
				
			||||||
 | 
					                           </tr>
 | 
				
			||||||
 | 
					                       </thead>
 | 
				
			||||||
 | 
					                       <tbody>
 | 
				
			||||||
 | 
					                           <?php if (file_exists($userStatsFile)) {
 | 
				
			||||||
 | 
					                               $displayStats = json_decode(
 | 
				
			||||||
 | 
					                                   file_get_contents($userStatsFile),
 | 
				
			||||||
 | 
					                                   true
 | 
				
			||||||
 | 
					                               );
 | 
				
			||||||
 | 
					                               if (!empty($displayStats)):
 | 
				
			||||||
 | 
					                                   uasort($displayStats, function ($a, $b) {
 | 
				
			||||||
 | 
					                                       return $b["last_seen"] - $a["last_seen"];
 | 
				
			||||||
 | 
					                                   });
 | 
				
			||||||
 | 
					                                   foreach (
 | 
				
			||||||
 | 
					                                       $displayStats
 | 
				
			||||||
 | 
					                                       as $userHash => $stat
 | 
				
			||||||
 | 
					                                   ) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                       $dayCount = isset($stat["days"])
 | 
				
			||||||
 | 
					                                           ? count($stat["days"])
 | 
				
			||||||
 | 
					                                           : 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                       // Display total_time from JSON directly
 | 
				
			||||||
 | 
					                                       $totalSeconds = isset(
 | 
				
			||||||
 | 
					                                           $stat["total_time"]
 | 
				
			||||||
 | 
					                                       )
 | 
				
			||||||
 | 
					                                           ? $stat["total_time"]
 | 
				
			||||||
 | 
					                                           : 0;
 | 
				
			||||||
 | 
					                                       $totalHours = round(
 | 
				
			||||||
 | 
					                                           $totalSeconds / 3600,
 | 
				
			||||||
 | 
					                                           1
 | 
				
			||||||
 | 
					                                       );
 | 
				
			||||||
 | 
					                                       $totalTime = $totalHours . " hours";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                       $isOnline = false;
 | 
				
			||||||
 | 
					                                       if (
 | 
				
			||||||
 | 
					                                           isset($stat["sessions"]) &&
 | 
				
			||||||
 | 
					                                           !empty($stat["sessions"])
 | 
				
			||||||
 | 
					                                       ) {
 | 
				
			||||||
 | 
					                                           $lastSession = end(
 | 
				
			||||||
 | 
					                                               $stat["sessions"]
 | 
				
			||||||
 | 
					                                           );
 | 
				
			||||||
 | 
					                                           if (
 | 
				
			||||||
 | 
					                                               $lastSession &&
 | 
				
			||||||
 | 
					                                               isset($lastSession["end"]) &&
 | 
				
			||||||
 | 
					                                               $lastSession["end"] === null
 | 
				
			||||||
 | 
					                                           ) {
 | 
				
			||||||
 | 
					                                               $isOnline = true;
 | 
				
			||||||
 | 
					                                           }
 | 
				
			||||||
 | 
					                                       }
 | 
				
			||||||
 | 
					                                       $rowStyle = $isOnline
 | 
				
			||||||
 | 
					                                           ? "background-color: rgba(140, 82, 255, 0.15);"
 | 
				
			||||||
 | 
					                                           : "";
 | 
				
			||||||
 | 
					                                       $nameStyle = $isOnline
 | 
				
			||||||
 | 
					                                           ? "color: #ffffff; font-weight: 700;"
 | 
				
			||||||
 | 
					                                           : "color: #ffffff; font-weight: 600;";
 | 
				
			||||||
 | 
					                                       ?>
 | 
				
			||||||
 | 
					                           <tr style="<?= $rowStyle ?>">
 | 
				
			||||||
 | 
					                               <td style="<?= $nameStyle ?>">
 | 
				
			||||||
 | 
					                                   <?= htmlspecialchars($stat["name"]) ?>
 | 
				
			||||||
 | 
					                                   <?php if ($isOnline): ?>
 | 
				
			||||||
 | 
					                                   <span class="badge" style="background-color: #28a745; font-size: 0.7rem; vertical-align: middle; margin-left: 5px;">Online</span>
 | 
				
			||||||
 | 
					                                   <?php if (
 | 
				
			||||||
 | 
					                                       isset($lastSession) &&
 | 
				
			||||||
 | 
					                                       isset($lastSession["channel"]) &&
 | 
				
			||||||
 | 
					                                       !empty($lastSession["channel"])
 | 
				
			||||||
 | 
					                                   ): ?>
 | 
				
			||||||
 | 
					                                   <!-- Removed channel badge, as requested -->
 | 
				
			||||||
 | 
					                                   <?php endif; ?>
 | 
				
			||||||
 | 
					                                   <?php endif; ?>
 | 
				
			||||||
 | 
					                               </td>
 | 
				
			||||||
 | 
					                               <td><?= date(
 | 
				
			||||||
 | 
					                                   "Y-m-d",
 | 
				
			||||||
 | 
					                                   $stat["first_seen"]
 | 
				
			||||||
 | 
					                               ) ?></td>
 | 
				
			||||||
 | 
					                               <td><?= date("Y-m-d", $stat["last_seen"]) ?></td>
 | 
				
			||||||
 | 
					                               <td><?= $stat["connect_count"] ?></td>
 | 
				
			||||||
 | 
					                               <td><?= $dayCount ?></td>
 | 
				
			||||||
 | 
					                               <td><?= $totalTime ?></td>
 | 
				
			||||||
 | 
					                           </tr>
 | 
				
			||||||
 | 
					                           <?php
 | 
				
			||||||
 | 
					                                   }
 | 
				
			||||||
 | 
					                               else:
 | 
				
			||||||
 | 
					                                    ?>
 | 
				
			||||||
 | 
					                           <tr>
 | 
				
			||||||
 | 
					                               <td colspan="6" class="empty-state">No user statistics available yet</td>
 | 
				
			||||||
 | 
					                           </tr>
 | 
				
			||||||
 | 
					                           <?php
 | 
				
			||||||
 | 
					                               endif;
 | 
				
			||||||
 | 
					                           } else {
 | 
				
			||||||
 | 
					                                ?>
 | 
				
			||||||
 | 
					                           <tr>
 | 
				
			||||||
 | 
					                               <td colspan="6" class="empty-state">Statistics file not found</td>
 | 
				
			||||||
 | 
					                           </tr>
 | 
				
			||||||
 | 
					                           <?php
 | 
				
			||||||
 | 
					                           } ?>
 | 
				
			||||||
 | 
					                       </tbody>
 | 
				
			||||||
 | 
					                   </table>
 | 
				
			||||||
 | 
					               </div>
 | 
				
			||||||
 | 
					           </div>
 | 
				
			||||||
 | 
					       </div>
 | 
				
			||||||
 | 
					   </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					document.addEventListener('DOMContentLoaded', function () {
 | 
				
			||||||
 | 
					    document.querySelectorAll('.user-stats-table').forEach(function (table) {
 | 
				
			||||||
 | 
					        table.querySelectorAll('th').forEach(function (header, columnIndex) {
 | 
				
			||||||
 | 
					            header.style.cursor = 'pointer';
 | 
				
			||||||
 | 
					            header.addEventListener('click', function () {
 | 
				
			||||||
 | 
					                const tbody = table.querySelector('tbody');
 | 
				
			||||||
 | 
					                const rows = Array.from(tbody.querySelectorAll('tr'));
 | 
				
			||||||
 | 
					                const isAsc = header.classList.toggle('asc');
 | 
				
			||||||
 | 
					                header.classList.toggle('desc', !isAsc);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Remove sort classes from other headers
 | 
				
			||||||
 | 
					                table.querySelectorAll('th').forEach(function (th, i) {
 | 
				
			||||||
 | 
					                    if (i !== columnIndex) th.classList.remove('asc', 'desc');
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                rows.sort(function (a, b) {
 | 
				
			||||||
 | 
					                    let cellA = a.children[columnIndex].textContent.trim();
 | 
				
			||||||
 | 
					                    let cellB = b.children[columnIndex].textContent.trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Handle date columns (YYYY-MM-DD)
 | 
				
			||||||
 | 
					                    const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
 | 
				
			||||||
 | 
					                    if (dateRegex.test(cellA) && dateRegex.test(cellB)) {
 | 
				
			||||||
 | 
					                        const timeA = new Date(cellA).getTime();
 | 
				
			||||||
 | 
					                        const timeB = new Date(cellB).getTime();
 | 
				
			||||||
 | 
					                        return isAsc ? timeA - timeB : timeB - timeA;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Handle time columns like "X hours"
 | 
				
			||||||
 | 
					                    const hoursRegex = /^(\d+(\.\d+)?)\s*hours?$/i;
 | 
				
			||||||
 | 
					                    const matchA = cellA.match(hoursRegex);
 | 
				
			||||||
 | 
					                    const matchB = cellB.match(hoursRegex);
 | 
				
			||||||
 | 
					                    if (matchA && matchB) {
 | 
				
			||||||
 | 
					                        const numA = parseFloat(matchA[1]);
 | 
				
			||||||
 | 
					                        const numB = parseFloat(matchB[1]);
 | 
				
			||||||
 | 
					                        return isAsc ? numA - numB : numB - numA;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Try to compare as numbers, fallback to string
 | 
				
			||||||
 | 
					                    const numA = parseFloat(cellA.replace(/[^0-9.\-]/g, ''));
 | 
				
			||||||
 | 
					                    const numB = parseFloat(cellB.replace(/[^0-9.\-]/g, ''));
 | 
				
			||||||
 | 
					                    if (!isNaN(numA) && !isNaN(numB) && cellA !== "" && cellB !== "") {
 | 
				
			||||||
 | 
					                        return isAsc ? numA - numB : numB - numA;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    return isAsc
 | 
				
			||||||
 | 
					                        ? cellA.localeCompare(cellB)
 | 
				
			||||||
 | 
					                        : cellB.localeCompare(cellA);
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                rows.forEach(function (row) {
 | 
				
			||||||
 | 
					                    tbody.appendChild(row);
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue