feat: harden billing provisioning and admin service UI workflows

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/6640cb6b-0d5a-4c91-bfaf-86dd1b71f701

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-08 21:24:27 +00:00 committed by GitHub
parent 87f1a943ec
commit bb4fd8b44e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 481 additions and 81 deletions

View file

@ -1302,8 +1302,9 @@ function gsp_panel_update_section()
// ---- GitHub Unstable -----------------------------------------------------
echo "<h3>GitHub Unstable</h3>\n";
echo "<p>GitHub Unstable represents the latest development branch and may be unstable.</p>\n";
echo "<p class='failure' style='display:inline-block;padding:5px 10px;'>"
. "&#9888; Warning: GitHub Unstable may contain bugs or incomplete features. Use with caution in production.</p><br><br>\n";
echo "<p style='display:inline-block;margin:4px 0 10px;padding:6px 10px;border-radius:6px;"
. "border:1px solid #d9b55a;background:#fff8e6;color:#6b5420;font-size:0.92em;'>"
. "&#9888; Cutting-edge updates may include unfinished changes. Use stable releases for production.</p><br>\n";
echo "<form method='POST'>\n";
echo "<input type='hidden' name='gsp_update_action' value='update_unstable'>\n";
echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n";

View file

@ -6,9 +6,9 @@
<title>Admin Service Configuration - GSP</title>
<style>
.svc-table { border-collapse: collapse; width: 100%; }
.svc-table th, .svc-table td { border: 1px solid #4a6080; padding: 6px 8px; vertical-align: middle; }
.svc-table th, .svc-table td { border: 1px solid rgba(86,105,130,0.6); padding: 8px 10px; vertical-align: middle; }
/* Sticky header: stays visible while scrolling; dark background with light text for readability */
.svc-table thead th { position: sticky; top: 0; z-index: 10; background: #2c3e50; color: #f0f0f0; white-space: nowrap; text-align: center; }
.svc-table thead th { position: sticky; top: 0; z-index: 10; background: #26354a; color: #f0f0f0; white-space: nowrap; text-align: center; }
.svc-table thead th.game-name { text-align: left; }
.svc-table td.game-name { text-align: left; white-space: nowrap; }
.price-input { width: 80px; }
@ -19,10 +19,28 @@
.img-fallback { display: none; max-width: 180px; margin-top: 4px; }
.img-fallback.img-fallback-visible { display: block; }
.muted { color: #999; font-size: 0.85em; }
.flash-ok { background: #d4edda; border: 1px solid #c3e6cb; padding: 8px 12px; margin-bottom: 10px; border-radius: 4px; color: #155724; }
.flash-err { background: #f8d7da; border: 1px solid #f5c6cb; padding: 8px 12px; margin-bottom: 10px; border-radius: 4px; color: #721c24; }
.flash-ok { background: #d4edda; border: 1px solid #c3e6cb; padding: 10px 12px; margin-bottom: 10px; border-radius: 6px; color: #155724; }
.flash-err { background: #f8d7da; border: 1px solid #f5c6cb; padding: 10px 12px; margin-bottom: 10px; border-radius: 6px; color: #721c24; }
.servers-cell { text-align: left; }
.server-cb-label { display: block; white-space: nowrap; margin: 2px 0; }
.action-cell { text-align: center; min-width: 120px; }
.btn-row-save, .btn-save-all {
border: 1px solid #3e7ab8;
border-radius: 6px;
background: #2f6dac;
color: #fff;
font-weight: 600;
padding: 6px 10px;
cursor: pointer;
}
.btn-save-all {
padding: 9px 14px;
font-size: 0.95rem;
}
.btn-row-save:hover, .btn-save-all:hover { background: #25598d; }
.sort-link { color: #d8e7ff; text-decoration: none; display: inline-flex; align-items: center; gap: 4px; }
.sort-link:hover { text-decoration: underline; }
.sort-active { color: #ffffff; font-weight: 700; }
</style>
</head>
<body>
@ -53,6 +71,10 @@
require_once(__DIR__ . '/bootstrap.php');
require_once(__DIR__ . '/includes/admin_auth.php');
if (session_status() === PHP_SESSION_NONE) {
session_name('opengamepanel_web');
session_start();
}
function h(mixed $s): string
{
@ -285,13 +307,47 @@ $syncMessages = sync_billing_services($db, $table_prefix);
$flash = [];
$flashType = 'ok';
$sort = strtolower((string)($_GET['sort'] ?? $_POST['sort'] ?? 'game'));
$dir = strtolower((string)($_GET['dir'] ?? $_POST['dir'] ?? 'asc')) === 'desc' ? 'desc' : 'asc';
$gameMode = strtolower((string)($_GET['game_mode'] ?? $_POST['game_mode'] ?? 'name'));
if (!in_array($sort, ['game', 'config', 'enabled', 'day', 'month', 'year', 'servers'], true)) {
$sort = 'game';
}
if (!in_array($gameMode, ['name', 'enabled'], true)) {
$gameMode = 'name';
}
$sortQuery = http_build_query([
'sort' => $sort,
'dir' => $dir,
'game_mode' => $gameMode,
]);
function sort_link_params(string $column, string $sort, string $dir, string $gameMode): array
{
$nextDir = ($sort === $column && $dir === 'asc') ? 'desc' : 'asc';
$nextGameMode = $gameMode;
if ($column === 'game' && $sort === 'game' && $gameMode === 'name') {
$nextGameMode = 'enabled';
$nextDir = 'asc';
} elseif ($column === 'game' && $sort === 'game' && $gameMode === 'enabled') {
$nextGameMode = 'name';
$nextDir = 'asc';
} elseif ($column !== 'game') {
$nextGameMode = 'name';
}
return [
'sort' => $column,
'dir' => $nextDir,
'game_mode' => $nextGameMode,
];
}
/* -----------------------------------------------------------------------
SAVE: service configuration form submitted
Only admin-editable fields are updated; service_name and home_cfg_id
are never overwritten here.
----------------------------------------------------------------------- */
if (isset($_POST['save_services'])) {
if (isset($_POST['save_services']) || isset($_POST['save_row'])) {
// Load valid remote server IDs for validation
$validServerIds = [];
$rsRes = $db->query("SELECT remote_server_id FROM `{$table_prefix}remote_servers`");
@ -302,9 +358,14 @@ if (isset($_POST['save_services'])) {
$postedServices = $_POST['svc'] ?? [];
$postedServers = $_POST['servers'] ?? [];
$rowOnlyServiceId = isset($_POST['save_row']) ? (int)$_POST['save_row'] : 0;
$updatedCount = 0;
foreach ((array)$postedServices as $sid => $svcData) {
$sid = (int)$sid;
if ($rowOnlyServiceId > 0 && $sid !== $rowOnlyServiceId) {
continue;
}
$enabled = isset($svcData['enabled']) ? 1 : 0;
$priceDaily = number_format((float)($svcData['price_daily'] ?? 0), 2, '.', '');
$priceMonthly = number_format((float)($svcData['price_monthly'] ?? 0), 2, '.', '');
@ -332,7 +393,7 @@ if (isset($_POST['save_services'])) {
}
$remoteServerIdStr = $db->real_escape_string(implode(',', $checkedIds));
$db->query(
$ok = $db->query(
"UPDATE `{$table_prefix}billing_services`
SET enabled = {$enabled},
price_daily = '{$priceDaily}',
@ -342,12 +403,38 @@ if (isset($_POST['save_services'])) {
slot_max_qty = {$slotMax},
description = '{$description}',
img_url = '{$imgUrl}',
remote_server_id = '{$remoteServerIdStr}'
WHERE service_id = {$sid}"
remote_server_id = '{$remoteServerIdStr}'
WHERE service_id = {$sid}"
);
if ($ok) {
$updatedCount++;
}
}
$flash[] = "Services saved.";
if ($updatedCount > 0) {
if ($rowOnlyServiceId > 0) {
$flash[] = "Service row #{$rowOnlyServiceId} saved.";
} else {
$flash[] = "{$updatedCount} service row(s) saved.";
}
} else {
$flashType = 'err';
if ($rowOnlyServiceId > 0) {
$flash[] = "No changes were saved for service row #{$rowOnlyServiceId}.";
} else {
$flash[] = "No service rows were updated.";
}
}
$_SESSION['billing_adminserverlist_flash'] = ['type' => $flashType, 'messages' => $flash];
header("Location: /adminserverlist.php?{$sortQuery}");
exit;
}
if (!empty($_SESSION['billing_adminserverlist_flash'])) {
$flashData = $_SESSION['billing_adminserverlist_flash'];
unset($_SESSION['billing_adminserverlist_flash']);
$flashType = ($flashData['type'] ?? 'ok') === 'err' ? 'err' : 'ok';
$flash = array_values(array_filter((array)($flashData['messages'] ?? []), 'is_string'));
}
/* -----------------------------------------------------------------------
@ -371,12 +458,54 @@ $svcRes = $db->query(
bs.remote_server_id, bs.description, bs.img_url,
ch.home_cfg_file
FROM `{$table_prefix}billing_services` bs
LEFT JOIN `{$table_prefix}config_homes` ch ON ch.home_cfg_id = bs.home_cfg_id
LEFT JOIN `{$table_prefix}config_homes` ch ON ch.home_cfg_id = bs.home_cfg_id
ORDER BY bs.service_name"
);
while ($svcRes && ($row = $svcRes->fetch_assoc())) {
$services[] = $row;
}
if (!empty($services)) {
usort($services, function (array $a, array $b) use ($sort, $dir, $gameMode): int {
$cmp = 0;
switch ($sort) {
case 'config':
$cmp = strcasecmp((string)($a['home_cfg_file'] ?? ''), (string)($b['home_cfg_file'] ?? ''));
break;
case 'enabled':
$cmp = ((int)($a['enabled'] ?? 0)) <=> ((int)($b['enabled'] ?? 0));
break;
case 'day':
$cmp = ((float)($a['price_daily'] ?? 0)) <=> ((float)($b['price_daily'] ?? 0));
break;
case 'month':
$cmp = ((float)($a['price_monthly'] ?? 0)) <=> ((float)($b['price_monthly'] ?? 0));
break;
case 'year':
$cmp = ((float)($a['price_year'] ?? 0)) <=> ((float)($b['price_year'] ?? 0));
break;
case 'servers':
$countA = trim((string)($a['remote_server_id'] ?? '')) === '' ? 0 : count(array_filter(explode(',', (string)$a['remote_server_id']), 'strlen'));
$countB = trim((string)($b['remote_server_id'] ?? '')) === '' ? 0 : count(array_filter(explode(',', (string)$b['remote_server_id']), 'strlen'));
$cmp = $countA <=> $countB;
break;
case 'game':
default:
if ($gameMode === 'enabled') {
$cmp = ((int)($b['enabled'] ?? 0)) <=> ((int)($a['enabled'] ?? 0));
if ($cmp === 0) {
$cmp = strcasecmp((string)($a['service_name'] ?? ''), (string)($b['service_name'] ?? ''));
}
} else {
$cmp = strcasecmp((string)($a['service_name'] ?? ''), (string)($b['service_name'] ?? ''));
}
break;
}
if ($cmp === 0) {
$cmp = ((int)($a['service_id'] ?? 0)) <=> ((int)($b['service_id'] ?? 0));
}
return $dir === 'desc' ? -$cmp : $cmp;
});
}
?>
<?php foreach (array_merge((array)$syncMessages, (array)$flash) as $msg): ?>
@ -398,22 +527,47 @@ while ($svcRes && ($row = $svcRes->fetch_assoc())) {
<form method="post" action="">
<input type="hidden" name="save_services" value="1">
<input type="hidden" name="sort" value="<?php echo h($sort); ?>">
<input type="hidden" name="dir" value="<?php echo h($dir); ?>">
<input type="hidden" name="game_mode" value="<?php echo h($gameMode); ?>">
<div style="overflow-x:auto;">
<table class="svc-table">
<thead>
<tr>
<th class="game-name">Game Name</th>
<th>Config XML</th>
<th>Enabled</th>
<th class="game-name">
<?php $p = sort_link_params('game', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'game' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Game Name</a>
</th>
<th>
<?php $p = sort_link_params('config', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'config' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Config XML</a>
</th>
<th>
<?php $p = sort_link_params('enabled', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'enabled' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Enabled</a>
</th>
<th>Min Slots</th>
<th>Max Slots</th>
<th>Price / Day ($)</th>
<th>Price / Month ($)</th>
<th>Price / Year ($)</th>
<th>
<?php $p = sort_link_params('day', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'day' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Price / Day ($)</a>
</th>
<th>
<?php $p = sort_link_params('month', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'month' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Price / Month ($)</a>
</th>
<th>
<?php $p = sort_link_params('year', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'year' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Price / Year ($)</a>
</th>
<th>Description</th>
<th>Image</th>
<th>Available Servers</th>
<th>
<?php $p = sort_link_params('servers', $sort, $dir, $gameMode); ?>
<a class="sort-link <?php echo $sort === 'servers' ? 'sort-active' : ''; ?>" href="/adminserverlist.php?<?php echo h(http_build_query($p)); ?>">Available Servers</a>
</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@ -541,6 +695,9 @@ while ($svcRes && ($row = $svcRes->fetch_assoc())) {
<?php endforeach; ?>
<?php endif; ?>
</td>
<td class="action-cell">
<button type="submit" class="btn-row-save" name="save_row" value="<?php echo $sid; ?>">Save Row</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
@ -548,7 +705,7 @@ while ($svcRes && ($row = $svcRes->fetch_assoc())) {
</div>
<div style="margin-top:14px;">
<button type="submit">Save Services</button>
<button type="submit" class="btn-save-all">Save All Services</button>
</div>
</form>

View file

@ -27,6 +27,137 @@ if (!function_exists('billing_invoke_provision')) {
}
}
if (!function_exists('billing_get_remote_ip_ids')) {
function billing_get_remote_ip_ids($db, string $db_prefix, int $remote_server_id): array
{
$rows = $db->resultQuery(
"SELECT ip_id FROM `{$db_prefix}remote_server_ips` WHERE remote_server_id=" . $db->realEscapeSingle($remote_server_id) . " ORDER BY ip_id ASC"
);
$ipIds = array();
foreach ((array)$rows as $row) {
$ipId = intval($row['ip_id'] ?? 0);
if ($ipId > 0) {
$ipIds[] = $ipId;
}
}
return $ipIds;
}
}
if (!function_exists('billing_allocate_home_port')) {
function billing_allocate_home_port($db, string $db_prefix, int $home_id, int $remote_server_id, int $home_cfg_id): array
{
$ipIds = billing_get_remote_ip_ids($db, $db_prefix, $remote_server_id);
if (empty($ipIds)) {
return array('ok' => false, 'error' => "No IP addresses are configured for remote server #{$remote_server_id}.");
}
foreach ($ipIds as $ipId) {
$ranges = $db->resultQuery(
"SELECT start_port, end_port, port_increment
FROM `{$db_prefix}arrange_ports`
WHERE ip_id=" . $db->realEscapeSingle($ipId) . "
AND home_cfg_id=" . $db->realEscapeSingle($home_cfg_id) . "
ORDER BY range_id ASC"
);
if (empty($ranges)) {
$ranges = $db->resultQuery(
"SELECT start_port, end_port, port_increment
FROM `{$db_prefix}arrange_ports`
WHERE ip_id=" . $db->realEscapeSingle($ipId) . "
AND home_cfg_id=0
ORDER BY range_id ASC"
);
}
if (empty($ranges)) {
continue;
}
$usedRows = $db->resultQuery(
"SELECT port FROM `{$db_prefix}home_ip_ports` WHERE ip_id=" . $db->realEscapeSingle($ipId)
);
$usedPorts = array();
foreach ((array)$usedRows as $usedRow) {
$usedPorts[intval($usedRow['port'] ?? 0)] = true;
}
foreach ((array)$ranges as $range) {
$start = intval($range['start_port'] ?? 0);
$end = intval($range['end_port'] ?? 0);
$increment = max(1, intval($range['port_increment'] ?? 1));
if ($start <= 0 || $end <= 0 || $start > $end) {
continue;
}
for ($port = $start; $port <= $end; $port += $increment) {
if (isset($usedPorts[$port])) {
continue;
}
$safeIpId = $db->realEscapeSingle($ipId);
$safePort = $db->realEscapeSingle($port);
$safeHome = $db->realEscapeSingle($home_id);
$insertOk = $db->query(
"INSERT INTO `{$db_prefix}home_ip_ports` (`ip_id`, `port`, `home_id`)
SELECT {$safeIpId}, {$safePort}, {$safeHome}
FROM DUAL
WHERE NOT EXISTS (
SELECT 1
FROM `{$db_prefix}home_ip_ports`
WHERE ip_id = {$safeIpId}
AND port = {$safePort}
)"
);
if (!$insertOk) {
continue;
}
$verify = $db->resultQuery(
"SELECT home_id FROM `{$db_prefix}home_ip_ports`
WHERE ip_id = {$safeIpId}
AND port = {$safePort}
AND home_id = {$safeHome}
LIMIT 1"
);
if (!empty($verify)) {
return array('ok' => true, 'ip_id' => $ipId, 'port' => intval($port));
}
}
}
}
return array('ok' => false, 'error' => "No available port in arrange_ports for remote server #{$remote_server_id} and home_cfg_id #{$home_cfg_id}.");
}
}
if (!function_exists('billing_resolve_mod_cfg_id')) {
function billing_resolve_mod_cfg_id($db, int $home_cfg_id, int $preferred_mod_cfg_id): array
{
$mods = $db->getCfgMods($home_cfg_id);
if (empty($mods)) {
return array('ok' => false, 'error' => "No config_mods rows found for home_cfg_id #{$home_cfg_id}.");
}
$first = null;
foreach ((array)$mods as $mod) {
$modCfgId = intval($mod['mod_cfg_id'] ?? 0);
if ($modCfgId <= 0) {
continue;
}
if ($first === null) {
$first = $modCfgId;
}
if ($preferred_mod_cfg_id > 0 && $modCfgId === $preferred_mod_cfg_id) {
return array('ok' => true, 'mod_cfg_id' => $modCfgId);
}
}
if ($first !== null) {
return array('ok' => true, 'mod_cfg_id' => $first);
}
return array('ok' => false, 'error' => "No usable mod_cfg_id found for home_cfg_id #{$home_cfg_id}.");
}
}
function exec_ogp_module()
{
global $db,$view,$settings,$table_prefix;
@ -84,9 +215,13 @@ function exec_ogp_module()
{
$provisioned_count = 0;
$failed_count = 0;
$failed_messages = array();
foreach ((array)$orders as $order)
{
$home_id = 0;
$order_failed = false;
$order_failure_reason = '';
$end_date = null;
$end_date_str = null;
$order_id = $order['order_id'];
@ -125,13 +260,16 @@ function exec_ogp_module()
$access_rights = $service[0]['access_rights'];
}
else
return;
{
$order_failed = true;
$order_failure_reason = "Service ID {$service_id} not found.";
}
if($alreadyProvisioned)
if(!$order_failed && $alreadyProvisioned)
{
$home_id = intval($order['home_id']);
}
elseif($extended)
elseif(!$order_failed && $extended)
{
$home_id = $order['home_id'];
@ -177,7 +315,7 @@ function exec_ogp_module()
//end WEBHOOK Discord
}
else
elseif(!$order_failed)
{
//OPTIONS, change it at your choice;
$extra_params = "";//no extra params defined by default
@ -189,59 +327,125 @@ function exec_ogp_module()
$rserver = $db->getRemoteServer($remote_server_id);
$game_path = "/home/gameserver/";
$home_id = $db->addGameHome( $remote_server_id, $user_id, $home_cfg_id, $game_path, $home_name, $remote_control_password, $ftp_password);
if (!$home_id || intval($home_id) <= 0) {
$order_failed = true;
$order_failure_reason = "Could not create server_homes row for order #{$order_id}.";
}
//Add IP:Port Pair to the Game Home
//need to get the IP_ID for this remote server.
$result = $db->resultQuery("SELECT ip_id FROM `{$db_prefix}remote_server_ips` WHERE remote_server_id=".$ip);
foreach ((array)$result as $rs)
{
$ip_id = $rs['ip_id'];
}
$add_port = $db->addGameIpPort( $home_id, $ip_id, $db->getNextAvailablePort($ip_id,$home_cfg_id) );
// Add IP:Port pair with arrange_ports exact home_cfg_id preference and home_cfg_id=0 fallback.
if (!$order_failed) {
$allocatedPort = billing_allocate_home_port($db, $db_prefix, intval($home_id), intval($remote_server_id), intval($home_cfg_id));
if (empty($allocatedPort['ok'])) {
$order_failed = true;
$order_failure_reason = (string)($allocatedPort['error'] ?? 'Port allocation failed.');
$db->logger("Provisioning pending install for order #{$order_id}: {$order_failure_reason}");
}
}
//Assign the Game Mod to the Game Home
$mod_id = $db->addModToGameHome( $home_id, $mod_cfg_id );
$db->updateGameModParams( $max_players, $extra_params, $cpu_affinity, $nice, $home_id, $mod_cfg_id );
$db->assignHomeTo( "user", $user_id, $home_id, $access_rights );
$resolved_mod_cfg_id = intval($mod_cfg_id);
if (!$order_failed) {
$modResolution = billing_resolve_mod_cfg_id($db, intval($home_cfg_id), intval($mod_cfg_id));
if (empty($modResolution['ok'])) {
$order_failed = true;
$order_failure_reason = (string)($modResolution['error'] ?? 'No mod profile available for base install.');
} else {
$resolved_mod_cfg_id = intval($modResolution['mod_cfg_id']);
}
}
$mod_id = false;
if (!$order_failed) {
$mod_id = $db->addModToGameHome( $home_id, $resolved_mod_cfg_id );
if ($mod_id === false) {
$order_failed = true;
$order_failure_reason = "Could not attach mod_cfg_id {$resolved_mod_cfg_id} to home #{$home_id}.";
}
}
if (!$order_failed) {
$db->updateGameModParams( $max_players, $extra_params, $cpu_affinity, $nice, $home_id, $resolved_mod_cfg_id );
$db->assignHomeTo( "user", $user_id, $home_id, $access_rights );
}
//Get The home info without mods in 1 array (Necesary for remote connection).
$home_info = $db->getGameHomeWithoutMods($home_id);
if (!$order_failed) {
$home_info = $db->getGameHomeWithoutMods($home_id);
if (empty($home_info)) {
$order_failed = true;
$order_failure_reason = "Could not load home info for home #{$home_id}.";
}
}
//Create the remote connection
$remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']);
if (!$order_failed) {
$remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']);
}
//Get Full home info in 1 array
$home_info = $db->getGameHome($home_id);
if (!$order_failed) {
$home_info = $db->getGameHome($home_id);
if (empty($home_info) || empty($home_info['mods'])) {
$order_failed = true;
$order_failure_reason = "Mods are not configured for home #{$home_id}; base install profile could not be resolved.";
}
}
//Read the Game Config from the XML file
$server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_info['home_cfg_file']);
if (!$order_failed) {
$server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_info['home_cfg_file']);
if ($server_xml === false) {
$order_failed = true;
$order_failure_reason = "Could not read server XML for home #{$home_id}.";
}
}
//Get Values from XML
$modkey = $home_info['mods'][$mod_id]['mod_key'];
$mod_xml = xml_get_mod($server_xml, $modkey);
$installer_name = $mod_xml->installer_name;
$mod_cfg_id = $home_info['mods'][$mod_id]['mod_cfg_id'];
$mod_xml = false;
$modkey = '';
$installer_name = '';
if (!$order_failed) {
$selected_mod = $home_info['mods'][$mod_id] ?? reset($home_info['mods']);
if (empty($selected_mod) || empty($selected_mod['mod_key'])) {
$order_failed = true;
$order_failure_reason = "No valid mod profile found for home #{$home_id}.";
} else {
$modkey = (string)$selected_mod['mod_key'];
$mod_xml = xml_get_mod($server_xml, $modkey);
if ($mod_xml === false && isset($server_xml->mods->mod[0])) {
$mod_xml = $server_xml->mods->mod[0];
$modkey = (string)$mod_xml['key'];
}
if ($mod_xml === false) {
$order_failed = true;
$order_failure_reason = "No installable mod profile exists in XML for home #{$home_id}.";
} else {
$installer_name = (string)$mod_xml->installer_name;
$resolved_mod_cfg_id = intval($selected_mod['mod_cfg_id'] ?? $resolved_mod_cfg_id);
}
}
}
//Get Preinstall commands from xml
$precmd = $server_xml->pre_install;
$precmd = !$order_failed ? $server_xml->pre_install : '';
//Get Postinstall commands from xml
$postcmd = $server_xml->post_install;
$postcmd = !$order_failed ? $server_xml->post_install : '';
//Enable FTP account in remote server
if ($ftp == "enabled")
if (!$order_failed && $ftp == "enabled")
{
$remote->ftp_mgr("useradd", $home_info['home_id'], $home_info['ftp_password'], $home_info['home_path']);
$db->changeFtpStatus('enabled',$home_info['home_id']);
}
//Install files for this service in the remote server
$exec_folder_path = clean_path($home_info['home_path'] . "/" . $server_xml->exe_location );
$exec_path = clean_path($exec_folder_path . "/" . $server_xml->server_exec_name );
if (!$order_failed) {
$exec_folder_path = clean_path($home_info['home_path'] . "/" . $server_xml->exe_location );
$exec_path = clean_path($exec_folder_path . "/" . $server_xml->server_exec_name );
}
if ( (string)$server_xml->installer === "steamcmd" && !empty((string)$installer_name) )
if (!$order_failed && (string)$server_xml->installer === "steamcmd" && !empty((string)$installer_name) )
{
if( preg_match("/win32/", $server_xml->game_key) OR preg_match("/win64/", $server_xml->game_key) )
$cfg_os = "windows";
@ -249,7 +453,7 @@ function exec_ogp_module()
$cfg_os = "linux";
// Some games like L4D2 require anonymous login
if($mod_xml->installer_login){
if(!empty($mod_xml->installer_login)){
$login = $mod_xml->installer_login;
$pass = '';
}else{
@ -266,7 +470,7 @@ function exec_ogp_module()
$betaname,$betapwd,$login,$pass,$settings['steam_guard'],
$exec_folder_path,$exec_path,$precmd,$postcmd,$cfg_os,'',$arch);
}
else
elseif (!$order_failed)
{
// No SteamCMD installer — run pre/post install scripts only.
if (!empty((string)$precmd)) {
@ -280,11 +484,13 @@ function exec_ogp_module()
$db->logger("Script-only install: post_install script returned no output for home_id $home_id");
}
}
echo "<h4><br><p>".get_lang('starting_installations')."</p></h4><br>";
//PANEL LOG
$db->logger( "CREATED NEW SERVER " . $home_id);
if (!$order_failed) {
echo "<h4><br><p>".get_lang('starting_installations')."</p></h4><br>";
//PANEL LOG
$db->logger( "CREATED NEW SERVER " . $home_id);
}
// SEND EMAIL to new server only
if($order['end_date'] == 0){
if(!$order_failed && $order['end_date'] == 0){
$settings = $db->getSettings();
$subject = "New Gameserver installed at " . $settings['panel_name'];
$email = $db->resultQuery(" SELECT DISTINCT users_email
@ -376,7 +582,14 @@ function exec_ogp_module()
$end_date_str = date('Y-m-d H:i:s', $end_date);
}
// Set order status to 'Active' (server provisioned and current)
if ($home_id <= 0) {
$order_failed = true;
if ($order_failure_reason === '') {
$order_failure_reason = "No home_id was produced for order #{$order_id}.";
}
}
// Set order status to 'Active' (billing active even if install is pending)
$db->query("UPDATE `{$db_prefix}billing_orders`
SET status='Active'
WHERE order_id=".$db->realEscapeSingle($order_id));
@ -394,25 +607,38 @@ function exec_ogp_module()
$db->query("UPDATE `{$db_prefix}billing_invoices`
SET home_id=" . $db->realEscapeSingle($home_id) . ",
billing_status='Active'
billing_status='Active',
status='paid'
WHERE order_id=" . $db->realEscapeSingle($order_id));
$db->query("UPDATE `{$db_prefix}billing_transactions`
SET home_id=" . $db->realEscapeSingle($home_id) . "
WHERE invoice_id IN (SELECT invoice_id FROM `{$db_prefix}billing_invoices` WHERE order_id=" . $db->realEscapeSingle($order_id) . ")");
// Set billing_status and next_invoice_date on server_homes
$db->query("UPDATE `{$db_prefix}server_homes`
SET billing_status = 'Active',
next_invoice_date = '" . $db->realEscapeSingle($end_date_str) . "',
billing_enabled = 1
WHERE home_id = " . $db->realEscapeSingle($home_id));
$provisioned_count++;
if ($home_id > 0) {
$db->query("UPDATE `{$db_prefix}game_mods`
SET max_players=" . $db->realEscapeSingle($max_players) . "
WHERE home_id=" . $db->realEscapeSingle($home_id));
}
if ($home_id > 0) {
// Set billing_status and next_invoice_date on server_homes
$db->query("UPDATE `{$db_prefix}server_homes`
SET billing_status = 'Active',
next_invoice_date = '" . $db->realEscapeSingle($end_date_str) . "',
billing_enabled = 1
WHERE home_id = " . $db->realEscapeSingle($home_id));
}
if ($order_failed) {
$failed_count++;
$failed_messages[] = "Order #{$order_id}: {$order_failure_reason}";
$db->logger("Provisioning pending install for order #{$order_id}: {$order_failure_reason}");
} else {
$provisioned_count++;
}
}
$db->query( "UPDATE `{$db_prefix}game_mods` SET max_players= ".$order['max_players']." WHERE home_id=".$db->realEscapeSingle($home_id));
// Show results and redirect
if ($provisioned_count > 0) {
@ -420,13 +646,29 @@ function exec_ogp_module()
echo "<h3>Server Provisioning Complete</h3>";
echo "<p>Successfully provisioned $provisioned_count server(s). Your server(s) are now active.</p>";
echo "</div>";
if ($failed_count > 0) {
echo "<div class='failure'>";
echo "<p>{$failed_count} order(s) were linked but left pending install:</p><ul>";
foreach ((array)$failed_messages as $failed_message) {
echo "<li>" . htmlspecialchars($failed_message, ENT_QUOTES, 'UTF-8') . "</li>";
}
echo "</ul></div>";
}
echo "<p><a href='home.php?m=gamemanager&p=game_monitor' class='btn'>View My Servers</a></p>";
// Auto-redirect after 3 seconds
echo "<script>setTimeout(function(){ window.location.href='home.php?m=gamemanager&p=game_monitor'; }, 3000);</script>";
} else {
echo "<div class='info'>";
echo "<p>No servers to provision. All orders have already been processed.</p>";
echo "</div>";
if ($failed_count > 0) {
echo "<div class='failure'><p>No servers were auto-installed. Orders are active but pending install:</p><ul>";
foreach ((array)$failed_messages as $failed_message) {
echo "<li>" . htmlspecialchars($failed_message, ENT_QUOTES, 'UTF-8') . "</li>";
}
echo "</ul></div>";
} else {
echo "<div class='info'>";
echo "<p>No servers to provision. All orders have already been processed.</p>";
echo "</div>";
}
echo "<p><a href='home.php?m=billing&p=my_orders' class='btn'>View My Orders</a></p>";
}
@ -445,5 +687,3 @@ function exec_ogp_module()
);
}
?>

View file

@ -149,7 +149,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: radial-gradient(circle at top, #1f3551 0%, #0f1724 58%, #0b111b 100%);
min-height: 100vh;
display: block;
margin: 0;
@ -165,13 +165,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
.login-container {
background: #ffffff; /* explicit white */
background: #f8fbff;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.28);
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
width: 100%;
max-width: 420px;
padding: 32px 28px;
border: 1px solid rgba(0,0,0,0.06);
border: 1px solid rgba(40,70,110,0.25);
}
.login-header {
@ -223,7 +223,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
.btn-login {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #3168a4 0%, #214978 100%);
color: white;
border: none;
border-radius: 8px;
@ -235,7 +235,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
box-shadow: 0 6px 20px rgba(38, 84, 136, 0.4);
}
.btn-login:active {
@ -268,7 +268,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
.footer-links a {
color: #667eea;
color: #3168a4;
text-decoration: none;
font-size: 0.9rem;
}
@ -295,7 +295,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
.login-links a {
color: #667eea;
color: #3168a4;
text-decoration: none;
}

View file

@ -1 +1 @@
Last Updated at 12:47pm on 2026-05-08
Last Updated at 9:18pm on 2026-08-05