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