809 lines
30 KiB
PHP
809 lines
30 KiB
PHP
<?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>
|