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;
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
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');
}
function privateRpc(array $config, string $name, array $payload = []): ?array
{
$base = (string) ($config['nakama_rpc_url'] ?? '');
$key = trim((string) ($config['nakama_http_key'] ?? ''));
if ($key === '' || !preg_match('~/private_stats$~', $base)) return null;
$url = preg_replace('~/private_stats$~', '/' . $name, $base) . '?http_key=' . rawurlencode($key) . '&unwrap';
$response = fetchJson($url, 'POST', json_encode($payload, JSON_THROW_ON_ERROR));
if (is_array($response) && isset($response['payload']) && is_string($response['payload'])) {
$decoded = json_decode($response['payload'], true);
if (is_array($decoded)) return $decoded;
}
return $response;
}
if (empty($_SESSION['authenticated'])):
?>
GerGames – Interne StatistikInterne Statistik
Geschützter GerGames-Bereich
= e($loginError) ?>
$codeKey], JSON_THROW_ON_ERROR));
if (is_array($response) && isset($response['payload']) && is_string($response['payload'])) {
$decodedPayload = json_decode($response['payload'], true);
if (is_array($decodedPayload)) $response = $decodedPayload;
}
if (is_array($response) && !empty($response['accepted'])) {
$_SESSION['promo_delete_flash'] = 'Der nicht eingelöste Code wurde gelöscht.';
header('Location: ./');
exit;
}
$reason = is_array($response) ? (string) ($response['error'] ?? '') : '';
$promoError = $reason === 'already_used'
? 'Dieser Code wurde inzwischen eingelöst und kann nicht gelöscht werden.'
: ($reason === 'not_found' ? 'Der Code ist bereits gelöscht oder nicht mehr vorhanden.' : 'Der Code konnte nicht gelöscht werden. Bitte später erneut versuchen.');
}
} elseif ($action === 'create') {
$reward = (string) ($_POST['reward'] ?? '');
$maxUses = filter_var($_POST['max_uses'] ?? '1', FILTER_VALIDATE_INT);
$note = trim((string) ($_POST['note'] ?? ''));
$noteLength = function_exists('mb_strlen') ? mb_strlen($note, 'UTF-8') : strlen($note);
$httpKey = trim((string) ($config['nakama_http_key'] ?? ''));
$rpcBase = preg_replace('~/private_stats$~', '/create_unlock_code', (string) ($config['nakama_rpc_url'] ?? ''));
if (!in_array($reward, ['all_towers', 'all_maps', 'all'], true)) {
$promoError = 'Bitte wähle einen gültigen Freischaltumfang.';
} elseif ($maxUses === false || $maxUses < 1 || $maxUses > 50000) {
$promoError = 'Die Anzahl der Einlösungen muss zwischen 1 und 50.000 liegen.';
} elseif ($noteLength > 160) {
$promoError = 'Die Bemerkung darf höchstens 160 Zeichen lang sein.';
} elseif ($httpKey === '' || !is_string($rpcBase) || $rpcBase === (string) ($config['nakama_rpc_url'] ?? '')) {
$promoError = 'Die interne Codeverwaltung ist noch nicht mit Nakama verbunden.';
} else {
$url = $rpcBase . '?http_key=' . rawurlencode($httpKey) . '&unwrap';
$response = fetchJson($url, 'POST', json_encode(['reward' => $reward, 'max_uses' => $maxUses, 'note' => $note], JSON_THROW_ON_ERROR));
if (is_array($response) && isset($response['payload']) && is_string($response['payload'])) {
$decodedPayload = json_decode($response['payload'], true);
if (is_array($decodedPayload)) $response = $decodedPayload;
}
if (is_array($response) && !empty($response['accepted']) && !empty($response['code'])) {
$_SESSION['promo_flash'] = [
'code' => (string) $response['code'],
'reward' => (string) ($response['reward'] ?? $reward),
'max_uses' => (int) ($response['max_uses'] ?? $maxUses),
'note' => (string) ($response['note'] ?? $note),
];
header('Location: ./');
exit;
}
$promoError = 'Der Server konnte keinen Code erstellen. Prüfe die Verbindung und versuche es erneut.';
}
} else {
$promoError = 'Unbekannte Code-Aktion.';
}
}
}
$balanceError = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['balance_action'])) {
if (!hash_equals((string) $_SESSION['csrf_token'], (string) ($_POST['csrf_token'] ?? ''))) {
$balanceError = 'Die Sitzung ist abgelaufen. Bitte lade die Seite neu und versuche es erneut.';
} elseif ((string) $_POST['balance_action'] !== 'save') {
$balanceError = 'Unbekannte Balance-Aktion.';
} else {
$httpKey = trim((string) ($config['nakama_http_key'] ?? ''));
$rpcBase = preg_replace('~/private_stats$~', '/set_game_balance', (string) ($config['nakama_rpc_url'] ?? ''));
$towerForm = $_POST['towers'] ?? [];
$enemyForm = $_POST['enemies'] ?? [];
$adminForm = is_array($_POST['admin_tower'] ?? null) ? $_POST['admin_tower'] : [];
$balance = ['schema' => 3, 'towers' => [], 'enemies' => [], 'admin_tower' => ['range' => $adminForm['range'] ?? '', 'cooldown' => $adminForm['cooldown'] ?? '']];
for ($i = 0; $i < 7; $i++) {
$row = is_array($towerForm[$i] ?? null) ? $towerForm[$i] : [];
$balance['towers'][] = ['cost' => $row['cost'] ?? '', 'damage' => $row['damage'] ?? '', 'range' => $row['range'] ?? '', 'cooldown' => $row['cooldown'] ?? '', 'targetClass' => $row['targetClass'] ?? '', 'bothScale' => $row['bothScale'] ?? ''];
}
for ($i = 0; $i < 9; $i++) {
$row = is_array($enemyForm[$i] ?? null) ? $enemyForm[$i] : [];
$balance['enemies'][] = ['hp' => $row['hp'] ?? '', 'speed' => $row['speed'] ?? '', 'reward' => $row['reward'] ?? '', 'breachDamage' => $row['breachDamage'] ?? '', 'shieldResistance' => $row['shieldResistance'] ?? ''];
}
if ($httpKey === '' || !is_string($rpcBase) || $rpcBase === (string) ($config['nakama_rpc_url'] ?? '')) {
$balanceError = 'Die Balance-Verwaltung ist noch nicht mit Nakama verbunden.';
} else {
$url = $rpcBase . '?http_key=' . rawurlencode($httpKey) . '&unwrap';
$response = fetchJson($url, 'POST', json_encode(['config' => $balance], JSON_THROW_ON_ERROR));
if (is_array($response) && isset($response['payload']) && is_string($response['payload'])) {
$decodedPayload = json_decode($response['payload'], true);
if (is_array($decodedPayload)) $response = $decodedPayload;
}
if (is_array($response) && !empty($response['accepted'])) {
$_SESSION['balance_flash'] = 'Balance gespeichert. Es wird beim nächsten Spielstart und in neuen Online-Partien verwendet.';
header('Location: ./#balance');
exit;
}
$balanceError = 'Der Server hat die Werte abgelehnt. Prüfe die Eingaben und ob der Runtime-Server aktualisiert wurde.';
}
}
}
$balanceFlash = (string) ($_SESSION['balance_flash'] ?? '');
unset($_SESSION['balance_flash']);
$balanceConfig = null;
$balanceLoadError = '';
$httpKey = trim((string) ($config['nakama_http_key'] ?? ''));
if ($httpKey !== '') {
$balanceRpc = preg_replace('~/private_stats$~', '/get_game_balance', (string) ($config['nakama_rpc_url'] ?? ''));
if (is_string($balanceRpc) && $balanceRpc !== (string) ($config['nakama_rpc_url'] ?? '')) {
$balanceResponse = fetchJson($balanceRpc . '?http_key=' . rawurlencode($httpKey) . '&unwrap', 'POST', '{}');
if (is_array($balanceResponse) && isset($balanceResponse['payload']) && is_string($balanceResponse['payload'])) {
$decodedPayload = json_decode($balanceResponse['payload'], true);
if (is_array($decodedPayload)) $balanceResponse = $decodedPayload;
}
if (is_array($balanceResponse) && is_array($balanceResponse['config'] ?? null)) $balanceConfig = $balanceResponse['config'];
else $balanceLoadError = 'Die aktuelle Balance konnte nicht geladen werden. Servermodul möglicherweise noch nicht aktualisiert.';
}
}
$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', intdiv((int) $playerStats['updated_at'], 1000)) : 'Noch keine Daten';
$directoryPage = filter_var($_GET['spieler_seite'] ?? 0, FILTER_VALIDATE_INT);
$directoryPage = $directoryPage === false ? 0 : max(0, min(10000, $directoryPage));
$playerDirectory = privateRpc($config, 'list_registered_players', ['page' => $directoryPage]);
if (is_array($playerDirectory) && isset($playerDirectory['players'], $playerDirectory['total'])) {
$players = (int) $playerDirectory['total'];
$activeToday = (int) ($playerDirectory['active_today'] ?? 0);
} else {
$playerDirectory = null;
}
$selectedPlayerId = strtolower(trim((string) ($_GET['spieler'] ?? '')));
if (!preg_match('/^[a-f0-9-]{36}$/D', $selectedPlayerId)) $selectedPlayerId = '';
$selectedPlayer = null;
if ($selectedPlayerId !== '') {
$playerResponse = privateRpc($config, 'get_registered_player', ['user_id' => $selectedPlayerId]);
if (is_array($playerResponse) && !empty($playerResponse['found']) && is_array($playerResponse['player'] ?? null)) {
$selectedPlayer = $playerResponse['player'];
}
}
$promoCodes = null;
$promoListError = '';
if ($httpKey !== '') {
$listRpcUrl = preg_replace('~/private_stats$~', '/list_unlock_codes', (string) ($config['nakama_rpc_url'] ?? ''));
if (is_string($listRpcUrl) && $listRpcUrl !== (string) ($config['nakama_rpc_url'] ?? '')) {
$listUrl = $listRpcUrl . '?http_key=' . rawurlencode($httpKey) . '&unwrap';
$listResponse = fetchJson($listUrl, 'POST', '{}');
if (is_array($listResponse) && isset($listResponse['payload']) && is_string($listResponse['payload'])) {
$decodedPayload = json_decode($listResponse['payload'], true);
if (is_array($decodedPayload)) $listResponse = $decodedPayload;
}
if (is_array($listResponse) && isset($listResponse['codes']) && is_array($listResponse['codes'])) {
$promoCodes = $listResponse['codes'];
} else {
$promoListError = 'Die Code-Übersicht konnte gerade nicht geladen werden.';
}
} else {
$promoListError = 'Die interne Codeverwaltung ist noch nicht mit Nakama verbunden.';
}
} else {
$promoListError = 'Die interne Codeverwaltung ist noch nicht mit Nakama verbunden.';
}
$diagnosticRows = null;
$diagnosticReports = [];
$diagnosticError = '';
$diagnosticRound = strtolower(trim((string) ($_GET['round_id'] ?? '')));
if ($diagnosticRound !== '' && !preg_match('/^[a-f0-9]{16}$/D', $diagnosticRound)) $diagnosticRound = '';
if ($httpKey !== '') {
$diagnosticRpc = preg_replace('~/private_stats$~', '/list_online_diagnostics', (string) ($config['nakama_rpc_url'] ?? ''));
if (is_string($diagnosticRpc) && $diagnosticRpc !== (string) ($config['nakama_rpc_url'] ?? '')) {
$diagnosticUrl = $diagnosticRpc . '?http_key=' . rawurlencode($httpKey) . '&unwrap';
$diagnosticResponse = fetchJson($diagnosticUrl, 'POST', '{}');
if (is_array($diagnosticResponse) && isset($diagnosticResponse['payload']) && is_string($diagnosticResponse['payload'])) {
$decoded = json_decode($diagnosticResponse['payload'], true);
if (is_array($decoded)) $diagnosticResponse = $decoded;
}
if (is_array($diagnosticResponse) && isset($diagnosticResponse['reports']) && is_array($diagnosticResponse['reports'])) {
$diagnosticRows = $diagnosticResponse['reports'];
if ($diagnosticRound !== '') {
$detailResponse = fetchJson($diagnosticUrl, 'POST', json_encode(['round_id' => $diagnosticRound], JSON_THROW_ON_ERROR));
if (is_array($detailResponse) && isset($detailResponse['payload']) && is_string($detailResponse['payload'])) {
$decoded = json_decode($detailResponse['payload'], true);
if (is_array($decoded)) $detailResponse = $decoded;
}
if (is_array($detailResponse) && isset($detailResponse['reports']) && is_array($detailResponse['reports'])) {
$diagnosticReports = $detailResponse['reports'];
} else {
$diagnosticError = 'Die ausgewählte Runde konnte nicht geladen werden.';
}
}
} else {
$diagnosticError = 'Die Online-Diagnose konnte nicht geladen werden. Servermodul eventuell noch nicht aktualisiert.';
}
}
}
?>
Void Bastion – Interne Statistik
= $downloadTotal ?>Downloads insgesamt= $latestDownloads ?>Downloads aktuelles Release= $players ?>Registrierte Piloten= $activeToday ?>Heute aktive Piloten= $platformDownloads['windows'] ?>Windows-Downloads= $platformDownloads['android'] ?>Android-Downloads
= e($latestRelease) ?>
Die Downloadwerte stammen direkt aus den Gitea-Release-Anhängen und werden alle fünf Minuten neu geladen.
Die private Nakama-Statistik ist noch nicht mit dem HTTP-Schlüssel verbunden.
Letzte Spieleraktualisierung: = e($updatedAt) ?>
Diese Seite ist nicht verlinkt, von Suchmaschinen ausgeschlossen und verlangt eine Anmeldung. Spielernamen, Aktivität und technische Rundenberichte sind nur hier einsehbar.
Registrierte Spieler
Spielerkonten und ihre letzte Aktivität. Automatisch erzeugte interne Testkonten werden ausgeblendet. „Zuletzt gespielt“ und Turmbauten werden ab der neuen Spielversion erfasst; frühere Runden lassen sich nicht rückwirkend zählen.
Die Spielerliste konnte gerade nicht geladen werden.
| Spieler | Registriert | Zuletzt gespielt | Zuletzt online | Turmbauten |
| = e((string) ($player['name'] ?? 'PILOT')) ?> | = (int) ($player['registered_at'] ?? 0) > 0 ? e(date('d.m.Y H:i', intdiv((int) $player['registered_at'], 1000))) : '–' ?> | = (int) ($player['last_played'] ?? 0) > 0 ? e(date('d.m.Y H:i', intdiv((int) $player['last_played'], 1000))) : 'Noch nicht erfasst' ?> | = (int) ($player['last_seen'] ?? 0) > 0 ? e(date('d.m.Y H:i', intdiv((int) $player['last_seen'], 1000))) : '–' ?> | = (int) ($player['tower_build_total'] ?? 0) ?> |
= (int) $playerDirectory['total'] ?> Spieler · Seite = $directoryPage + 1 ?> 0): ?>← Zurück Weiter →
= $selectedPlayer !== null ? e((string) ($selectedPlayer['name'] ?? 'Spieler')) : 'Spieler nicht gefunden' ?>
$name) $towerRows[] = ['name' => $name, 'preview' => $towerPreviews[$index], 'count' => (int) ($builds[$index] ?? 0)]; usort($towerRows, static fn(array $a, array $b): int => $b['count'] <=> $a['count']); ?>
Registriert: = e(date('d.m.Y H:i', intdiv((int) ($selectedPlayer['registered_at'] ?? 0), 1000))) ?> · Zuletzt gespielt: = (int) ($selectedPlayer['last_played'] ?? 0) > 0 ? e(date('d.m.Y H:i', intdiv((int) $selectedPlayer['last_played'], 1000))) : 'Noch nicht erfasst' ?> · Insgesamt = (int) ($selectedPlayer['tower_build_total'] ?? 0) ?> Türme gebaut
| Turm | Gebaut |
$tower): ?> = e($tower['name']) ?>= $rank === 0 && $tower['count'] > 0 ? ' · meistgebaut' : '' ?> | = $tower['count'] ?> |
Gezählt werden bestätigte Bauten im Online-Koop sowie lokal gebaute Türme, sobald das Spiel sie übertragen konnte. KI- und Admin-Türme zählen nicht mit.
← Zur Spielerliste
Online-Diagnose
Die beiden PCs senden ihre technischen Messwerte am Ende einer Online-Partie automatisch mit derselben Rundenkennung. Berichte sind maximal 14 Tage relevant; Namen und Raumcodes werden hier nicht gespeichert.
= e($diagnosticError) ?>
Noch keine Verbindung zur privaten Online-Diagnose.
Noch keine Berichte aus der neuen Spielversion vorhanden.
| Runde | Seite | Zeitpunkt | Version | Messungen |
|---|
= e($rowRound) ?> | = (int) ($row['slot'] ?? -1) + 1 ?> | = e(date('d.m.Y H:i:s', intdiv((int) ($row['created_at'] ?? 0), 1000))) ?> | = e((string) ($row['version'] ?? '')) ?> | = (int) ($row['record_count'] ?? 0) ?> |
Runde = e($diagnosticRound) ?>
Für diese Kennung liegen noch keine Berichte vor.
Seite = (int) ($report['slot'] ?? -1) + 1 ?> · = e((string) ($report['version'] ?? '')) ?>
| UTC | Spielstände/s | Abstand p95 / max | Tick-Sprünge | Warteschlange verworfen | Bildaussetzer | Gegner / Türme |
|---|
| = e((string) ($record['utc'] ?? '')) ?> | = e((string) ($record['snapshots_per_second'] ?? 0)) ?> | = (int) ($record['gap_p95_ms'] ?? 0) ?> / = (int) ($record['gap_max_ms'] ?? 0) ?> ms | = (int) ($record['tick_jumps_over_1'] ?? 0) ?> | = (int) ($record['queue_dropped'] ?? 0) ?> | = (int) ($record['frame_stalls_over_200_ms'] ?? 0) ?> (max = (int) ($record['frame_max_ms'] ?? 0) ?> ms) | = (int) ($record['enemies'] ?? 0) ?> / = (int) ($record['towers'] ?? 0) ?> |
Einzelereignisse anzeigen
| UTC | Ereignis | Dauer | Server-Tick |
|---|
| = e((string) ($record['utc'] ?? '')) ?> | = e((string) ($record['event'] ?? '')) ?> | = (int) ($record['duration_ms'] ?? 0) ?> ms | = (int) ($record['last_server_tick'] ?? 0) ?> |
Zur Übersicht
Einheiten-Balancing
Eine gemeinsame serverseitige Konfiguration wird für Offline-Spiele beim nächsten Start geladen und in neuen Online-Partien direkt vom Server verwendet. Bei fehlender Verbindung nutzt das Spiel den letzten lokal gespeicherten Stand. Laufende Partien bleiben unverändert.
= e($balanceFlash) ?>
= e($balanceError) ?>
= e($balanceLoadError) ?>
Das Balancing-Formular wird angezeigt, sobald der Runtime-Server die Balance-Funktionen bereitstellt.
Freischaltcodes erstellen
Codes und Bemerkungen werden ausschließlich in der geschützten Serverablage verwaltet. Der Klartext ist nur in diesem angemeldeten Bereich sichtbar.
Code erstellt:= e((string) $promoFlash['code']) ?>= e((string) $promoFlash['reward']) ?> · offen · maximal = (int) $promoFlash['max_uses'] ?> Einlösungen · = e((string) $promoFlash['note']) ?>
= e($promoDeleteFlash) ?>
= e($promoError) ?>
= e($promoListError) ?>
Code-Übersicht
Noch keine Freischaltcodes vorhanden.
| Code | Freischaltung | Status | Einlösungen | Erstellt | Bemerkung | Aktion |
= $maxUses ? 'redeemed' : ($uses > 0 ? 'partial' : 'open');
$statusLabel = $status === 'redeemed' ? 'Eingelöst' : ($status === 'partial' ? 'Teilweise eingelöst' : 'Offen');
$createdAt = (int) ($promoCode['created_at'] ?? 0);
$createdLabel = $createdAt > 0 ? date('d.m.Y H:i', intdiv($createdAt, 1000)) : '—';
$codeLabel = (string) ($promoCode['code'] ?? '');
if ($codeLabel === '') $codeLabel = 'Alter Code – Klartext nicht gespeichert';
$codeKey = strtolower((string) ($promoCode['code_key'] ?? ''));
?>= e($codeLabel) ?> | = e((string) ($promoCode['reward'] ?? '')) ?> | = e($statusLabel) ?> | = $uses ?> / = $maxUses ?> | = e($createdLabel) ?> | = e((string) (($promoCode['note'] ?? '') !== '' ? $promoCode['note'] : '—')) ?> | — |