From bb4fd8b44e713b7095f98310f4fb9eb39a35d789 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 21:24:27 +0000 Subject: [PATCH] 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> --- CHANGELOG.md | 1 + docs/COPILOT_TODO.md | 1 + modules/administration/panel_update.php | 5 +- modules/billing/adminserverlist.php | 193 +++++++++++-- modules/billing/create_servers.php | 344 ++++++++++++++++++++---- modules/billing/login.php | 16 +- modules/billing/timestamp.txt | 2 +- 7 files changed, 481 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f52143b..530024f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/COPILOT_TODO.md b/docs/COPILOT_TODO.md index 6986a0b7..08262d02 100644 --- a/docs/COPILOT_TODO.md +++ b/docs/COPILOT_TODO.md @@ -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. diff --git a/modules/administration/panel_update.php b/modules/administration/panel_update.php index ed87a164..3e4fb2ad 100644 --- a/modules/administration/panel_update.php +++ b/modules/administration/panel_update.php @@ -1302,8 +1302,9 @@ function gsp_panel_update_section() // ---- GitHub Unstable ----------------------------------------------------- echo "

GitHub Unstable

\n"; echo "

GitHub Unstable represents the latest development branch and may be unstable.

\n"; - echo "

" - . "⚠ Warning: GitHub Unstable may contain bugs or incomplete features. Use with caution in production.



\n"; + echo "

" + . "⚠ Cutting-edge updates may include unfinished changes. Use stable releases for production.


\n"; echo "
\n"; echo "\n"; echo "\n"; diff --git a/modules/billing/adminserverlist.php b/modules/billing/adminserverlist.php index afd4e033..b22367c3 100644 --- a/modules/billing/adminserverlist.php +++ b/modules/billing/adminserverlist.php @@ -6,9 +6,9 @@ Admin Service Configuration - GSP @@ -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; + }); +} ?> @@ -398,22 +527,47 @@ while ($svcRes && ($row = $svcRes->fetch_assoc())) { + + +
- - - + + + - - - + + + - + + @@ -541,6 +695,9 @@ while ($svcRes && ($row = $svcRes->fetch_assoc())) { + @@ -548,7 +705,7 @@ while ($svcRes && ($row = $svcRes->fetch_assoc())) {
- +
diff --git a/modules/billing/create_servers.php b/modules/billing/create_servers.php index 963832af..f304d505 100644 --- a/modules/billing/create_servers.php +++ b/modules/billing/create_servers.php @@ -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 "


".get_lang('starting_installations')."


"; - //PANEL LOG - $db->logger( "CREATED NEW SERVER " . $home_id); + if (!$order_failed) { + echo "


".get_lang('starting_installations')."


"; + //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 "

Server Provisioning Complete

"; echo "

Successfully provisioned $provisioned_count server(s). Your server(s) are now active.

"; echo ""; + if ($failed_count > 0) { + echo "
"; + echo "

{$failed_count} order(s) were linked but left pending install:

    "; + foreach ((array)$failed_messages as $failed_message) { + echo "
  • " . htmlspecialchars($failed_message, ENT_QUOTES, 'UTF-8') . "
  • "; + } + echo "
"; + } echo "

View My Servers

"; // Auto-redirect after 3 seconds echo ""; } else { - echo "
"; - echo "

No servers to provision. All orders have already been processed.

"; - echo "
"; + if ($failed_count > 0) { + echo "

No servers were auto-installed. Orders are active but pending install:

    "; + foreach ((array)$failed_messages as $failed_message) { + echo "
  • " . htmlspecialchars($failed_message, ENT_QUOTES, 'UTF-8') . "
  • "; + } + echo "
"; + } else { + echo "
"; + echo "

No servers to provision. All orders have already been processed.

"; + echo "
"; + } echo "

View My Orders

"; } @@ -445,5 +687,3 @@ function exec_ogp_module() ); } ?> - - diff --git a/modules/billing/login.php b/modules/billing/login.php index 2092f987..6faa6d41 100644 --- a/modules/billing/login.php +++ b/modules/billing/login.php @@ -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; } diff --git a/modules/billing/timestamp.txt b/modules/billing/timestamp.txt index abf44559..cca16ba0 100644 --- a/modules/billing/timestamp.txt +++ b/modules/billing/timestamp.txt @@ -1 +1 @@ -Last Updated at 12:47pm on 2026-05-08 +Last Updated at 9:18pm on 2026-08-05
Game NameConfig XMLEnabled + + Game Name + + + Config XML + + + Enabled + Min Slots Max SlotsPrice / Day ($)Price / Month ($)Price / Year ($) + + Price / Day ($) + + + Price / Month ($) + + + Price / Year ($) + Description ImageAvailable Servers + + Available Servers + Action
+ +