200120261529

This commit is contained in:
Manfred Aabye
2026-01-20 15:30:09 +01:00
committed by GitHub
parent 7f58dde3d5
commit 0f10a6459f
5 changed files with 641 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
<?php
// Tabellen und Spalten der Robust-Datenbank (aus robust.sql)
$ROBUST_TABLES = [
'AgentPrefs' => ['PrincipalID','AccessPrefs','HoverHeight','Language','LanguageIsPublic','PermEveryone','PermGroup','PermNextOwner'],
'assets' => ['name','description','assetType','local','temporary','data','id','create_time','access_time','asset_flags','CreatorID'],
'auth' => ['UUID','passwordHash','passwordSalt','webLoginKey','accountType'],
'Avatars' => ['PrincipalID','Name','Value'],
'balances' => ['user','balance','status','type'],
'classifieds' => ['classifieduuid','creatoruuid','creationdate','expirationdate','category','name','description','parceluuid','parentestate','snapshotuuid','simname','posglobal','parcelname','classifiedflags','priceforlisting'],
'Friends' => ['PrincipalID','Friend','Flags','Offered'],
'GridUser' => ['UserID','HomeRegionID','HomePosition','HomeLookAt','LastRegionID','LastPosition','LastLookAt','Online','Login','Logout'],
'hg_traveling_data' => ['SessionID','UserID','GridExternalName','ServiceToken','ClientIPAddress','MyIPAddress','TMStamp'],
'im_offline' => ['ID','PrincipalID','FromID','Message','TMStamp'],
'inventoryfolders' => ['folderName','type','version','folderID','agentID','parentFolderID'],
'inventoryitems' => ['assetID','assetType','inventoryName','inventoryDescription','inventoryNextPermissions','inventoryCurrentPermissions','invType','creatorID','inventoryBasePermissions','inventoryEveryOnePermissions','salePrice','saleType','creationDate','groupID','groupOwned','flags','inventoryID','avatarID','parentFolderID','inventoryGroupPermissions'],
'migrations' => ['name','version'],
'MuteList' => ['AgentID','MuteID','MuteName','MuteType','MuteFlags','Stamp'],
'os_groups_groups' => ['GroupID','Location','Name','Charter','InsigniaID','FounderID','MembershipFee','OpenEnrollment','ShowInList','AllowPublish','MaturePublish','OwnerRoleID'],
'os_groups_invites' => ['InviteID','GroupID','RoleID','PrincipalID','TMStamp'],
'os_groups_membership' => ['GroupID','PrincipalID','SelectedRoleID','Contribution','ListInProfile','AcceptNotices','AccessToken'],
'os_groups_notices' => ['GroupID','NoticeID','TMStamp','FromName','Subject','Message','HasAttachment','AttachmentType','AttachmentName','AttachmentItemID','AttachmentOwnerID'],
'os_groups_principals' => ['PrincipalID','ActiveGroupID'],
'os_groups_rolemembership' => ['GroupID','RoleID','PrincipalID'],
'os_groups_roles' => ['GroupID','RoleID','Name','Description','Title','Powers'],
'Presence' => ['UserID','RegionID','SessionID','SecureSessionID','LastSeen'],
'regions' => ['uuid','regionHandle','regionName','regionRecvKey','regionSendKey','regionSecret','regionDataURI','serverIP','serverPort','serverURI','locX','locY','locZ','eastOverrideHandle','westOverrideHandle','southOverrideHandle','northOverrideHandle','regionAssetURI','regionAssetRecvKey','regionAssetSendKey','regionUserURI','regionUserRecvKey','regionUserSendKey','regionMapTexture','serverHttpPort','serverRemotingPort','owner_uuid','originUUID','access','ScopeID','sizeX','sizeY','flags','last_seen','PrincipalID','Token','parcelMapTexture'],
'tokens' => ['UUID','token','validity'],
'totalsales' => ['UUID','user','objectUUID','type','TotalCount','TotalAmount','time'],
'transactions' => ['UUID','sender','receiver','amount','senderBalance','receiverBalance','objectUUID','objectName','regionHandle','regionUUID','type','time','secure','status','commonName','description'],
'UserAccounts' => ['PrincipalID','ScopeID','FirstName','LastName','Email','ServiceURLs','Created','UserLevel','UserFlags','UserTitle','active'],
'userdata' => ['UserId','TagId','DataKey','DataVal'],
'userinfo' => ['user','simip','avatar','pass','type','class','serverurl'],
'usernotes' => ['useruuid','targetuuid','notes'],
'userpicks' => ['pickuuid','creatoruuid','toppick','parceluuid','name','description','snapshotuuid','user','originalname','simname','posglobal','sortorder','enabled','gatekeeper'],
'userprofile' => ['useruuid','profilePartner','profileAllowPublish','profileMaturePublish','profileURL','profileWantToMask','profileWantToText','profileSkillsMask','profileSkillsText','profileLanguages','profileImage','profileAboutText','profileFirstImage','profileFirstText'],
'usersettings' => ['useruuid','imviaemail','visible','email'],
];
// Datenbank-Utility für alle Tabellen der Robust-Datenbank (OpenSim)
// Wiederverwendbar in anderen Skripten via require/include
class RobustDB {
private $pdo;
/**
* Erstelle RobustDB-Instanz.
* @param string|PDO $host Hostname oder PDO-Objekt
* @param string|null $dbname
* @param string|null $user
* @param string|null $pass
*/
public function __construct($host, $dbname = null, $user = null, $pass = null) {
if ($host instanceof PDO) {
$this->pdo = $host;
} else {
// Prüfe, ob datainc.php eine $pdo-Variable bereitstellt
if ($host === 'datainc') {
require_once __DIR__ . '/datainc.php';
if (isset($pdo) && $pdo instanceof PDO) {
$this->pdo = $pdo;
return;
} else {
throw new Exception('Keine gültige PDO-Verbindung in datainc.php gefunden.');
}
}
$dsn = "mysql:host=$host;dbname=$dbname;charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
$this->pdo = new PDO($dsn, $user, $pass, $options);
}
}
// Alle Tabellen auflisten
public function listTables() {
$stmt = $this->pdo->query("SHOW TABLES");
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
// Alle Einträge einer Tabelle lesen
public function getAll($table) {
$stmt = $this->pdo->query("SELECT * FROM `" . addslashes($table) . "`");
return $stmt->fetchAll();
}
// Einzelnen Eintrag lesen (nach Primärschlüssel)
public function getById($table, $idColumn, $idValue) {
$sql = "SELECT * FROM `" . addslashes($table) . "` WHERE `$idColumn` = :id LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->execute(['id' => $idValue]);
return $stmt->fetch();
}
// Eintrag einfügen
public function insert($table, $data) {
$columns = array_keys($data);
$placeholders = array_map(function($col) { return ":$col"; }, $columns);
$sql = "INSERT INTO `" . addslashes($table) . "` (" . implode(",", $columns) . ") VALUES (" . implode(",", $placeholders) . ")";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($data);
return $this->pdo->lastInsertId();
}
// Eintrag aktualisieren
public function update($table, $idColumn, $idValue, $data) {
$set = [];
foreach ($data as $col => $val) {
$set[] = "`$col` = :$col";
}
$sql = "UPDATE `" . addslashes($table) . "` SET " . implode(", ", $set) . " WHERE `$idColumn` = :id";
$data['id'] = $idValue;
$stmt = $this->pdo->prepare($sql);
return $stmt->execute($data);
}
// Eintrag löschen
public function delete($table, $idColumn, $idValue) {
$sql = "DELETE FROM `" . addslashes($table) . "` WHERE `$idColumn` = :id";
$stmt = $this->pdo->prepare($sql);
return $stmt->execute(['id' => $idValue]);
}
// Beliebige Abfrage (z.B. für komplexe Filter)
public function query($sql, $params = []) {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
}
// Beispiel für die Nutzung in anderen Skripten:
// require_once 'statistics/data.php';
// $db = new RobustDB($host, $dbname, $user, $pass);
// $result = $db->getAll('UserAccounts');
?>
+30
View File
@@ -0,0 +1,30 @@
<?php
// Sicherheitsheader setzen
header('X-Frame-Options: SAMEORIGIN');
header('X-Content-Type-Options: nosniff');
header('X-XSS-Protection: 1; mode=block');
if (session_status() === PHP_SESSION_NONE) {
session_start([
'cookie_httponly' => true,
'cookie_secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
'cookie_samesite' => 'Strict',
]);
}
// Datenbankverbindung für Statistiksoftware
$dsn = 'mysql:host=localhost;dbname=opensim_database;charset=utf8mb4';
$user = 'dein_db_user';
$pass = 'dein_db_password';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
// Im Produktivbetrieb keine Details ausgeben!
http_response_code(500);
exit('Interner Fehler.');
}
+35
View File
@@ -0,0 +1,35 @@
<?php
// datatest.php ist der Test für data.php und datainc.php
// Es soll aus jeder Tabelle der Datenbank der erste Eintrag gelesen werden, aber 'data' in der Tabelle 'assets' soll ausgeblendet werden.
require_once __DIR__ . '/data.php';
// RobustDB mit datainc.php-Verbindung nutzen
$db = new RobustDB('datainc');
global $ROBUST_TABLES;
foreach ($ROBUST_TABLES as $table => $columns) {
// Für assets: alle Spalten außer 'data' abfragen
if ($table === 'assets') {
$cols = array_filter($columns, function($c) { return $c !== 'data'; });
$sql = "SELECT " . implode(", ", $cols) . " FROM `$table` LIMIT 1";
$result = $db->query($sql);
echo "<h3>$table</h3>\n";
if ($result && count($result) > 0) {
$row = $result[0];
$row['[BLOB]'] = '[BLOB ausgeblendet]';
echo '<pre>' . htmlspecialchars(print_r($row, true)) . '</pre>';
} else {
echo '<em>Keine Einträge gefunden.</em>';
}
} else {
$result = $db->query("SELECT * FROM `$table` LIMIT 1");
echo "<h3>$table</h3>\n";
if ($result && count($result) > 0) {
echo '<pre>' . htmlspecialchars(print_r($result[0], true)) . '</pre>';
} else {
echo '<em>Keine Einträge gefunden.</em>';
}
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
// Sicherheitsheader setzen
header('X-Frame-Options: SAMEORIGIN');
header('X-Content-Type-Options: nosniff');
header('X-XSS-Protection: 1; mode=block');
if (session_status() === PHP_SESSION_NONE) {
session_start([
'cookie_httponly' => true,
'cookie_secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
'cookie_samesite' => 'Strict',
]);
}
// Datenbankverbindung für Statistiksoftware
$dsn = 'mysql:host=localhost;dbname=opensim_stats;charset=utf8mb4';
$user = 'dein_db_user';
$pass = 'dein_db_passwort';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
// Im Produktivbetrieb keine Details ausgeben!
http_response_code(500);
exit('Interner Fehler.');
}
+409
View File
@@ -0,0 +1,409 @@
<?php
include_once 'ssinc.php';
// SStats - Statistiksoftware für OpenSimulator-Server
// Zusätzliche Sicherheitsheader
header('X-Frame-Options: SAMEORIGIN');
header('X-Content-Type-Options: nosniff');
header('X-XSS-Protection: 1; mode=block');
if (session_status() === PHP_SESSION_NONE) {
session_start([
'cookie_httponly' => true,
'cookie_secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
'cookie_samesite' => 'Strict',
]);
}
// CSRF-Basisschutz für POST-Formulare (falls später genutzt)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
http_response_code(403);
exit('Ungültiges CSRF-Token.');
}
}
// Regionen laden
$sql_regions = 'SELECT uuid, regionName, locX, locY, sizeX, sizeY, serverIP, serverPort FROM regions ORDER BY regionName';
$regions = $pdo->query($sql_regions)->fetchAll(PDO::FETCH_ASSOC);
// Online-User (Presence, falls leer: keine online)
$sql_online = 'SELECT p.UserID, ua.FirstName, ua.LastName, p.RegionID, r.regionName, p.LastSeen
FROM Presence p
LEFT JOIN UserAccounts ua ON p.UserID = ua.PrincipalID
LEFT JOIN regions r ON p.RegionID = r.uuid
ORDER BY p.LastSeen DESC';
$online = $pdo->query($sql_online)->fetchAll(PDO::FETCH_ASSOC);
// GridUser mit Status (Online/Offline)
$sql_griduser = 'SELECT gu.UserID, gu.LastRegionID, gu.Login, gu.Logout, gu.Online, ua.FirstName, ua.LastName
FROM GridUser gu
LEFT JOIN UserAccounts ua ON gu.UserID = ua.PrincipalID';
$gridusers = $pdo->query($sql_griduser)->fetchAll(PDO::FETCH_ASSOC);
// MuteList laden
$sql_mute = 'SELECT m.AgentID, m.MuteID, m.MuteName, m.MuteType, ua.FirstName, ua.LastName FROM MuteList m LEFT JOIN UserAccounts ua ON m.MuteID = ua.PrincipalID';
$mutelist = $pdo->query($sql_mute)->fetchAll(PDO::FETCH_ASSOC);
// Gruppen laden
$sql_groups = 'SELECT * FROM os_groups_groups ORDER BY Name ASC, Charter ASC';
$groups = $pdo->query($sql_groups)->fetchAll(PDO::FETCH_ASSOC);
// Benutzerinformationen laden
$sql_userinfo = 'SELECT * FROM userinfo ORDER BY simip ASC, avatar ASC, serverurl DESC';
$userinfo = $pdo->query($sql_userinfo)->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html>
<head>
<title>OpenSimulator Statistik</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<style>
html,body,h1,h2,h3,h4,h5 {font-family: "Roboto", sans-serif}
.w3-table-all {font-size: 12px;}
.w3-container {margin-bottom: 32px;}
.w3-sidebar {z-index: 3;width:250px;top:0;left:0;}
.w3-bar-block .w3-bar-item {padding:16px}
.w3-main {margin-left:250px;}
th {cursor:pointer;}
th.sorted-asc:after {content: " \25B2";}
th.sorted-desc:after {content: " \25BC";}
@media (max-width:600px) {
.w3-sidebar {display:none;}
.w3-main {margin-left:0;}
}
</style>
</head>
<body class="w3-light-grey">
<!-- Sidebar -->
<nav class="w3-sidebar w3-bar-block w3-collapse w3-large w3-blue-grey w3-animate-left" id="mySidebar">
<a class="w3-bar-item w3-button w3-hover-teal w3-center w3-padding-32" href="#">
<i class="fa fa-bar-chart fa-3x w3-margin-bottom" aria-hidden="true"></i><br>
</a>
<a class="w3-bar-item w3-button w3-hover-teal" href="#regionen"><i class="fa fa-map fa-fw w3-margin-right"></i>Regionen</a>
<a class="w3-bar-item w3-button w3-hover-teal" href="#gruppen"><i class="fa fa-users fa-fw w3-margin-right"></i>Gruppen</a>
<a class="w3-bar-item w3-button w3-hover-teal" href="#online"><i class="fas fa-user-alt fa-fw w3-margin-right"></i>Online</a>
<a class="w3-bar-item w3-button w3-hover-teal" href="#userinfo"><i class="fas fa-user-friends fa-fw w3-margin-right"></i>Benutzerinfo</a>
<a class="w3-bar-item w3-button w3-hover-teal" href="#griduser"><i class="fa fa-address-book fa-fw w3-margin-right"></i>GridUser</a>
<a class="w3-bar-item w3-button w3-hover-teal" href="#mutelist"><i class="fa fa-volume-off fa-fw w3-margin-right"></i>MuteList</a>
</nav>
<!-- Topbar -->
<header class="w3-bar w3-top w3-blue-grey w3-large" style="z-index:4">
<button class="w3-bar-item w3-button w3-hide-large w3-hover-none w3-hover-text-light-grey" onclick="w3_open();"><i class="fa fa-bars"></i> Menü</button>
<span class="w3-bar-item w3-right">OpenSimulator Statistik Dashboard</span>
</header>
<div class="w3-main" style="margin-left:250px;margin-top:43px;">
<div class="w3-container w3-padding-16">
<h1 class="w3-xxlarge">OpenSimulator Region Statistik</h1>
<p>Live-Statistiken zu Regionen und Benutzern</p>
</div>
<!-- Regionen Übersicht -->
<div class="w3-container w3-white w3-card-4 w3-margin-bottom" id="regionen">
<h2 class="w3-text-grey w3-padding-16"><i class="fa fa-map fa-fw w3-margin-right w3-xxlarge w3-text-teal"></i>Regionen im Grid</h2>
<div class="w3-responsive">
<table class="w3-table-all w3-hoverable">
<thead>
<tr class="w3-teal">
<th>Name</th>
<th>UUID</th>
<th>Position</th>
<th>Größe</th>
<th>Server-IP</th>
<th>Port</th>
</tr>
</thead>
<tbody>
<?php if (empty($regions)): ?>
<tr><td colspan="6" class="w3-center">Zur Zeit sind keine Regionen im Grid.</td></tr>
<?php else: ?>
<?php foreach ($regions as $region): ?>
<tr>
<td><?= htmlspecialchars($region['regionName']) ?></td>
<td><?= htmlspecialchars($region['uuid']) ?></td>
<td><?= htmlspecialchars($region['locX']) ?>, <?= htmlspecialchars($region['locY']) ?></td>
<td><?= htmlspecialchars($region['sizeX']) ?> x <?= htmlspecialchars($region['sizeY']) ?></td>
<td><?= htmlspecialchars($region['serverIP']) ?></td>
<td><?= htmlspecialchars($region['serverPort']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Gruppen Übersicht -->
<div class="w3-container w3-white w3-card-4 w3-margin-bottom" id="gruppen">
<h2 class="w3-text-grey w3-padding-16"><i class="fas fa-users fa-fw w3-margin-right w3-xxlarge w3-text-teal"></i>Gruppen im Grid</h2>
<div class="w3-responsive">
<?php if (empty($groups)): ?>
<div class="w3-panel w3-pale-yellow w3-border w3-margin-bottom">Zur Zeit sind keine Gruppen im Grid.</div>
<?php else: ?>
<table class="w3-table-all w3-hoverable">
<thead>
<tr class="w3-teal">
<th>Name</th>
<th>Gruppenbeschreibung (Charter)</th>
<th>Gründer (FounderID)</th>
<th>Location</th>
<th>InsigniaID</th>
<th>Mitgliedsbeitrag</th>
<th>Offen</th>
</tr>
</thead>
<tbody>
<?php foreach ($groups as $group): ?>
<tr>
<td><?= htmlspecialchars($group['Name']) ?></td>
<td><?= htmlspecialchars($group['Charter']) ?></td>
<td><?= htmlspecialchars($group['FounderID']) ?></td>
<td><?= htmlspecialchars($group['Location']) ?></td>
<td><?= htmlspecialchars($group['InsigniaID']) ?></td>
<td><?= htmlspecialchars($group['MembershipFee']) ?></td>
<td><?= htmlspecialchars($group['OpenEnrollment']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<!-- Online-User Übersicht -->
<div class="w3-container w3-white w3-card-4 w3-margin-bottom" id="online">
<h2 class="w3-text-grey w3-padding-16"><i class="fas fa-user-alt fa-fw w3-margin-right w3-xxlarge w3-text-teal"></i>Online-Mitglieder</h2>
<div class="w3-responsive">
<?php if (empty($online)): ?>
<div class="w3-panel w3-pale-yellow w3-border">Zur Zeit befindet sich kein Mitglied im Grid.</div>
<?php else: ?>
<table class="w3-table-all w3-hoverable">
<thead>
<tr class="w3-teal">
<th>Name</th>
<th>UserID</th>
<th>Region</th>
<th>Region-Name</th>
<th>Letzte Aktivität</th>
</tr>
</thead>
<tbody>
<?php foreach ($online as $user): ?>
<tr>
<td>
<?php
$name = trim(($user['FirstName'] ?? '') . ' ' . ($user['LastName'] ?? ''));
if ($name === '' && !empty($user['UserID'])) {
// Suche in GridUser nach UserID
$gridName = '';
foreach ($gridusers as $gu) {
if (isset($gu['UserID'])) {
$parts = explode(';', $gu['UserID']);
if ($parts[0] === $user['UserID'] && isset($parts[2])) {
$gridName = $parts[2];
break;
}
}
}
echo htmlspecialchars($gridName !== '' ? $gridName : $user['UserID']);
} else {
echo htmlspecialchars($name);
}
?>
</td>
<td><?= htmlspecialchars($user['UserID']) ?></td>
<td><?= htmlspecialchars($user['RegionID']) ?></td>
<td><?= htmlspecialchars($user['regionName']) ?></td>
<td><?= htmlspecialchars($user['LastSeen']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<!-- Benutzerinformationen Übersicht -->
<div class="w3-container w3-white w3-card-4 w3-margin-bottom" id="userinfo">
<h2 class="w3-text-grey w3-padding-16"><i class="fas fa-user-friends fa-fw w3-margin-right w3-xxlarge w3-text-teal"></i>Benutzerinformationen</h2>
<div class="w3-responsive">
<?php if (empty($userinfo)): ?>
<div class="w3-panel w3-pale-yellow w3-border w3-margin-bottom">Zur Zeit sind keine Benutzerinformationen im Grid.</div>
<?php else: ?>
<table class="w3-table-all w3-hoverable">
<thead>
<tr class="w3-teal">
<th>Avatar</th>
<th>Server-URL</th>
</tr>
</thead>
<tbody>
<?php foreach ($userinfo as $info): ?>
<tr>
<td><?= htmlspecialchars($info['avatar']) ?></td>
<td><?= htmlspecialchars($info['serverurl']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<!-- GridUser Übersicht -->
<div class="w3-container w3-white w3-card-4 w3-margin-bottom" id="griduser">
<h2 class="w3-text-grey w3-padding-16"><i class="fa fa-address-book fa-fw w3-margin-right w3-xxlarge w3-text-teal"></i>Alle GridUser</h2>
<div class="w3-responsive">
<table class="w3-table-all w3-hoverable">
<thead>
<tr class="w3-teal">
<th>Status</th>
<th>Name</th>
<th>Benutzerkennung</th>
<th>Heimatadresse</th>
<th>Vollständiger Name</th>
<th>Letzte Region</th>
<th>Login</th>
<th>Logout</th>
</tr>
</thead>
<tbody>
<?php if (empty($gridusers)): ?>
<tr><td colspan="8" class="w3-center">Zur Zeit gibt es keine GridUser im Grid.</td></tr>
<?php else: ?>
<?php foreach ($gridusers as $user): ?>
<?php
$kennung = $heimat = $vollname = '';
if (!empty($user['UserID'])) {
$parts = explode(';', $user['UserID']);
$kennung = $parts[0] ?? '';
$heimat = $parts[1] ?? '';
$vollname = $parts[2] ?? '';
}
?>
<tr>
<td style="text-align:center;">
<?php
$isOnline = false;
$originalOnline = isset($user['Online']) ? $user['Online'] : '';
if (isset($user['Online'])) {
$val = strtolower(trim((string)$user['Online']));
$onlineValues = ['1', 'true', 'yes', 'y'];
$isOnline = in_array($val, $onlineValues, true);
// Auch numerisch prüfen (z.B. int 1)
if (!$isOnline && is_numeric($user['Online'])) {
$isOnline = ((int)$user['Online']) === 1;
}
}
?>
<?php if ($isOnline): ?>
<span title="Online (DB: <?=htmlspecialchars($originalOnline)?>)" style="color:green;font-weight:bold;">Online</span>
<?php else: ?>
<span title="Offline (DB: <?=htmlspecialchars($originalOnline)?>)" style="color:red;font-weight:bold;">Offline</span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars(($user['FirstName'] ?? '') . ' ' . ($user['LastName'] ?? '')) ?></td>
<td><?= htmlspecialchars($kennung) ?></td>
<td><?= htmlspecialchars($heimat) ?></td>
<td><?= htmlspecialchars($vollname) ?></td>
<td><?= htmlspecialchars($user['LastRegionID']) ?></td>
<td><?= htmlspecialchars($user['Login']) ?></td>
<td><?= htmlspecialchars($user['Logout']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- MuteList Übersicht -->
<div class="w3-container w3-white w3-card-4 w3-margin-bottom" id="mutelist">
<h2 class="w3-text-grey w3-padding-16"><i class="fa fa-volume-off fa-fw w3-margin-right w3-xxlarge w3-text-teal"></i>Stummgeschaltete Nutzer (MuteList)</h2>
<div class="w3-responsive">
<?php if (empty($mutelist)): ?>
<div class="w3-panel w3-pale-yellow w3-border w3-margin-bottom">Im Grid ist zur Zeit niemand stummgeschaltet.</div>
<?php else: ?>
<table class="w3-table-all w3-hoverable">
<thead>
<tr class="w3-teal">
<th>Stummschaltender (AgentID)</th>
<th>Stummgeschaltet (MuteID)</th>
<th>Name</th>
<th>MuteName</th>
<th>MuteType</th>
</tr>
</thead>
<tbody>
<?php foreach ($mutelist as $mute): ?>
<tr>
<td><?= htmlspecialchars($mute['AgentID']) ?></td>
<td><?= htmlspecialchars($mute['MuteID']) ?></td>
<td><?= htmlspecialchars(trim(($mute['FirstName'] ?? '') . ' ' . ($mute['LastName'] ?? ''))) ?></td>
<td><?= htmlspecialchars($mute['MuteName']) ?></td>
<td><?= htmlspecialchars($mute['MuteType']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
</div>
<footer class="w3-container w3-teal w3-center w3-margin-top">
<p>OpenSimulator Statistiksoftware &copy; 2026</p>
</footer>
</div>
<script>
function w3_open() {
document.getElementById("mySidebar").style.display = "block";
}
function w3_close() {
document.getElementById("mySidebar").style.display = "none";
}
// Tabellen sortierbar machen
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('table.w3-table-all').forEach(function(table) {
let headers = table.querySelectorAll('th');
headers.forEach(function(th, idx) {
th.addEventListener('click', function() {
let rows = Array.from(table.querySelectorAll('tbody > tr'));
let asc = !th.classList.contains('sorted-asc');
headers.forEach(h => h.classList.remove('sorted-asc', 'sorted-desc'));
th.classList.add(asc ? 'sorted-asc' : 'sorted-desc');
rows.sort(function(a, b) {
let va = a.children[idx].textContent.trim().toLowerCase();
let vb = b.children[idx].textContent.trim().toLowerCase();
// Versuche numerisch zu sortieren, falls möglich
let na = parseFloat(va.replace(/,/g, '.'));
let nb = parseFloat(vb.replace(/,/g, '.'));
if (!isNaN(na) && !isNaN(nb)) {
return asc ? na - nb : nb - na;
}
return asc ? va.localeCompare(vb) : vb.localeCompare(va);
});
let tbody = table.querySelector('tbody');
rows.forEach(row => tbody.appendChild(row));
});
});
});
});
</script>
</body>
</html>