Merge pull request #141 from GameServerPanel/copilot/gsp-automatic-server-creation

Harden billing auto-provisioning (port/mod defaults + linkage) and streamline billing/admin UI workflows
This commit is contained in:
Frank Harris 2026-05-08 16:34:11 -05:00 committed by GitHub
commit daae48d9de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 481 additions and 81 deletions

View file

@ -1,6 +1,7 @@
# Changelog
## 2026-05-08
- **Provisioning + billing admin UX reliability pass:** Hardened automatic server provisioning to reserve ports from `arrange_ports` (exact `home_cfg_id`, then `home_cfg_id=0` fallback), prevent duplicate `home_ip_ports` assignment, keep order/invoice/home linkage intact even when install is pending, and apply safe default mod resolution so base installs are not blocked by missing explicit mod choices. Refreshed billing admin service management with row-level save actions, sortable columns (including Game Name enabled-first toggle), clearer save feedback, cleaner unstable-update caution styling, login theme polish, and updated storefront timestamp footer metadata.
- **Steam Workshop reliability + UI simplification:** Removed customer CLI/update scripting instructions from Workshop user flow, reduced per-server behavior options to supported modes only, switched remaining Steam Workshop SQL references to prefix helpers (no `OGP_DB_PREFIX` strings), hardened queued-update agent processing (`queued → updating → installed/failed`) with clearer error persistence, and refreshed monitor/support documentation links to open game-specific docs (fallback to docs index) in a new tab.
- **Billing docs routing refresh:** Updated docs browser links/icons to root-relative storefront paths (`/docs.php`, `/docs/...`) and removed stale hardcoded panel host guidance from getting-started documentation.

View file

@ -8,3 +8,4 @@
- Add an integration smoke test that exercises paid checkout, free checkout, and add-to-cart on installs with/without `period_start` to prevent billing schema drift regressions.
- Add a storefront visual-regression check at 375px and 430px breakpoints covering login, order, and cart pages to prevent mobile overflow regressions.
- Complete a full pass over all `modules/billing/docs/*` game guides to standardize OS/Workshop/RCON capability statements against current XML-backed server support.
- Add an automated billing provisioning integration test fixture that verifies arrange_ports exact/fallback allocation, duplicate-port protection, and home_id linkage after paid/free checkout.

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