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