Publish Void Bastion website

This commit is contained in:
Dominic Springer committed 2026-10-04 21:27:04 +02:00
commit 8474894756
15 files changed
+570

No files matched your search

+137
View File
@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
header('X-Robots-Tag: noindex, nofollow, noarchive', true);
header('Cache-Control: no-store, private', true);
header('Referrer-Policy: no-referrer', true);
header('X-Content-Type-Options: nosniff', true);
session_name('void_bastion_internal_stats');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/intern/statistik/',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict',
]);
session_start();
$configPath = __DIR__ . '/stats-config.local.php';
if (!is_file($configPath)) {
http_response_code(503);
exit('Die interne Statistik ist noch nicht konfiguriert.');
}
$config = require $configPath;
if (isset($_GET['logout'])) {
$_SESSION = [];
session_destroy();
header('Location: ./');
exit;
}
$loginError = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['username'], $_POST['password'])) {
$userOk = hash_equals((string) $config['username'], (string) $_POST['username']);
$passwordOk = hash_equals((string) $config['password_sha256'], hash('sha256', (string) $_POST['password']));
if ($userOk && $passwordOk) {
session_regenerate_id(true);
$_SESSION['authenticated'] = true;
header('Location: ./');
exit;
}
usleep(350000);
$loginError = 'Anmeldung fehlgeschlagen.';
}
function fetchJson(string $url, string $method = 'GET', ?string $body = null): ?array
{
if (function_exists('curl_init')) {
$handle = curl_init($url);
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CONNECTTIMEOUT => 4,
CURLOPT_TIMEOUT => 8,
CURLOPT_USERAGENT => 'GerGames-Internal-Stats/1.0',
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json'],
CURLOPT_CUSTOMREQUEST => $method,
];
if ($body !== null) {
$options[CURLOPT_POSTFIELDS] = $body;
}
curl_setopt_array($handle, $options);
$response = curl_exec($handle);
$status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
if (!is_string($response) || $status < 200 || $status >= 300) return null;
} else {
$context = stream_context_create(['http' => [
'method' => $method,
'timeout' => 8,
'ignore_errors' => true,
'header' => "Accept: application/json\r\nContent-Type: application/json\r\nUser-Agent: GerGames-Internal-Stats/1.0\r\n",
'content' => $body ?? '',
]]);
$response = @file_get_contents($url, false, $context);
if (!is_string($response)) return null;
}
$decoded = json_decode($response, true);
return is_array($decoded) ? $decoded : null;
}
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
if (empty($_SESSION['authenticated'])):
?>
<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow,noarchive"><title>GerGames – Interne Statistik</title><style>
:root{color-scheme:dark;font-family:Inter,system-ui,sans-serif;background:#06121e;color:#eaf8fc}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:radial-gradient(circle at 50% 0,#12394d 0,transparent 48%),#06121e}.box{width:min(100%,420px);padding:32px;border:1px solid #2d7182;background:#091d2a;box-shadow:0 24px 80px #0008}h1{margin:0 0 8px;font-size:1.7rem}p{color:#9db6c1}label{display:block;margin:18px 0 6px;color:#bcd5df}input,button{width:100%;min-height:48px;border:1px solid #316f80;background:#071722;color:#fff;padding:10px 12px;font:inherit}button{margin-top:22px;background:#5cecff;color:#031119;font-weight:800;cursor:pointer}.error{color:#ffac99}
</style></head><body><main class="box"><h1>Interne Statistik</h1><p>Geschützter GerGames-Bereich</p><?php if ($loginError): ?><p class="error"><?= e($loginError) ?></p><?php endif; ?><form method="post" autocomplete="off"><label for="username">Benutzername</label><input id="username" name="username" required autocomplete="username"><label for="password">Passwort</label><input id="password" name="password" type="password" required autocomplete="current-password"><button type="submit">Anmelden</button></form></main></body></html>
<?php
exit;
endif;
$releases = fetchJson((string) $config['gitea_releases_api']) ?? [];
$downloadTotal = 0;
$latestDownloads = 0;
$latestRelease = 'Noch kein Release';
$platformDownloads = ['windows' => 0, 'android' => 0];
foreach ($releases as $releaseIndex => $release) {
if (!is_array($release)) continue;
if ($releaseIndex === 0) $latestRelease = (string) ($release['name'] ?? $release['tag_name'] ?? 'Aktuelles Release');
foreach (($release['assets'] ?? []) as $asset) {
if (!is_array($asset)) continue;
$count = max(0, (int) ($asset['download_count'] ?? 0));
$downloadTotal += $count;
if ($releaseIndex === 0) $latestDownloads += $count;
$name = strtolower((string) ($asset['name'] ?? ''));
if (substr($name, -4) === '.exe') $platformDownloads['windows'] += $count;
if (substr($name, -4) === '.apk') $platformDownloads['android'] += $count;
}
}
$playerStats = null;
$httpKey = trim((string) ($config['nakama_http_key'] ?? ''));
if ($httpKey !== '') {
$statsUrl = (string) $config['nakama_rpc_url'] . '?http_key=' . rawurlencode($httpKey) . '&unwrap';
$rpcResponse = fetchJson($statsUrl, 'POST', '{}');
if (is_array($rpcResponse)) {
$playerStats = $rpcResponse;
if (isset($rpcResponse['payload']) && is_string($rpcResponse['payload'])) {
$payload = json_decode($rpcResponse['payload'], true);
if (is_array($payload)) $playerStats = $payload;
}
}
}
$players = (int) ($playerStats['total_players'] ?? 0);
$today = gmdate('Y-m-d');
$activeToday = (int) (($playerStats['daily_active'] ?? [])[$today] ?? 0);
$updatedAt = isset($playerStats['updated_at']) ? date('d.m.Y H:i', (int) $playerStats['updated_at'] / 1000) : 'Noch keine Daten';
?>
<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow,noarchive"><meta http-equiv="refresh" content="300"><title>Void Bastion – Interne Statistik</title><style>
:root{color-scheme:dark;--bg:#06121e;--panel:#0a2030;--line:#26596a;--cyan:#5cecff;--muted:#92aebb;font-family:Inter,system-ui,sans-serif}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#12384b 0,transparent 35%),var(--bg);color:#edfaff;min-height:100vh}.shell{width:min(1120px,calc(100% - 32px));margin:auto;padding:48px 0}header{display:flex;justify-content:space-between;align-items:start;gap:24px;margin-bottom:34px}h1{margin:0;font-size:clamp(2rem,5vw,4rem);letter-spacing:-.04em}p{color:var(--muted)}a{color:var(--cyan)}.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.card{padding:24px;border:1px solid var(--line);background:linear-gradient(145deg,#0d2c3d,#071722)}.card strong{display:block;color:var(--cyan);font-size:clamp(2rem,4vw,3.5rem);line-height:1}.card span{display:block;margin-top:10px;color:#b9ced7}.panel{margin-top:18px;padding:24px;border:1px solid var(--line);background:#081b28}.warning{color:#ffbd7b}.meta{font-size:.9rem}@media(max-width:800px){.grid{grid-template-columns:1fr 1fr}header{display:block}}@media(max-width:480px){.grid{grid-template-columns:1fr}}
</style></head><body><main class="shell"><header><div><p>GERGAMES · NICHT ÖFFENTLICH</p><h1>Void Bastion Statistik</h1></div><a href="?logout=1">Abmelden</a></header><section class="grid"><article class="card"><strong><?= $downloadTotal ?></strong><span>Downloads insgesamt</span></article><article class="card"><strong><?= $latestDownloads ?></strong><span>Downloads aktuelles Release</span></article><article class="card"><strong><?= $players ?></strong><span>Registrierte Piloten</span></article><article class="card"><strong><?= $activeToday ?></strong><span>Heute aktive Piloten</span></article><article class="card"><strong><?= $platformDownloads['windows'] ?></strong><span>Windows-Downloads</span></article><article class="card"><strong><?= $platformDownloads['android'] ?></strong><span>Android-Downloads</span></article></section><section class="panel"><h2><?= e($latestRelease) ?></h2><p>Die Downloadwerte stammen direkt aus den Gitea-Release-Anhängen und werden alle fünf Minuten neu geladen.</p><?php if ($playerStats === null): ?><p class="warning">Die private Nakama-Statistik ist noch nicht mit dem HTTP-Schlüssel verbunden.</p><?php else: ?><p>Letzte Spieleraktualisierung: <?= e($updatedAt) ?></p><?php endif; ?><p class="meta">Diese Seite ist nicht verlinkt, von Suchmaschinen ausgeschlossen und verlangt eine Anmeldung. Es werden ausschließlich zusammengefasste Werte dargestellt.</p></section></main></body></html>