298 lines
26 KiB
PHP
298 lines
26 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
require_once __DIR__ . '/includes/bootstrap.php';
|
||
pnpaas_session_start();
|
||
|
||
if (empty($_SESSION['pnpaas_user_id'])) {
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
|
||
$userId = (int)$_SESSION['pnpaas_user_id'];
|
||
try {
|
||
$currentUserQuery = pnpaas_db()->prepare('SELECT username, role, status FROM users WHERE id = :id LIMIT 1');
|
||
$currentUserQuery->execute(['id' => $userId]);
|
||
$currentUser = $currentUserQuery->fetch();
|
||
} catch (Throwable $exception) {
|
||
error_log('PnPaaS session user lookup error: ' . $exception->getMessage());
|
||
$currentUser = false;
|
||
}
|
||
if (!is_array($currentUser) || (string)$currentUser['status'] !== 'active') {
|
||
$_SESSION = [];
|
||
session_destroy();
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
$_SESSION['pnpaas_admin'] = (string)$currentUser['username'];
|
||
$_SESSION['pnpaas_role'] = (string)$currentUser['role'];
|
||
$role = (string)$currentUser['role'];
|
||
$username = (string)$currentUser['username'];
|
||
$isAdmin = $role === 'admin';
|
||
$csrf = pnpaas_csrf_token();
|
||
$message = null;
|
||
$error = null;
|
||
|
||
$plans = [
|
||
's' => ['label' => 'S', 'memory' => 512, 'cpu' => 1.00, 'storage' => 1024],
|
||
'm' => ['label' => 'M', 'memory' => 2048, 'cpu' => 2.00, 'storage' => 10240],
|
||
'l' => ['label' => 'L', 'memory' => 4096, 'cpu' => 4.00, 'storage' => 25600],
|
||
];
|
||
|
||
function dashboard_escape(string $value): string { return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); }
|
||
function dashboard_status(string $status): string {
|
||
return match ($status) {
|
||
'pending_payment' => 'Zahlung ausstehend',
|
||
'provisioning' => 'Wird eingerichtet',
|
||
'running' => 'Läuft',
|
||
'stopped' => 'Gestoppt',
|
||
'error' => 'Fehler',
|
||
default => ucfirst($status),
|
||
};
|
||
}
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
if (!pnpaas_valid_csrf((string)($_POST['csrf_token'] ?? ''))) {
|
||
$error = 'Die Sitzung ist abgelaufen. Bitte laden Sie die Seite neu.';
|
||
} else {
|
||
$action = (string)($_POST['action'] ?? '');
|
||
try {
|
||
$db = pnpaas_db();
|
||
if ($action === 'request_instance') {
|
||
$name = trim((string)($_POST['name'] ?? ''));
|
||
$planKey = strtolower(trim((string)($_POST['plan'] ?? '')));
|
||
$paymentMethod = (string)($_POST['payment_method'] ?? 'bank_transfer');
|
||
if ($name === '' || !preg_match('/^[A-Za-z0-9][A-Za-z0-9 _.-]{1,119}$/', $name)) {
|
||
throw new RuntimeException('Bitte einen gültigen Instanznamen eingeben.');
|
||
}
|
||
if (!isset($plans[$planKey]) || !in_array($paymentMethod, ['paypal', 'bank_transfer'], true)) {
|
||
throw new RuntimeException('Tarif oder Zahlungsart ist ungültig.');
|
||
}
|
||
$slug = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $name) ?? 'foundry');
|
||
$containerName = 'pnpaas-' . trim($slug, '-') . '-' . bin2hex(random_bytes(3));
|
||
$port = (int)$db->query('SELECT COALESCE(MAX(port), 29999) + 1 FROM instances')->fetchColumn();
|
||
if ($port > 60000) throw new RuntimeException('Es sind derzeit keine Ports verfügbar.');
|
||
$p = $plans[$planKey];
|
||
$statement = $db->prepare('INSERT INTO instances (owner_user_id, name, plan, payment_status, payment_method, container_name, port, volume_size_mb, memory_limit_mb, cpu_limit, status) VALUES (:owner, :name, :plan, "pending", :method, :container, :port, :storage, :memory, :cpu, "pending_payment")');
|
||
$statement->execute(['owner' => $userId, 'name' => $name, 'plan' => $planKey, 'method' => $paymentMethod, 'container' => $containerName, 'port' => $port, 'storage' => $p['storage'], 'memory' => $p['memory'], 'cpu' => $p['cpu']]);
|
||
$message = 'Die Instanzanfrage wurde erfasst und wartet auf die Zahlungsfreigabe.';
|
||
} elseif ($action === 'approve_instance' && $isAdmin) {
|
||
$instanceId = (int)($_POST['instance_id'] ?? 0);
|
||
$query = $db->prepare('SELECT * FROM instances WHERE id = :id AND ((status = "pending_payment") OR (status = "error" AND payment_status = "paid")) LIMIT 1');
|
||
$query->execute(['id' => $instanceId]);
|
||
$instance = $query->fetch();
|
||
if (!$instance) throw new RuntimeException('Diese Instanzanfrage ist nicht mehr freigabefähig.');
|
||
$db->prepare('UPDATE instances SET payment_status = "paid", paid_at = CURRENT_TIMESTAMP, status = "provisioning", error_message = NULL WHERE id = :id')->execute(['id' => $instanceId]);
|
||
$endpoint = pnpaas_portainer_endpoint_id();
|
||
$dataPath = '/opt/foundry/instances/' . $instance['container_name'];
|
||
$payload = [
|
||
'Image' => 'pnpaas/foundryvtt:14.368',
|
||
'Env' => ['FOUNDRY_VERSION=14.368'],
|
||
'ExposedPorts' => ['30000/tcp' => new stdClass()],
|
||
'HostConfig' => [
|
||
'Binds' => [$dataPath . ':/data'],
|
||
'PortBindings' => ['30000/tcp' => [['HostPort' => (string)$instance['port']]]],
|
||
'Memory' => (int)$instance['memory_limit_mb'] * 1024 * 1024,
|
||
'NanoCpus' => (int)((float)$instance['cpu_limit'] * 1000000000),
|
||
'RestartPolicy' => ['Name' => 'unless-stopped'],
|
||
],
|
||
];
|
||
$created = pnpaas_portainer_request('/api/endpoints/' . $endpoint . '/docker/containers/create?name=' . rawurlencode($instance['container_name']), 'POST', $payload);
|
||
$containerId = (string)($created['Id'] ?? '');
|
||
if ($containerId === '') throw new RuntimeException('Portainer hat keine Container-ID geliefert.');
|
||
pnpaas_portainer_request('/api/endpoints/' . $endpoint . '/docker/containers/' . rawurlencode($containerId) . '/start', 'POST', []);
|
||
$db->prepare('UPDATE instances SET portainer_container_id = :cid, status = "running" WHERE id = :id')->execute(['cid' => $containerId, 'id' => $instanceId]);
|
||
$message = 'Die Instanz wurde bezahlt markiert, erstellt und gestartet.';
|
||
} elseif ($action === 'instance_action') {
|
||
$instanceId = (int)($_POST['instance_id'] ?? 0);
|
||
$operation = (string)($_POST['operation'] ?? '');
|
||
$allowedOperations = ['start', 'stop', 'restart', 'redeploy', 'delete'];
|
||
if (!in_array($operation, $allowedOperations, true)) {
|
||
throw new RuntimeException('Nicht unterstützte Instanzaktion.');
|
||
}
|
||
$query = $db->prepare('SELECT * FROM instances WHERE id = :id AND status <> "deleted" AND (owner_user_id = :owner OR :is_admin = 1) LIMIT 1');
|
||
$query->execute(['id' => $instanceId, 'owner' => $userId, 'is_admin' => $isAdmin ? 1 : 0]);
|
||
$instance = $query->fetch();
|
||
if (!$instance || empty($instance['portainer_container_id'])) {
|
||
throw new RuntimeException('Die Instanz ist nicht verfügbar.');
|
||
}
|
||
if ($operation === 'delete' && !$isAdmin) {
|
||
throw new RuntimeException('Nur Administratoren dürfen Instanzen löschen.');
|
||
}
|
||
$endpoint = pnpaas_portainer_endpoint_id();
|
||
$containerId = rawurlencode((string)$instance['portainer_container_id']);
|
||
if ($operation === 'redeploy') {
|
||
$db->prepare('UPDATE instances SET status = "provisioning", error_message = NULL WHERE id = :id')->execute(['id' => $instanceId]);
|
||
pnpaas_portainer_request('/api/endpoints/' . $endpoint . '/docker/containers/' . $containerId . '?force=true', 'DELETE', null);
|
||
$dataPath = '/opt/foundry/instances/' . $instance['container_name'];
|
||
$payload = [
|
||
'Image' => 'pnpaas/foundryvtt:14.368',
|
||
'Env' => ['FOUNDRY_VERSION=14.368'],
|
||
'ExposedPorts' => ['30000/tcp' => new stdClass()],
|
||
'HostConfig' => [
|
||
'Binds' => [$dataPath . ':/data'],
|
||
'PortBindings' => ['30000/tcp' => [['HostPort' => (string)$instance['port']]]],
|
||
'Memory' => (int)$instance['memory_limit_mb'] * 1024 * 1024,
|
||
'NanoCpus' => (int)((float)$instance['cpu_limit'] * 1000000000),
|
||
'RestartPolicy' => ['Name' => 'unless-stopped'],
|
||
],
|
||
];
|
||
$created = pnpaas_portainer_request('/api/endpoints/' . $endpoint . '/docker/containers/create?name=' . rawurlencode($instance['container_name']), 'POST', $payload);
|
||
$newContainerId = (string)($created['Id'] ?? '');
|
||
if ($newContainerId === '') throw new RuntimeException('Portainer hat keine neue Container-ID geliefert.');
|
||
pnpaas_portainer_request('/api/endpoints/' . $endpoint . '/docker/containers/' . rawurlencode($newContainerId) . '/start', 'POST', []);
|
||
$db->prepare('UPDATE instances SET portainer_container_id = :cid, status = "running", error_message = NULL WHERE id = :id')->execute(['cid' => $newContainerId, 'id' => $instanceId]);
|
||
$message = 'Die Instanz wurde neu bereitgestellt. Die Daten in /data wurden beibehalten.';
|
||
} else {
|
||
$apiPath = '/api/endpoints/' . $endpoint . '/docker/containers/' . $containerId . '/' . $operation;
|
||
pnpaas_portainer_request($apiPath, $operation === 'delete' ? 'DELETE' : 'POST', []);
|
||
$newStatus = match ($operation) {
|
||
'start', 'restart' => 'running',
|
||
'stop' => 'stopped',
|
||
'delete' => 'deleted',
|
||
};
|
||
$db->prepare('UPDATE instances SET status = :status, error_message = NULL WHERE id = :id')->execute(['status' => $newStatus, 'id' => $instanceId]);
|
||
$message = match ($operation) {
|
||
'start' => 'Die Instanz wurde gestartet.',
|
||
'stop' => 'Die Instanz wurde gestoppt.',
|
||
'restart' => 'Die Instanz wurde neu gestartet.',
|
||
'delete' => 'Die Instanz wurde gelöscht. Das Datenverzeichnis bleibt zur Sicherheit erhalten.',
|
||
};
|
||
}
|
||
}
|
||
} catch (Throwable $exception) {
|
||
if (in_array(($action ?? ''), ['approve_instance', 'instance_action'], true) && isset($instanceId)) {
|
||
try { pnpaas_db()->prepare('UPDATE instances SET status = "error", error_message = :error WHERE id = :id')->execute(['error' => 'Die Bereitstellung oder Aktion konnte nicht abgeschlossen werden.', 'id' => $instanceId]); } catch (Throwable) {}
|
||
}
|
||
error_log('PnPaaS instance action error: ' . $exception->getMessage());
|
||
$error = 'Die angeforderte Aktion konnte nicht ausgeführt werden. Bitte versuchen Sie es erneut.';
|
||
}
|
||
}
|
||
}
|
||
|
||
$view = (string)($_GET['view'] ?? '');
|
||
$showCustomers = $isAdmin && $view === 'customers';
|
||
$showInstances = !$isAdmin || $view === 'instances' || ($isAdmin && $_SERVER['REQUEST_METHOD'] === 'POST');
|
||
$instances = [];
|
||
$customers = [];
|
||
$realms = [];
|
||
$realmError = null;
|
||
|
||
if ($isAdmin && !$showInstances) {
|
||
try {
|
||
$configuredEndpointId = pnpaas_portainer_endpoint_id();
|
||
// Die Endpoint-Metadaten sind für eingeschränkte Portainer-Benutzer nicht immer lesbar.
|
||
// Die konfigurierte ID reicht aus; der eigentliche Zugriff erfolgt über den Docker-Proxy.
|
||
$endpoints = [[
|
||
'Id' => $configuredEndpointId,
|
||
'Name' => 'Portainer-Umgebung ' . $configuredEndpointId,
|
||
'Status' => 1,
|
||
]];
|
||
foreach ($endpoints as $endpoint) {
|
||
$endpointId = (int)($endpoint['Id'] ?? 0);
|
||
if ($endpointId < 1) continue;
|
||
$realm = [
|
||
'name' => (string)($endpoint['Name'] ?? ('Realm ' . $endpointId)),
|
||
'online' => ((int)($endpoint['Status'] ?? 0) === 1),
|
||
'version' => 'nicht verfügbar', 'cpus' => 0, 'cpu_percent' => 0.0,
|
||
'memory_total' => 0, 'memory_used' => 0, 'disk_used' => 0,
|
||
'docker_root' => 'nicht verfügbar', 'containers_running' => 0, 'containers_total' => 0,
|
||
];
|
||
try {
|
||
$info = pnpaas_portainer_request('/api/endpoints/' . $endpointId . '/docker/info');
|
||
$disk = pnpaas_portainer_request('/api/endpoints/' . $endpointId . '/docker/system/df');
|
||
$containers = pnpaas_portainer_request('/api/endpoints/' . $endpointId . '/docker/containers/json?all=1');
|
||
$memoryUsed = 0;
|
||
$cpuPercent = 0.0;
|
||
foreach ($containers as $container) {
|
||
if (($container['State'] ?? '') !== 'running' || empty($container['Id'])) continue;
|
||
try {
|
||
$stats = pnpaas_portainer_request('/api/endpoints/' . $endpointId . '/docker/containers/' . rawurlencode((string)$container['Id']) . '/stats?stream=false');
|
||
$memoryUsed += (int)($stats['memory_stats']['usage'] ?? 0);
|
||
$cpuDelta = (int)($stats['cpu_stats']['cpu_usage']['total_usage'] ?? 0) - (int)($stats['precpu_stats']['cpu_usage']['total_usage'] ?? 0);
|
||
$systemDelta = (int)($stats['cpu_stats']['system_cpu_usage'] ?? 0) - (int)($stats['precpu_stats']['system_cpu_usage'] ?? 0);
|
||
$onlineCpus = max(1, (int)($stats['cpu_stats']['online_cpus'] ?? $info['NCPU'] ?? 1));
|
||
if ($systemDelta > 0) $cpuPercent += ($cpuDelta / $systemDelta) * $onlineCpus * 100;
|
||
} catch (Throwable) {
|
||
// Ein einzelner Container darf die Realm-Anzeige nicht ausfallen lassen.
|
||
}
|
||
}
|
||
$diskUsed = (int)($disk['LayersSize'] ?? 0);
|
||
foreach (($disk['Containers'] ?? []) as $item) $diskUsed += (int)($item['SizeRw'] ?? 0);
|
||
foreach (($disk['BuildCache'] ?? []) as $item) $diskUsed += (int)($item['Size'] ?? 0);
|
||
$realm['online'] = true;
|
||
$realm['version'] = (string)($info['ServerVersion'] ?? 'unbekannt');
|
||
$realm['cpus'] = (int)($info['NCPU'] ?? 0);
|
||
$realm['cpu_percent'] = $cpuPercent;
|
||
$realm['memory_total'] = (int)($info['MemTotal'] ?? 0);
|
||
$realm['memory_used'] = $memoryUsed;
|
||
$realm['disk_used'] = $diskUsed;
|
||
$realm['docker_root'] = (string)($info['DockerRootDir'] ?? 'unbekannt');
|
||
$realm['containers_running'] = (int)($info['ContainersRunning'] ?? 0);
|
||
$realm['containers_total'] = (int)($info['Containers'] ?? 0);
|
||
} catch (Throwable $exception) {
|
||
error_log('PnPaaS realm ' . $endpointId . ' error: ' . $exception->getMessage());
|
||
}
|
||
$realms[] = $realm;
|
||
}
|
||
} catch (Throwable $exception) {
|
||
error_log('PnPaaS realm dashboard error: ' . $exception->getMessage());
|
||
$realmError = 'Die Realm-Daten konnten derzeit nicht vollständig geladen werden.';
|
||
}
|
||
}
|
||
|
||
if ($showInstances) {
|
||
$statement = $isAdmin
|
||
? pnpaas_db()->query('SELECT i.*, u.username FROM instances i JOIN users u ON u.id = i.owner_user_id ORDER BY i.created_at DESC')
|
||
: (function () use ($userId) { $s = pnpaas_db()->prepare('SELECT i.*, u.username FROM instances i JOIN users u ON u.id = i.owner_user_id WHERE i.owner_user_id = :owner ORDER BY i.created_at DESC'); $s->execute(['owner' => $userId]); return $s; })();
|
||
$instances = $statement->fetchAll();
|
||
}
|
||
|
||
if ($showCustomers) {
|
||
$customers = pnpaas_db()->query('SELECT id, username, email, role, status, created_at FROM users ORDER BY created_at DESC')->fetchAll();
|
||
}
|
||
|
||
function dashboard_bytes(int $bytes): string {
|
||
if ($bytes <= 0) return 'nicht verfügbar';
|
||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
$index = min((int)floor(log($bytes, 1024)), count($units) - 1);
|
||
return number_format($bytes / (1024 ** $index), 1, ',', '.') . ' ' . $units[$index];
|
||
}
|
||
?>
|
||
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>PnPaaS – Dashboard</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@500;600&family=Open+Sans:wght@400;600;700&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="assets/style.css">
|
||
</head>
|
||
<body>
|
||
<header class="topbar"><div class="topbar-brand"><strong>PnPaaS</strong><span class="topbar-subtitle"><?= $isAdmin ? 'Administration' : 'Kundenbereich' ?></span></div><nav class="main-nav" aria-label="Hauptnavigation"><a class="<?= !$showInstances && !$showCustomers ? 'active' : '' ?>" href="dashboard.php">Dashboard</a><a class="<?= $showInstances ? 'active' : '' ?>" href="dashboard.php?view=instances">Instanzverwaltung</a><?php if ($isAdmin): ?><a class="<?= $showCustomers ? 'active' : '' ?>" href="dashboard.php?view=customers">Kundenverwaltung</a><?php endif; ?></nav><div class="topbar-actions"><span><?= dashboard_escape($username) ?></span><a href="logout.php">Abmelden</a></div></header>
|
||
<main class="dashboard">
|
||
<?php if ($isAdmin && !$showInstances && !$showCustomers): ?>
|
||
<p class="eyebrow">Systemübersicht</p><h1>Realms</h1>
|
||
<p class="intro">Status und Ressourcen der Portainer-Server, auf denen die FoundryVTT-Instanzen ausgeführt werden.</p>
|
||
<?php if ($realmError !== null): ?><div class="alert alert-error" role="alert"><?= dashboard_escape($realmError) ?></div><?php endif; ?>
|
||
<?php if ($realms === [] && $realmError === null): ?><div class="empty-state">Keine Realms konfiguriert.</div><?php else: ?><section class="realm-grid" aria-label="Realm-Status">
|
||
<?php foreach ($realms as $realm): ?><article class="realm-card"><div class="realm-card-header"><div><p class="eyebrow">Portainer-Server</p><h2><?= dashboard_escape($realm['name']) ?></h2></div><span class="status-badge <?= $realm['online'] ? 'status-online' : 'status-offline' ?>"><?= $realm['online'] ? 'Server Up' : 'Server Down' ?></span></div>
|
||
<div class="realm-meta"><span>Docker <?= dashboard_escape($realm['version']) ?></span><span><?= (int)$realm['containers_running'] ?> / <?= (int)$realm['containers_total'] ?> Container aktiv</span></div>
|
||
<div class="realm-metrics"><div class="realm-metric"><span class="metric-label">CPU</span><strong><?= number_format((float)$realm['cpu_percent'], 1, ',', '.') ?> %</strong><small><?= (int)$realm['cpus'] ?> CPU-Kerne · Container-Auslastung</small></div><div class="realm-metric"><span class="metric-label">RAM</span><strong><?= dashboard_bytes((int)$realm['memory_used']) ?></strong><small>von <?= dashboard_bytes((int)$realm['memory_total']) ?> Docker-Speicher</small></div><div class="realm-metric"><span class="metric-label">Festplatte</span><strong><?= dashboard_bytes((int)$realm['disk_used']) ?></strong><small>belegte Docker-Daten · Kapazität nicht gemeldet</small></div></div>
|
||
<div class="realm-footer"><span>Docker Root: <code><?= dashboard_escape($realm['docker_root']) ?></code></span><span>Stand: <?= date('H:i:s') ?></span></div></article><?php endforeach; ?></section><?php endif; ?>
|
||
<p class="dashboard-link"><a href="dashboard.php?view=instances">Instanzen verwalten →</a></p>
|
||
<?php elseif ($showCustomers): ?>
|
||
<p class="eyebrow">Administration</p><h1>Kundenverwaltung</h1>
|
||
<p class="intro">Benutzerkonten und deren Zugriffsstatus.</p>
|
||
<section class="data-section"><div class="table-wrap"><table class="data-table"><thead><tr><th>Benutzername</th><th>E-Mail</th><th>Rolle</th><th>Status</th><th>Angelegt</th></tr></thead><tbody><?php foreach ($customers as $customer): ?><tr><td data-label="Benutzername"><strong><?= dashboard_escape((string)$customer['username']) ?></strong></td><td data-label="E-Mail"><?= dashboard_escape((string)$customer['email']) ?></td><td data-label="Rolle"><?= dashboard_escape((string)$customer['role']) ?></td><td data-label="Status"><?= dashboard_escape((string)$customer['status']) ?></td><td data-label="Angelegt"><?= dashboard_escape((string)$customer['created_at']) ?></td></tr><?php endforeach; ?></tbody></table></div></section>
|
||
<?php else: ?>
|
||
<p class="eyebrow">Übersicht</p><h1><?= $isAdmin ? 'Instanzen verwalten' : 'Meine Instanzen' ?></h1>
|
||
<p class="intro">FoundryVTT-Instanzen buchen und ihren Bereitstellungsstatus verfolgen.</p>
|
||
<?php if ($message !== null): ?><div class="alert alert-success" role="status"><?= dashboard_escape($message) ?></div><?php endif; ?>
|
||
<?php if ($error !== null): ?><div class="alert alert-error" role="alert"><?= dashboard_escape($error) ?></div><?php endif; ?>
|
||
<section class="data-section"><div class="section-heading"><div><p class="eyebrow">Neue Buchung</p><h2>Instanz anfragen</h2></div><span class="status-badge">S · M · L</span></div>
|
||
<form method="post" class="login-form"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="request_instance"><label for="name">Instanzname</label><input id="name" name="name" required maxlength="120" placeholder="Meine Spielrunde"><label for="plan">Tarif</label><select id="plan" name="plan"><option value="s">S – 512 MB RAM · 1 CPU · 1 GB</option><option value="m">M – 2 GB RAM · 2 CPU · 10 GB</option><option value="l">L – 4 GB RAM · 4 CPU · 25 GB</option></select><label for="payment_method">Zahlungsart</label><select id="payment_method" name="payment_method"><option value="paypal">PayPal</option><option value="bank_transfer">Überweisung</option></select><button type="submit">Instanz anfragen</button></form></section>
|
||
<section class="data-section"><div class="section-heading"><div><p class="eyebrow">Provisionierung</p><h2>Instanzen</h2></div><?php if ($isAdmin): ?><a href="dashboard.php">← Realms</a><?php endif; ?></div>
|
||
<?php if ($instances === []): ?><div class="empty-state">Noch keine Instanzen vorhanden.</div><?php else: ?><div class="table-wrap"><table class="data-table"><thead><tr><th>Name</th><?php if ($isAdmin): ?><th>Benutzer</th><?php endif; ?><th>Tarif</th><th>Zahlung</th><th>Status</th><th>Adresse</th><th>Aktion</th></tr></thead><tbody><?php foreach ($instances as $instance): ?><tr><td data-label="Name"><strong><?= dashboard_escape((string)$instance['name']) ?></strong></td><?php if ($isAdmin): ?><td data-label="Benutzer"><?= dashboard_escape((string)$instance['username']) ?></td><?php endif; ?><td data-label="Tarif"><?= strtoupper(dashboard_escape((string)$instance['plan'])) ?></td><td data-label="Zahlung"><?= dashboard_escape((string)$instance['payment_status']) ?></td><td data-label="Status"><span class="container-state <?= dashboard_escape((string)$instance['status']) ?>"><?= dashboard_escape(dashboard_status((string)$instance['status'])) ?></span><?php if (!empty($instance['error_message'])): ?><br><small><?= dashboard_escape((string)$instance['error_message']) ?></small><?php endif; ?></td><td data-label="Adresse"><?php if ($instance['status'] === 'running'): ?>:<?= dashboard_escape((string)$instance['port']) ?><?php else: ?>–<?php endif; ?></td><td data-label="Aktion"><?php if (in_array((string)$instance['status'], ['pending_payment', 'error'], true) && $isAdmin && ($instance['status'] === 'pending_payment' || $instance['payment_status'] === 'paid')): ?><form method="post"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="approve_instance"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit"><?= $instance['status'] === 'error' ? 'Erneut provisionieren' : 'Zahlung freigeben' ?></button></form><?php elseif (in_array($instance['status'], ['running', 'stopped'], true)): ?><div class="instance-actions"><form method="post" onsubmit="return confirm('Instanz wirklich neu bereitstellen? Die Daten in /data bleiben erhalten.');"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="redeploy"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Redeploy</button></form><?php if ($instance['status'] === 'running'): ?><form method="post"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="stop"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Stoppen</button></form><?php else: ?><form method="post"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="start"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Starten</button></form><?php endif; ?><form method="post"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="restart"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Neustarten</button></form><?php if ($isAdmin): ?><form method="post" onsubmit="return confirm('Instanz wirklich löschen? Das Datenverzeichnis bleibt erhalten.');"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="delete"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Löschen</button></form><?php endif; ?></div><?php else: ?>–<?php endif; ?></td></tr><?php endforeach; ?></tbody></table></div><?php endif; ?></section>
|
||
<?php endif; ?></main></body></html>
|