diff --git a/CHANGELOG.md b/CHANGELOG.md index 64cd67f9..a73e9945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## 2026-05-09 +- **Mandatory provisioning trace log + existing-home install retry:** Added visible fail-closed trace logging to `modules/billing/logs/provisioning_trace.log`, threaded detailed per-order provisioning results into checkout success flows, and changed billing provisioning so `Active` orders with an existing `home_id` only skip when the install is already complete. Incomplete existing homes now keep allocating missing IP/mod data and re-use `gamemanager_trigger_update_install()` with traced inputs/outputs instead of silently requiring a manual Game Monitor update. - **Billing provisioning auto-run + idempotency hardening:** Updated paid capture, free checkout, PayPal webhook renewals, and admin add-home registration to always hand activated orders to `billing_invoke_provision()` in trusted internal context. `modules/billing/create_servers.php` now enforces 6-character alphanumeric default passwords, defaults new homes to `ftp_status=1`, adds stricter service/node/port/mod validation logging, retries existing `home_id` installs only when incomplete, and skips already-installed homes to prevent duplicate provisioning on refresh/retry paths. - **Game Monitor IP:PORT fallback reliability:** `modules/gamemanager/server_monitor.php` now resolves missing display/connect endpoints from `home_ip_ports` + `remote_server_ips` for each home so newly provisioned servers consistently show IP:PORT even when initial joined row data is incomplete. - **Billing docs + auto-provision/install reliability:** Updated Game Monitor and Support documentation links to always open `https://gameservers.world/docs/` (game-specific path when available) in a new tab, and hardened billing provisioning so existing `home_id` orders can retry install automatically via the shared gamemanager update trigger (no manual Update Server click required). Added structured provisioning logs (`modules/billing/logs/provisioning.log`) and enriched panel log context with order/invoice/user/home/home_cfg/mod/ip/port/mechanism/result/error fields; PayPal webhook renewals now auto-provision when an order still has no `home_id`. diff --git a/docs/COPILOT_TODO.md b/docs/COPILOT_TODO.md index fb603535..f881dbde 100644 --- a/docs/COPILOT_TODO.md +++ b/docs/COPILOT_TODO.md @@ -12,3 +12,4 @@ - Add a billing UI badge/filter that distinguishes "pending install" vs "installed" states directly in customer/server order views. - Add an admin billing orders "provisioning details" drawer that reads `modules/billing/logs/provisioning.log` and shows the latest mechanism/result/error per order without leaving the panel. - Add an automated end-to-end check that verifies `create_servers.php` skips already-installed homes while still retrying existing-home orders with missing executable/IP-port/mod prerequisites. +- Add a repeatable QA fixture that exercises `modules/billing/logs/provisioning_trace.log` writability failures and verifies payment success pages surface the traced provision result for paid and free orders. diff --git a/modules/billing/api/capture_order.php b/modules/billing/api/capture_order.php index 86203ff2..1f1b0538 100644 --- a/modules/billing/api/capture_order.php +++ b/modules/billing/api/capture_order.php @@ -397,8 +397,23 @@ if (!empty($newOrderIds)) { } } else { cap_log('AUTO_PROVISION_SKIPPED', 'panel bootstrap failed — orders require manual provisioning: ' . implode(',', $newOrderIds)); + $autoProvision = [ + 'provisioned_count' => 0, + 'failed_count' => count($newOrderIds), + 'details' => [], + 'trace_log_path' => 'modules/billing/logs/provisioning_trace.log', + 'trace_error' => 'Panel bootstrap failed before billing provisioning could start.', + ]; } } +if (function_exists('billing_store_provision_session_result')) { + billing_store_provision_session_result($txid, [ + 'source' => 'api/capture_order.php', + 'txid' => $txid, + 'order_ids' => $newOrderIds, + 'result' => $autoProvision, + ]); +} unset($_SESSION['cart_coupon_code'], $_SESSION['cart_coupon_id']); diff --git a/modules/billing/checkout_free.php b/modules/billing/checkout_free.php index aa9e3709..1046d490 100644 --- a/modules/billing/checkout_free.php +++ b/modules/billing/checkout_free.php @@ -224,6 +224,7 @@ if ($couponId > 0 && !empty($invoices)) { unset($_SESSION['cart_coupon_code'], $_SESSION['cart_coupon_id']); // Attempt automatic provisioning via panel bridge +$autoProvision = ['provisioned_count' => 0, 'failed_count' => 0, 'details' => [], 'trace_log_path' => 'modules/billing/logs/provisioning_trace.log']; if (!empty($newOrderIds)) { require_once __DIR__ . '/includes/panel_bridge.php'; $panelCtx = billing_panel_bootstrap(); @@ -231,14 +232,30 @@ if (!empty($newOrderIds)) { $GLOBALS['db'] = $panelCtx['db']; $GLOBALS['settings'] = $panelCtx['settings']; require_once __DIR__ . '/create_servers.php'; - billing_invoke_provision(['order_ids' => $newOrderIds, 'user_id' => $userId, 'is_admin' => true]); + $autoProvision = billing_invoke_provision(['order_ids' => $newOrderIds, 'user_id' => $userId, 'is_admin' => true]); + } else { + $autoProvision = [ + 'provisioned_count' => 0, + 'failed_count' => count($newOrderIds), + 'details' => [], + 'trace_log_path' => 'modules/billing/logs/provisioning_trace.log', + 'trace_error' => 'Panel bootstrap failed before billing provisioning could start.', + ]; } // If panel bootstrap fails the order is Active and admins can provision via the orders panel. } +if (function_exists('billing_store_provision_session_result')) { + billing_store_provision_session_result($txid, [ + 'source' => 'checkout_free.php', + 'txid' => $txid, + 'order_ids' => $newOrderIds, + 'result' => $autoProvision, + ]); +} if ($mysqli instanceof mysqli) { mysqli_close($mysqli); } -header('Location: /payment_success.php?order_id=' . urlencode($txid)); +header('Location: /payment_success.php?order_id=' . urlencode($txid) . '&source=free'); exit; diff --git a/modules/billing/create_servers.php b/modules/billing/create_servers.php index 37e15b6a..a3f375e9 100644 --- a/modules/billing/create_servers.php +++ b/modules/billing/create_servers.php @@ -12,6 +12,218 @@ if (!defined('BILLING_CPU_AFFINITY_NA')) { if (!defined('BILLING_NICE_DEFAULT')) { define('BILLING_NICE_DEFAULT', '0'); } +if (!defined('BILLING_PROVISION_TRACE_LOG')) { + define('BILLING_PROVISION_TRACE_LOG', 'modules/billing/logs/provisioning_trace.log'); +} + +if (!function_exists('billing_provision_trace_relative_path')) { + function billing_provision_trace_relative_path(): string + { + return BILLING_PROVISION_TRACE_LOG; + } +} + +if (!function_exists('billing_provision_trace_path')) { + function billing_provision_trace_path(): string + { + return __DIR__ . '/logs/provisioning_trace.log'; + } +} + +if (!function_exists('billing_set_trace_error')) { + function billing_set_trace_error(string $message): void + { + $GLOBALS['BILLING_PROVISION_TRACE_ERROR'] = $message; + error_log('billing_provision_trace: ' . $message); + } +} + +if (!function_exists('billing_format_trace_value')) { + function billing_format_trace_value($value): string + { + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + if ($value === null) { + return 'null'; + } + if (is_scalar($value)) { + return (string)$value; + } + $json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + return $json === false ? '[unserializable]' : $json; + } +} + +if (!function_exists('billing_ensure_trace_log_ready')) { + function billing_ensure_trace_log_ready(): array + { + $logFile = billing_provision_trace_path(); + $logDir = dirname($logFile); + if (!is_dir($logDir) && !@mkdir($logDir, 0755, true) && !is_dir($logDir)) { + $error = "Could not create billing trace log directory: {$logDir}"; + billing_set_trace_error($error); + return array('ok' => false, 'error' => $error, 'path' => $logFile); + } + if (!is_writable($logDir)) { + $error = "Billing trace log directory is not writable: {$logDir}"; + billing_set_trace_error($error); + return array('ok' => false, 'error' => $error, 'path' => $logFile); + } + if (!file_exists($logFile)) { + $result = @file_put_contents($logFile, '', FILE_APPEND | LOCK_EX); + if ($result === false) { + $error = "Could not create billing trace log file: {$logFile}"; + billing_set_trace_error($error); + return array('ok' => false, 'error' => $error, 'path' => $logFile); + } + } + if (!is_writable($logFile)) { + $error = "Billing trace log file is not writable: {$logFile}"; + billing_set_trace_error($error); + return array('ok' => false, 'error' => $error, 'path' => $logFile); + } + return array('ok' => true, 'path' => $logFile); + } +} + +if (!function_exists('billing_provision_trace')) { + function billing_provision_trace($message, array $context = array()): bool + { + $ready = billing_ensure_trace_log_ready(); + if (empty($ready['ok'])) { + return false; + } + $baseContext = isset($GLOBALS['BILLING_PROVISION_TRACE_CONTEXT']) && is_array($GLOBALS['BILLING_PROVISION_TRACE_CONTEXT']) + ? $GLOBALS['BILLING_PROVISION_TRACE_CONTEXT'] + : array(); + $merged = array_merge($baseContext, $context); + $line = '[' . date('Y-m-d H:i:s') . '] ' . trim((string)$message); + if (!empty($merged)) { + $parts = array(); + foreach ($merged as $key => $value) { + $parts[] = $key . '=' . billing_format_trace_value($value); + } + $line .= ' | ' . implode(' | ', $parts); + } + $result = @file_put_contents($ready['path'], $line . PHP_EOL, FILE_APPEND | LOCK_EX); + if ($result === false) { + billing_set_trace_error('Failed to append billing trace log: ' . $ready['path']); + return false; + } + return true; + } +} + +if (!function_exists('billing_get_server_home_row')) { + function billing_get_server_home_row($db, string $db_prefix, int $home_id): array + { + if ($home_id <= 0) { + return array(); + } + $row = $db->resultQuery( + "SELECT * FROM `{$db_prefix}server_homes` + WHERE home_id=" . $db->realEscapeSingle($home_id) . " + LIMIT 1" + ); + return !empty($row[0]) ? (array)$row[0] : array(); + } +} + +if (!function_exists('billing_trace_home_info_summary')) { + function billing_trace_home_info_summary(array $home_info): array + { + $mods = array(); + foreach ((array)($home_info['mods'] ?? array()) as $modId => $modInfo) { + $mods[] = array( + 'mod_id' => intval($modId), + 'mod_key' => $modInfo['mod_key'] ?? '', + ); + } + return array( + 'home_id' => intval($home_info['home_id'] ?? 0), + 'home_cfg_id' => intval($home_info['home_cfg_id'] ?? 0), + 'home_cfg_file' => $home_info['home_cfg_file'] ?? '', + 'remote_server_id' => intval($home_info['remote_server_id'] ?? 0), + 'home_path' => $home_info['home_path'] ?? '', + 'agent_ip' => $home_info['agent_ip'] ?? '', + 'agent_port' => $home_info['agent_port'] ?? '', + 'mods' => $mods, + ); + } +} + +if (!function_exists('billing_trace_settings_summary')) { + function billing_trace_settings_summary(array $settings): array + { + return array( + 'panel_name' => $settings['panel_name'] ?? '', + 'steam_user_configured' => !empty($settings['steam_user']), + 'steam_guard_configured' => !empty($settings['steam_guard']), + ); + } +} + +if (!function_exists('billing_detect_install_state')) { + function billing_detect_install_state(array $home_info): array + { + $state = array( + 'complete' => false, + 'remote_status' => 'unknown', + 'update_active' => false, + 'exec_path' => '', + 'exec_exists' => false, + 'reason' => '', + ); + if (empty($home_info['home_id'])) { + $state['reason'] = 'home_info is missing home_id.'; + return $state; + } + if (empty($home_info['home_cfg_file'])) { + $state['reason'] = 'home_cfg_file is missing; install completion cannot be verified.'; + return $state; + } + $server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']); + if (!$server_xml) { + $state['reason'] = 'Could not read server config XML; install completion cannot be verified.'; + return $state; + } + $server_exec_name = trim((string)($server_xml->server_exec_name ?? '')); + if ($server_exec_name === '') { + $state['reason'] = 'server_exec_name is empty; install completion cannot be verified.'; + return $state; + } + $remote = new OGPRemoteLibrary($home_info['agent_ip'], $home_info['agent_port'], $home_info['encryption_key'], $home_info['timeout']); + $hostStat = $remote->status_chk(); + $state['remote_status'] = ($hostStat === 1) ? 'online' : 'offline'; + if ($hostStat !== 1) { + $state['reason'] = 'Agent is offline; install completion cannot be verified.'; + return $state; + } + $log_txt = ''; + $update_active = $remote->get_log(OGP_SCREEN_TYPE_UPDATE, intval($home_info['home_id']), clean_path($home_info['home_path']), $log_txt); + $state['update_active'] = ($update_active == 1); + $state['update_log'] = $log_txt; + $execFolder = clean_path($home_info['home_path'] . "/" . (string)($server_xml->exe_location ?? '')); + $execPath = clean_path($execFolder . "/" . $server_exec_name); + $state['exec_path'] = $execPath; + $state['exec_exists'] = ($remote->rfile_exists($execPath) === 1); + $state['complete'] = $state['exec_exists']; + $state['reason'] = $state['exec_exists'] + ? 'Expected executable already exists on the remote server.' + : 'Expected executable is missing on the remote server.'; + return $state; + } +} + +if (!function_exists('billing_store_provision_session_result')) { + function billing_store_provision_session_result(string $key, array $payload): void + { + if (session_status() === PHP_SESSION_ACTIVE) { + $_SESSION['billing_provision_results'][$key] = $payload; + } + } +} if (!function_exists('billing_generate_provision_password')) { function billing_generate_provision_password() @@ -26,6 +238,7 @@ if (!function_exists('billing_generate_provision_password')) { } return $password; } catch (Throwable $e) { + billing_provision_trace('Password generation fallback after exception.', array('exception' => $e->getMessage())); for ($i = 0; $i < $length; $i++) { $password .= $alphabet[mt_rand(0, $alphabetLen - 1)]; } @@ -58,13 +271,23 @@ if (!function_exists('billing_agent_offline_reason')) { if (!function_exists('billing_invoke_provision')) { function billing_invoke_provision(array $options = array()) { + if (empty($options['caller_source'])) { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); + $caller = $trace[1] ?? array(); + $options['caller_source'] = ($caller['file'] ?? __FILE__) . ':' . intval($caller['line'] ?? 0); + } + unset($GLOBALS['BILLING_PROVISION_TRACE_ERROR'], $GLOBALS['BILLING_PROVISION_TRACE_CONTEXT']); $GLOBALS['BILLING_PROVISION_OVERRIDE'] = $options; ob_start(); exec_ogp_module(); $output = ob_get_clean(); $result = isset($GLOBALS['BILLING_PROVISION_LAST_RESULT']) ? $GLOBALS['BILLING_PROVISION_LAST_RESULT'] : array(); $result['output'] = $output; - unset($GLOBALS['BILLING_PROVISION_OVERRIDE'], $GLOBALS['BILLING_PROVISION_LAST_RESULT']); + $result['trace_log_path'] = billing_provision_trace_relative_path(); + if (!empty($GLOBALS['BILLING_PROVISION_TRACE_ERROR'])) { + $result['trace_error'] = $GLOBALS['BILLING_PROVISION_TRACE_ERROR']; + } + unset($GLOBALS['BILLING_PROVISION_OVERRIDE'], $GLOBALS['BILLING_PROVISION_LAST_RESULT'], $GLOBALS['BILLING_PROVISION_TRACE_ERROR'], $GLOBALS['BILLING_PROVISION_TRACE_CONTEXT']); return $result; } } @@ -90,7 +313,18 @@ 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); + billing_provision_trace('Resolved remote server IP IDs for port allocation.', array( + 'home_id' => $home_id, + 'selected_remote_server_id' => $remote_server_id, + 'selected_home_cfg_id' => $home_cfg_id, + 'ip_ids' => $ipIds, + )); if (empty($ipIds)) { + billing_provision_trace('Port allocation failed because no remote_server_ips rows were found.', array( + 'home_id' => $home_id, + 'selected_remote_server_id' => $remote_server_id, + 'selected_home_cfg_id' => $home_cfg_id, + )); return array('ok' => false, 'error' => "No IP addresses are configured for remote server #{$remote_server_id}."); } @@ -115,8 +349,18 @@ if (!function_exists('billing_allocate_home_port')) { } if (empty($ranges)) { $ips_with_no_range[] = $ipId; + billing_provision_trace('No port range found for current ip_id.', array( + 'home_id' => $home_id, + 'selected_ip_id' => $ipId, + 'selected_home_cfg_id' => $home_cfg_id, + )); continue; } + billing_provision_trace('Loaded port ranges for current ip_id.', array( + 'home_id' => $home_id, + 'selected_ip_id' => $ipId, + 'port_ranges_used' => $ranges, + )); $usedRows = $db->resultQuery( "SELECT port FROM `{$db_prefix}home_ip_ports` WHERE ip_id=" . $db->realEscapeSingle($ipId) @@ -152,6 +396,12 @@ if (!function_exists('billing_allocate_home_port')) { AND port = {$safePort} )" ); + billing_provision_trace('Attempted home_ip_ports insert.', array( + 'home_id' => $home_id, + 'selected_ip_id' => $ipId, + 'selected_port' => $port, + 'home_ip_ports_insert_succeeded' => (bool)$insertOk, + )); if (!$insertOk) { continue; } @@ -163,6 +413,12 @@ if (!function_exists('billing_allocate_home_port')) { LIMIT 1" ); if (!empty($verify)) { + billing_provision_trace('Port allocation succeeded.', array( + 'home_id' => $home_id, + 'selected_ip_id' => $ipId, + 'selected_port' => $port, + 'home_ip_ports_insert_succeeded' => true, + )); return array('ok' => true, 'ip_id' => $ipId, 'port' => intval($port)); } } @@ -171,8 +427,19 @@ if (!function_exists('billing_allocate_home_port')) { } if (!empty($ips_with_no_range) && count($ips_with_no_range) === count($ipIds)) { + billing_provision_trace('Port allocation failed because no matching arrange_ports ranges were found.', array( + 'home_id' => $home_id, + 'selected_remote_server_id' => $remote_server_id, + 'ips_with_no_range' => $ips_with_no_range, + )); return array('ok' => false, 'error' => "No port range found for home_cfg_id #{$home_cfg_id} on ip_id(s) [" . implode(',', $ips_with_no_range) . "] for remote server #{$remote_server_id}."); } + billing_provision_trace('Port allocation failed because all ranges were exhausted.', array( + 'home_id' => $home_id, + 'selected_remote_server_id' => $remote_server_id, + 'selected_home_cfg_id' => $home_cfg_id, + 'ips_exhausted' => !empty($ips_exhausted) ? $ips_exhausted : $ipIds, + )); return array('ok' => false, 'error' => "No available port in arrange_ports for remote server #{$remote_server_id}, home_cfg_id #{$home_cfg_id}, ip_id(s) [" . implode(',', !empty($ips_exhausted) ? $ips_exhausted : $ipIds) . "]."); } } @@ -181,7 +448,15 @@ 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); + billing_provision_trace('Loaded config mods for home_cfg_id.', array( + 'selected_home_cfg_id' => $home_cfg_id, + 'preferred_mod_cfg_id' => $preferred_mod_cfg_id, + 'cfg_mod_rows' => $mods, + )); if (empty($mods)) { + billing_provision_trace('No config mods found for home_cfg_id.', array( + 'selected_home_cfg_id' => $home_cfg_id, + )); return array('ok' => false, 'error' => "No config_mods rows found for home_cfg_id #{$home_cfg_id}."); } @@ -197,14 +472,26 @@ if (!function_exists('billing_resolve_mod_cfg_id')) { $first = $modCfgId; } if ($preferred_mod_cfg_id > 0 && $modCfgId === $preferred_mod_cfg_id) { + billing_provision_trace('Selected preferred mod_cfg_id for provisioning.', array( + 'selected_home_cfg_id' => $home_cfg_id, + 'selected_mod_cfg_id' => $modCfgId, + )); return array('ok' => true, 'mod_cfg_id' => $modCfgId); } } if ($first !== null) { + billing_provision_trace('Selected fallback mod_cfg_id for provisioning.', array( + 'selected_home_cfg_id' => $home_cfg_id, + 'selected_mod_cfg_id' => $first, + )); return array('ok' => true, 'mod_cfg_id' => $first); } + billing_provision_trace('No usable mod_cfg_id was found for provisioning.', array( + 'selected_home_cfg_id' => $home_cfg_id, + 'available_mod_cfg_ids' => $available_mod_cfg_ids, + )); return array('ok' => false, 'error' => "No usable mod_cfg_id found for home_cfg_id #{$home_cfg_id}. Available mod_cfg_id values: [" . implode(',', $available_mod_cfg_ids) . "]."); } } @@ -263,6 +550,9 @@ function exec_ogp_module() $user_id = isset($override['user_id']) ? intval($override['user_id']) : (isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0); $isAdmin = isset($override['is_admin']) ? (bool)$override['is_admin'] : $db->isAdmin($user_id); $provision_all = $override ? !empty($override['provision_all']) : isset($_POST['provision_all']); + $caller_source = $override['caller_source'] ?? (__FILE__ . ':0'); + $dbNameRow = $db->resultQuery("SELECT DATABASE() AS db_name"); + $active_db_name = $dbNameRow[0]['db_name'] ?? ''; $orderIds = array(); if ($override && !empty($override['order_ids'])) { $orderIds = array_map('intval', (array)$override['order_ids']); @@ -279,20 +569,44 @@ function exec_ogp_module() $orderIds = array(intval($order_id)); } } + $traceReady = billing_ensure_trace_log_ready(); + if (empty($traceReady['ok'])) { + echo "

" . htmlspecialchars((string)$traceReady['error'], ENT_QUOTES, 'UTF-8') . "

"; + $GLOBALS['BILLING_PROVISION_LAST_RESULT'] = array( + 'provisioned_count' => 0, + 'failed_count' => 1, + 'orders' => array(), + 'details' => array(), + 'trace_log_path' => billing_provision_trace_relative_path(), + 'trace_error' => $traceReady['error'], + ); + return; + } + billing_provision_trace('START provisioning attempt', array( + 'caller_source' => $caller_source, + 'order_ids_received' => $orderIds, + 'user_id_received' => $user_id, + 'is_admin' => $isAdmin, + 'provision_all' => $provision_all, + 'active_db_name' => $active_db_name, + 'db_prefix' => $db_prefix, + )); // Handle provision_all request - provision all Active (paid) orders for this user if ($provision_all) { if ( $isAdmin ){ - $orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE status='Active' AND (home_id='0' OR home_id='') ORDER BY order_id" ); + $orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE status='Active' ORDER BY order_id" ); } else { - $orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE user_id=".$db->realEscapeSingle($user_id)." AND status='Active' AND (home_id='0' OR home_id='') ORDER BY order_id" ); + $orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE user_id=".$db->realEscapeSingle($user_id)." AND status='Active' ORDER BY order_id" ); } + billing_provision_trace('Loaded orders for provision_all request.', array('loaded_order_count' => count((array)$orders))); } // Handle provision_single or order_id parameter - provision specific order else { if (empty($orderIds)) { + billing_provision_trace('END failure: provisioning request returned early because no order ID was supplied.'); echo "
No order ID specified.
"; - $GLOBALS['BILLING_PROVISION_LAST_RESULT'] = array('provisioned_count'=>0,'failed_count'=>0,'orders'=>array()); + $GLOBALS['BILLING_PROVISION_LAST_RESULT'] = array('provisioned_count'=>0,'failed_count'=>0,'orders'=>array(),'details'=>array(),'trace_log_path'=>billing_provision_trace_relative_path()); return; } $idList = implode(',', array_map('intval', $orderIds)); @@ -301,8 +615,10 @@ function exec_ogp_module() } else { $orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE order_id IN ($idList) AND user_id=".$db->realEscapeSingle($user_id)." AND status='Active'" ); } + billing_provision_trace('Loaded explicit order list for provisioning.', array('loaded_order_count' => count((array)$orders))); } $processed_orders = array(); + $order_results = array(); if( !empty($orders) ) { $provisioned_count = 0; @@ -311,6 +627,13 @@ function exec_ogp_module() foreach ((array)$orders as $order) { + $trace_id = uniqid('prov_', true); + $GLOBALS['BILLING_PROVISION_TRACE_CONTEXT'] = array( + 'trace_id' => $trace_id, + 'order_id' => intval($order['order_id'] ?? 0), + ); + billing_provision_trace('Loaded billing order for provisioning.', array('order_row' => $order)); + try { $home_id = 0; $order_failed = false; $order_failure_reason = ''; @@ -343,7 +666,12 @@ function exec_ogp_module() $install_message = ''; $install_attempted = false; $needs_existing_home_retry = false; + $skip_reason = ''; $home_info = array(); + $home_row_before = array(); + $home_row_after = array(); + $install_state = array(); + $autoInstall = array(); $invoiceRow = $db->resultQuery( "SELECT invoice_id FROM `{$db_prefix}billing_invoices` @@ -354,10 +682,16 @@ function exec_ogp_module() if (!empty($invoiceRow[0]['invoice_id'])) { $provision_invoice_id = intval($invoiceRow[0]['invoice_id']); } + billing_provision_trace('Resolved latest invoice row for order.', array('invoice_row' => $invoiceRow)); //Query service info $service = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_services` WHERE service_id=".$db->realEscapeSingle($service_id) ); + billing_provision_trace('Loaded billing service row.', array( + 'service_id' => intval($service_id), + 'service_row_found' => !empty($service[0]), + 'service_row' => !empty($service[0]) ? $service[0] : array(), + )); if( !empty( $service[0] ) ) { @@ -371,6 +705,13 @@ function exec_ogp_module() $install_method = $service[0]['install_method']; $manual_url = $service[0]['manual_url']; $access_rights = $service[0]['access_rights']; + billing_provision_trace('Provisioning inputs resolved from order and service.', array( + 'service_id' => intval($service_id), + 'order_status' => $order['status'] ?? '', + 'order_home_id_before_provisioning' => intval($order['home_id'] ?? 0), + 'selected_home_cfg_id' => intval($home_cfg_id), + 'selected_remote_server_id' => intval($remote_server_id), + )); if (intval($home_cfg_id) <= 0) { $order_failed = true; $order_failure_reason = "Invalid home_cfg_id '{$home_cfg_id}' for service_id {$service_id}."; @@ -384,52 +725,81 @@ function exec_ogp_module() { $order_failed = true; $order_failure_reason = "Service ID {$service_id} not found."; + billing_provision_trace('Eligibility skip: billing service row is missing.', array('service_id' => intval($service_id))); } if(!$order_failed && $already_provisioned) { $home_id = intval($order['home_id']); + $home_row_before = billing_get_server_home_row($db, $db_prefix, $home_id); + billing_provision_trace('Existing home_id detected on billing order.', array( + 'order_home_id_before_provisioning' => intval($order['home_id'] ?? 0), + 'server_homes_row_before' => $home_row_before, + )); $home_info = $db->getGameHome($home_id); - if (empty($home_info)) { + if (empty($home_row_before) || empty($home_info)) { $order_failed = true; $order_failure_reason = "Order #{$order_id} references home_id {$home_id} but server_homes row is missing."; $db->logger('BILLING PROVISION DATA INTEGRITY ERROR: ' . $order_failure_reason); + billing_provision_trace('Eligibility failure: existing home_id is linked but server_homes row is missing.', array('home_id_after_creation_or_lookup' => $home_id)); } $existingIpPort = billing_get_home_ip_port($db, $db_prefix, intval($home_id)); if (!empty($existingIpPort['ok'])) { $selected_ip_id = intval($existingIpPort['ip_id']); $selected_port = intval($existingIpPort['port']); } + billing_provision_trace('Existing home IP:port lookup completed.', array( + 'home_id_after_creation_or_lookup' => $home_id, + 'ip_port_row_found' => !empty($existingIpPort['ok']), + 'selected_ip_id' => intval($selected_ip_id), + 'selected_port' => intval($selected_port), + )); $has_ip_port = !empty($existingIpPort['ok']); $has_mods = !empty($home_info['mods']) && is_array($home_info['mods']); if (!$order_failed && (!$has_ip_port || !$has_mods)) { $needs_existing_home_retry = true; $install_message = "Existing home #{$home_id} requires provisioning completion (ip_port=" . ($has_ip_port ? 'yes' : 'no') . ", mods=" . ($has_mods ? 'yes' : 'no') . ")."; + billing_provision_trace('Existing home requires provisioning retry because prerequisites are incomplete.', array( + 'home_id_after_creation_or_lookup' => $home_id, + 'has_ip_port' => $has_ip_port, + 'has_mods' => $has_mods, + )); } if (!$order_failed && !$needs_existing_home_retry) { - $server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']); - if ($server_xml && !empty((string)$server_xml->server_exec_name)) { - $remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']); - if ($remote->status_chk() === 1) { - $exec_path = clean_path($home_info['home_path'] . "/" . (string)$server_xml->exe_location . "/" . (string)$server_xml->server_exec_name); - if ($remote->rfile_exists($exec_path) !== 1) { - $needs_existing_home_retry = true; - $install_message = "Existing home #{$home_id} missing expected executable '" . basename($exec_path) . "'; retrying install."; - } - } + $install_state = billing_detect_install_state((array)$home_info); + billing_provision_trace('Existing home install state verification completed.', array( + 'home_id_after_creation_or_lookup' => $home_id, + 'install_state' => $install_state, + )); + if (empty($install_state['complete'])) { + $needs_existing_home_retry = true; + $install_message = "Existing home #{$home_id} is not fully installed yet. " . ($install_state['reason'] ?? 'Install completion could not be verified.'); } } - if (!$order_failed && !$needs_existing_home_retry) { + if (!$order_failed && !$needs_existing_home_retry && !empty($install_state['complete'])) { $install_result = 'completed'; $install_message = $install_message !== '' ? $install_message : "Order #{$order_id} already provisioned and installed; no action required."; + $skip_reason = $install_message; + billing_provision_trace('Eligibility skip: existing home is already provisioned and install is complete.', array( + 'home_id_after_creation_or_lookup' => $home_id, + 'skip_reason' => $skip_reason, + )); } } elseif(!$order_failed && $extended) { $home_id = $order['home_id']; + billing_provision_trace('Processing renewal for existing billing order.', array( + 'home_id_after_creation_or_lookup' => intval($home_id), + )); //Get The home info without mods in 1 array (Necesary for remote connection). $home_info = $db->getGameHomeWithoutMods($home_id); + $home_row_before = billing_get_server_home_row($db, $db_prefix, intval($home_id)); + billing_provision_trace('Loaded renewal home state.', array( + 'server_homes_row_before' => $home_row_before, + 'home_info_summary' => billing_trace_home_info_summary((array)$home_info), + )); //Create the remote connection $remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']); @@ -472,6 +842,9 @@ function exec_ogp_module() } elseif(!$order_failed) { + billing_provision_trace('Provisioning new home because billing order has no completed install yet.', array( + 'order_home_id_before_provisioning' => intval($order['home_id'] ?? 0), + )); //OPTIONS, change it at your choice; $extra_params = "";//no extra params defined by default $cpu_affinity = "NA";//Affinity to one core/thread of the cpu by number, use NA to disable it @@ -483,15 +856,29 @@ function exec_ogp_module() if (empty($rserver)) { $order_failed = true; $order_failure_reason = "Remote server #{$remote_server_id} not found for order #{$order_id} (service_id {$service_id})."; + billing_provision_trace('Eligibility failure: selected remote server row is missing.', array( + 'selected_remote_server_id' => intval($remote_server_id), + )); } $game_path = "/home/gameserver/"; if (!$order_failed) { $home_id = $db->addGameHome($remote_server_id, $user_id, $home_cfg_id, $game_path, $home_name, $remote_control_password, $ftp_password); + billing_provision_trace('Attempted server_homes creation for billing order.', array( + 'selected_remote_server_id' => intval($remote_server_id), + 'selected_home_cfg_id' => intval($home_cfg_id), + 'home_id_after_creation_or_lookup' => intval($home_id), + )); } if (!$order_failed && (!$home_id || intval($home_id) <= 0)) { $order_failed = true; $order_failure_reason = "Could not create server_homes row for order #{$order_id}."; } + if (!$order_failed) { + $home_row_before = billing_get_server_home_row($db, $db_prefix, intval($home_id)); + billing_provision_trace('Loaded server_homes row immediately after creation.', array( + 'server_homes_row_before' => $home_row_before, + )); + } if (!$order_failed) { // Billing storefront defaults FTP to enabled for newly provisioned homes so panel/account flows stay consistent after checkout. $db->changeFtpStatus('enabled', intval($home_id)); @@ -509,6 +896,10 @@ function exec_ogp_module() } else { $selected_ip_id = intval($allocatedPort['ip_id'] ?? 0); $selected_port = intval($allocatedPort['port'] ?? 0); + billing_provision_trace('Selected IP:port for new home.', array( + 'selected_ip_id' => $selected_ip_id, + 'selected_port' => $selected_port, + )); } } @@ -528,6 +919,11 @@ function exec_ogp_module() $mod_id = false; if (!$order_failed) { $mod_id = $db->addModToGameHome( $home_id, $resolved_mod_cfg_id ); + billing_provision_trace('Attempted game_mods attach for home.', array( + 'home_id_after_creation_or_lookup' => intval($home_id), + 'selected_mod_cfg_id' => intval($resolved_mod_cfg_id), + 'selected_mod_id' => intval($mod_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}."; @@ -539,6 +935,11 @@ function exec_ogp_module() $db->updateGameModParams( $max_players, $extra_params, $cpu_affinity, $nice, $home_id, $resolved_mod_cfg_id ); $db->assignHomeTo( "user", $user_id, $home_id, $access_rights ); $selected_mod_id = intval($mod_id); + billing_provision_trace('Updated game_mod params and assigned home to user.', array( + 'home_id_after_creation_or_lookup' => intval($home_id), + 'selected_mod_id' => intval($selected_mod_id), + 'user_id_received' => intval($user_id), + )); } //Get The home info without mods in 1 array (Necesary for remote connection). @@ -573,16 +974,31 @@ function exec_ogp_module() { $remote->ftp_mgr("useradd", $home_info['home_id'], $home_info['ftp_password'], $home_info['home_path']); $db->changeFtpStatus('enabled',$home_info['home_id']); + billing_provision_trace('Enabled FTP account for provisioned home.', array( + 'home_id_after_creation_or_lookup' => intval($home_info['home_id']), + )); } if (!$order_failed) { $install_attempted = true; + billing_provision_trace('Calling gamemanager_trigger_update_install for newly provisioned home.', array( + 'exact_call' => "gamemanager_trigger_update_install(\$db, \$home_info, {$mod_id}, ['settings' => ...])", + 'home_id_after_creation_or_lookup' => intval($home_id), + 'selected_mod_id' => intval($mod_id), + 'home_info_summary' => billing_trace_home_info_summary((array)$home_info), + 'selected_settings' => billing_trace_settings_summary((array)$settings), + )); $autoInstall = gamemanager_trigger_update_install( $db, $home_info, intval($mod_id), array('settings' => $settings) ); + billing_provision_trace('gamemanager_trigger_update_install returned for newly provisioned home.', array( + 'home_id_after_creation_or_lookup' => intval($home_id), + 'selected_mod_id' => intval($mod_id), + 'gamemanager_trigger_update_install_result' => $autoInstall, + )); $mod_id = intval($autoInstall['mod_id'] ?? $mod_id); $selected_mod_id = intval($mod_id); $install_message = (string)($autoInstall['message'] ?? ''); @@ -641,11 +1057,20 @@ function exec_ogp_module() // Retry install for orders that already have home_id but never triggered installation. if (!$order_failed && !$extended && !$install_attempted && intval($home_id) > 0 && (!$already_provisioned || $needs_existing_home_retry)) { + billing_provision_trace('Continuing provisioning for existing home because install is incomplete.', array( + 'home_id_after_creation_or_lookup' => intval($home_id), + 'needs_existing_home_retry' => $needs_existing_home_retry, + 'install_message' => $install_message, + )); if ($selected_ip_id <= 0 || $selected_port <= 0) { $existingIpPort = billing_get_home_ip_port($db, $db_prefix, intval($home_id)); if (!empty($existingIpPort['ok'])) { $selected_ip_id = intval($existingIpPort['ip_id']); $selected_port = intval($existingIpPort['port']); + billing_provision_trace('Reused existing IP:port for existing home retry.', array( + 'selected_ip_id' => $selected_ip_id, + 'selected_port' => $selected_port, + )); } else { $allocatedPort = billing_allocate_home_port($db, $db_prefix, intval($home_id), intval($remote_server_id), intval($home_cfg_id)); if (empty($allocatedPort['ok'])) { @@ -656,6 +1081,10 @@ function exec_ogp_module() } else { $selected_ip_id = intval($allocatedPort['ip_id'] ?? 0); $selected_port = intval($allocatedPort['port'] ?? 0); + billing_provision_trace('Allocated new IP:port for existing home retry.', array( + 'selected_ip_id' => $selected_ip_id, + 'selected_port' => $selected_port, + )); } } } @@ -680,6 +1109,11 @@ function exec_ogp_module() } else { $resolved_mod_cfg_id = intval($modResolution['mod_cfg_id']); $selected_mod_id = intval($db->addModToGameHome(intval($home_id), intval($resolved_mod_cfg_id))); + billing_provision_trace('Attempted to attach missing mod during existing home retry.', array( + 'home_id_after_creation_or_lookup' => intval($home_id), + 'selected_mod_cfg_id' => intval($resolved_mod_cfg_id), + 'selected_mod_id' => intval($selected_mod_id), + )); if ($selected_mod_id <= 0) { $order_failed = true; $order_failure_reason = "Could not attach mod_cfg_id {$resolved_mod_cfg_id} to home #{$home_id}."; @@ -695,12 +1129,24 @@ function exec_ogp_module() if (!$order_failed) { $selected_mod_id = intval(gamemanager_choose_mod_id((array)$home_info, intval($selected_mod_id))); $install_attempted = true; + billing_provision_trace('Calling gamemanager_trigger_update_install for existing home retry.', array( + 'exact_call' => "gamemanager_trigger_update_install(\$db, \$home_info, {$selected_mod_id}, ['settings' => ...])", + 'home_id_after_creation_or_lookup' => intval($home_id), + 'selected_mod_id' => intval($selected_mod_id), + 'home_info_summary' => billing_trace_home_info_summary((array)$home_info), + 'selected_settings' => billing_trace_settings_summary((array)$settings), + )); $autoInstall = gamemanager_trigger_update_install( $db, (array)$home_info, intval($selected_mod_id), array('settings' => $settings) ); + billing_provision_trace('gamemanager_trigger_update_install returned for existing home retry.', array( + 'home_id_after_creation_or_lookup' => intval($home_id), + 'selected_mod_id' => intval($selected_mod_id), + 'gamemanager_trigger_update_install_result' => $autoInstall, + )); $selected_mod_id = intval($autoInstall['mod_id'] ?? $selected_mod_id); $install_message = (string)($autoInstall['message'] ?? ''); if (!empty($autoInstall['already_running'])) { @@ -757,6 +1203,12 @@ function exec_ogp_module() if ($order_failure_reason === '') { $order_failure_reason = "No home_id was produced for order #{$order_id}."; } + billing_provision_trace('Eligibility failure: provisioning finished without a valid home_id.', array( + 'home_id_after_creation_or_lookup' => intval($home_id), + )); + } + if ($home_id > 0 && empty($home_row_before)) { + $home_row_before = billing_get_server_home_row($db, $db_prefix, intval($home_id)); } // Set order status to 'Active' (billing active even if install is pending) @@ -772,14 +1224,22 @@ function exec_ogp_module() WHERE order_id=".$db->realEscapeSingle($order_id)); // Save home_id created by this order - $db->query("UPDATE `{$db_prefix}billing_orders` + $orderHomeUpdateOk = $db->query("UPDATE `{$db_prefix}billing_orders` SET home_id='" . $db->realEscapeSingle($home_id) . "' WHERE order_id=".$db->realEscapeSingle($order_id)); + billing_provision_trace('Updated billing_orders.home_id.', array( + 'home_id_after_creation_or_lookup' => intval($home_id), + 'billing_orders_home_id_updated' => (bool)$orderHomeUpdateOk, + )); - $db->query("UPDATE `{$db_prefix}billing_invoices` + $invoiceHomeUpdateOk = $db->query("UPDATE `{$db_prefix}billing_invoices` SET home_id=" . $db->realEscapeSingle($home_id) . ", billing_status='Active', status='paid' WHERE order_id=" . $db->realEscapeSingle($order_id)); + billing_provision_trace('Updated billing_invoices.home_id and billing status.', array( + 'home_id_after_creation_or_lookup' => intval($home_id), + 'billing_invoices_home_id_updated' => (bool)$invoiceHomeUpdateOk, + )); $db->query("UPDATE `{$db_prefix}billing_transactions` SET home_id=" . $db->realEscapeSingle($home_id) . " @@ -798,6 +1258,10 @@ function exec_ogp_module() next_invoice_date = '" . $db->realEscapeSingle($end_date_str) . "', billing_enabled = 1 WHERE home_id = " . $db->realEscapeSingle($home_id)); + $home_row_after = billing_get_server_home_row($db, $db_prefix, intval($home_id)); + billing_provision_trace('Loaded server_homes row after billing linkage updates.', array( + 'server_homes_row_after' => $home_row_after, + )); } $provisionContext = array( @@ -813,6 +1277,7 @@ function exec_ogp_module() 'install_result' => $order_failed ? 'failed' : (string)$install_result, 'error' => $order_failed ? (string)$order_failure_reason : '', 'message' => (string)$install_message, + 'skip_reason' => (string)$skip_reason, ); billing_write_provision_log($provisionContext); $db->logger( @@ -833,9 +1298,51 @@ function exec_ogp_module() $failed_count++; $failed_messages[] = "Order #{$order_id}: {$order_failure_reason}"; $db->logger("Provisioning pending install for order #{$order_id}: {$order_failure_reason}"); + billing_provision_trace('END failure', array( + 'home_id_after_creation_or_lookup' => intval($home_id), + 'end_reason' => $order_failure_reason, + )); } else { $provisioned_count++; + billing_provision_trace('END success', array( + 'home_id_after_creation_or_lookup' => intval($home_id), + 'install_result' => (string)$install_result, + )); } + $order_results[] = array( + 'trace_id' => $trace_id, + 'order_id' => intval($order_id), + 'user_id' => intval($user_id), + 'service_id' => intval($service_id), + 'home_id' => intval($home_id), + 'mod_id' => intval($selected_mod_id), + 'install_result' => $order_failed ? 'failed' : (string)$install_result, + 'install_message' => (string)$install_message, + 'error' => $order_failed ? (string)$order_failure_reason : '', + 'trace_log_path' => billing_provision_trace_relative_path(), + ); + } catch (Throwable $e) { + $failed_count++; + $order_id = intval($order['order_id'] ?? 0); + $message = "Order #{$order_id} threw an exception during provisioning: " . $e->getMessage(); + $failed_messages[] = $message; + $db->logger('BILLING PROVISION EXCEPTION: ' . $message); + billing_provision_trace('Provisioning exception caught.', array('exception' => $e->getMessage())); + billing_provision_trace('END failure', array('end_reason' => $message)); + $order_results[] = array( + 'trace_id' => $trace_id, + 'order_id' => $order_id, + 'user_id' => intval($order['user_id'] ?? 0), + 'service_id' => intval($order['service_id'] ?? 0), + 'home_id' => intval($order['home_id'] ?? 0), + 'mod_id' => 0, + 'install_result' => 'failed', + 'install_message' => '', + 'error' => $e->getMessage(), + 'trace_log_path' => billing_provision_trace_relative_path(), + ); + } + unset($GLOBALS['BILLING_PROVISION_TRACE_CONTEXT']); } @@ -872,6 +1379,10 @@ function exec_ogp_module() } } else { + billing_provision_trace('END failure: no paid orders matched provisioning request.', array( + 'caller_source' => $caller_source, + 'order_ids_received' => $orderIds, + )); echo "
"; echo "

No paid orders found to provision.

"; echo "
"; @@ -883,6 +1394,13 @@ function exec_ogp_module() 'provisioned_count' => isset($provisioned_count) ? $provisioned_count : 0, 'failed_count' => isset($failed_count) ? $failed_count : 0, 'orders' => $processed_orders, + 'details' => $order_results, + 'trace_log_path' => billing_provision_trace_relative_path(), + 'trace_error' => $GLOBALS['BILLING_PROVISION_TRACE_ERROR'] ?? '', ); + billing_provision_trace('END provisioning attempt', array( + 'provisioned_count' => intval($GLOBALS['BILLING_PROVISION_LAST_RESULT']['provisioned_count'] ?? 0), + 'failed_count' => intval($GLOBALS['BILLING_PROVISION_LAST_RESULT']['failed_count'] ?? 0), + )); } ?> diff --git a/modules/billing/payment_success.php b/modules/billing/payment_success.php index 1dcbb3fa..cd80494d 100644 --- a/modules/billing/payment_success.php +++ b/modules/billing/payment_success.php @@ -4,7 +4,10 @@ * Shows order confirmation after successful PayPal payment * Standalone billing module - uses only standard PHP mysqli */ -session_start(); +if (session_status() === PHP_SESSION_NONE) { + session_name('opengamepanel_web'); + session_start(); +} require_once(__DIR__ . '/includes/config_loader.php'); // Variables from config.inc.php (helps IDEs understand scope) @@ -16,10 +19,13 @@ require_once(__DIR__ . '/includes/config_loader.php'); // Get PayPal order ID from URL $paypal_order_id = isset($_GET['order_id']) ? trim($_GET['order_id']) : ''; +$success_source = isset($_GET['source']) ? trim($_GET['source']) : ''; // Get user ID from session $user_id = isset($_SESSION['website_user_id']) ? intval($_SESSION['website_user_id']) : (isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0); +$is_admin_viewer = strtolower((string)($_SESSION['users_group'] ?? '')) === 'admin'; +$provision_summary = $_SESSION['billing_provision_results'][$paypal_order_id] ?? null; // Connect to database $db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null); @@ -46,6 +52,19 @@ function billing_payment_success_provision_state(array $order): array return ['label' => 'INSTALL STARTED', 'message' => 'Server created and install/update trigger has been started or queued.', 'class' => 'status-badge']; } +function billing_payment_success_banner(array $summary, array $orders): array +{ + if (!empty($summary['result']['failed_count'])) { + return ['class' => 'status-failed', 'message' => 'Provisioning failed; support has been notified.']; + } + foreach ($orders as $order) { + if (($order['provision_state']['label'] ?? '') === 'FAILED') { + return ['class' => 'status-failed', 'message' => 'Provisioning failed; support has been notified.']; + } + } + return ['class' => 'status-pending', 'message' => 'Your server is being installed.']; +} + if ($db && $user_id > 0) { // Get recent orders for this user (just paid) $query = "SELECT o.*, s.service_name, @@ -163,6 +182,37 @@ if ($db && $user_id > 0) { .status-failed { background: #d9534f; } + .provision-banner { + margin: 20px 0 0; + padding: 14px 18px; + border-radius: 6px; + color: #fff; + font-weight: 600; + } + .provision-debug { + margin-top: 20px; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 14px 16px; + background: #f8f9fa; + } + .provision-debug summary { + cursor: pointer; + font-weight: 600; + } + .provision-debug-table { + width: 100%; + border-collapse: collapse; + margin-top: 12px; + } + .provision-debug-table th, + .provision-debug-table td { + border-bottom: 1px solid #dee2e6; + padding: 8px 6px; + text-align: left; + font-size: 0.92em; + vertical-align: top; + } .btn { display: inline-block; padding: 12px 24px; @@ -191,6 +241,7 @@ if ($db && $user_id > 0) { +
@@ -211,6 +262,49 @@ if ($db && $user_id > 0) {
  • 📧 Email Notification: You'll receive a confirmation email with your order details
  • 🎮 Access Your Servers: Log into the Game Server Panel to manage your new servers
  • +
    + +
    + +
    > + Provisioning Debug Summary +

    Provisioning started:

    +

    Provisioning succeeded:

    +

    Provisioning failed: 0 ? 'yes' : 'no'; ?>

    +

    Log file path:

    + +

    Trace error:

    + + + + + + + + + + + + + + + + + + + + + + + +
    OrderStatusHome IDMod IDMessage
    #
    + +
    + +
    + Free checkout completed. +
    +
    0): ?> diff --git a/modules/billing/timestamp.txt b/modules/billing/timestamp.txt index 11522bc6..553ab5b7 100644 --- a/modules/billing/timestamp.txt +++ b/modules/billing/timestamp.txt @@ -1 +1 @@ -Last Updated at 10:02pm on 2026-05-08 +Last Updated at 2:43pm on 2026-05-09 diff --git a/modules/gamemanager/update_actions.php b/modules/gamemanager/update_actions.php index 45376a7a..88a5f5e9 100644 --- a/modules/gamemanager/update_actions.php +++ b/modules/gamemanager/update_actions.php @@ -20,36 +20,42 @@ if (!function_exists('gamemanager_trigger_update_install')) { { $home_id = intval($home_info['home_id'] ?? 0); $mod_id = gamemanager_choose_mod_id($home_info, $mod_id); + $resultBase = array( + 'trigger' => 'gamemanager_trigger_update_install', + 'home_id' => $home_id, + 'mod_id' => $mod_id, + 'output' => null, + ); if ($home_id <= 0) { - return array('ok' => false, 'pending' => true, 'message' => 'Invalid home_id.', 'mod_id' => $mod_id); + return $resultBase + array('ok' => false, 'pending' => true, 'message' => 'Invalid home_id.'); } if ($mod_id <= 0 || empty($home_info['mods'][$mod_id])) { - return array('ok' => false, 'pending' => true, 'message' => "No mod profile configured for home #{$home_id}.", 'mod_id' => $mod_id); + return $resultBase + array('ok' => false, 'pending' => true, 'message' => "No mod profile configured for home #{$home_id}."); } $server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']); if (!$server_xml) { - return array('ok' => false, 'pending' => true, 'message' => "Could not read server config XML for home #{$home_id}.", 'mod_id' => $mod_id); + return $resultBase + array('ok' => false, 'pending' => true, 'message' => "Could not read server config XML for home #{$home_id}."); } $remote = new OGPRemoteLibrary($home_info['agent_ip'], $home_info['agent_port'], $home_info['encryption_key'], $home_info['timeout']); if ($remote->status_chk() === 0) { - return array('ok' => false, 'pending' => true, 'message' => 'Agent is offline.', 'mod_id' => $mod_id); + return $resultBase + array('ok' => false, 'pending' => true, 'message' => 'Agent is offline.'); } if ($remote->is_screen_running(OGP_SCREEN_TYPE_HOME, $home_id) == 1) { - return array('ok' => false, 'pending' => false, 'message' => 'Server is running and cannot be updated.', 'mod_id' => $mod_id); + return $resultBase + array('ok' => false, 'pending' => false, 'message' => 'Server is running and cannot be updated.'); } $log_txt = ''; $update_active = $remote->get_log(OGP_SCREEN_TYPE_UPDATE, $home_id, clean_path($home_info['home_path']), $log_txt); if ($update_active == 1) { - return array('ok' => true, 'started' => true, 'already_running' => true, 'message' => 'Update already in progress.', 'mod_id' => $mod_id); + return $resultBase + array('ok' => true, 'started' => true, 'already_running' => true, 'message' => 'Update already in progress.', 'output' => $log_txt); } $modkey = $home_info['mods'][$mod_id]['mod_key'] ?? ''; $mod_xml = xml_get_mod($server_xml, $modkey); if (!$mod_xml) { - return array('ok' => false, 'pending' => true, 'message' => "Mod key '{$modkey}' not found in XML.", 'mod_id' => $mod_id); + return $resultBase + array('ok' => false, 'pending' => true, 'message' => "Mod key '{$modkey}' not found in XML."); } $installer_name = isset($mod_xml->installer_name) ? (string)$mod_xml->installer_name : (string)$modkey; @@ -65,17 +71,17 @@ if (!function_exists('gamemanager_trigger_update_install')) { $master_server_home_id = intval($options['master_server_home_id'] ?? 0); if ($master_server_home_id > 0) { if ($db->getMasterServer($home_info['remote_server_id'], $home_info['home_cfg_id']) != $master_server_home_id) { - return array('ok' => false, 'pending' => false, 'message' => 'Attempting update from non-master server.', 'mod_id' => $mod_id); + return $resultBase + array('ok' => false, 'pending' => false, 'message' => 'Attempting update from non-master server.'); } if ($master_server_home_id == $home_id) { - return array('ok' => false, 'pending' => false, 'message' => 'Cannot update from own self.', 'mod_id' => $mod_id); + return $resultBase + array('ok' => false, 'pending' => false, 'message' => 'Cannot update from own self.'); } $ms_info = $db->getGameHome($master_server_home_id); $steam_out = $remote->masterServerUpdate($home_id, $home_info['home_path'], $master_server_home_id, $ms_info['home_path'], $exec_folder_path, $exec_path, $precmd, $postcmd); if ($steam_out === 0) { - return array('ok' => false, 'pending' => true, 'message' => 'Failed to start master update.', 'mod_id' => $mod_id); + return $resultBase + array('ok' => false, 'pending' => true, 'message' => 'Failed to start master update.', 'output' => $steam_out); } - return array('ok' => true, 'started' => true, 'message' => 'Update started.', 'mod_id' => $mod_id); + return $resultBase + array('ok' => true, 'started' => true, 'message' => 'Update started.', 'output' => $steam_out); } $use_steamcmd = ((string)$server_xml->installer === "steamcmd"); @@ -105,9 +111,9 @@ if (!function_exists('gamemanager_trigger_update_install')) { $betaname, $betapwd, $login, $pass, $settings['steam_guard'] ?? '', $exec_folder_path, $exec_path, $precmd, $postcmd, $cfg_os, $lockFiles, $arch); if ($steam_out === 0) { - return array('ok' => false, 'pending' => true, 'message' => 'Failed to start SteamCMD update.', 'mod_id' => $mod_id); + return $resultBase + array('ok' => false, 'pending' => true, 'message' => 'Failed to start SteamCMD update.', 'output' => $steam_out); } - return array('ok' => true, 'started' => true, 'message' => 'Update started.', 'mod_id' => $mod_id); + return $resultBase + array('ok' => true, 'started' => true, 'message' => 'Update started.', 'output' => $steam_out); } $ran_scripts = false; @@ -120,11 +126,17 @@ if (!function_exists('gamemanager_trigger_update_install')) { $ran_scripts = true; } return array( + 'trigger' => 'gamemanager_trigger_update_install', 'ok' => true, 'started' => $ran_scripts, 'completed' => !$ran_scripts, 'message' => $ran_scripts ? 'Script install started.' : 'No installer command was required.', - 'mod_id' => $mod_id + 'mod_id' => $mod_id, + 'home_id' => $home_id, + 'output' => array( + 'precmd' => (string)$precmd, + 'postcmd' => (string)$postcmd, + ), ); } }