From acbb850e21f47c63edd3812e39cdf61229c2e1ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 23:45:27 +0000 Subject: [PATCH] fix: add_to_cart SQL mismatch, Browse Servers routing, canonical game dedup, OS-aware locations, XML editor improvements Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/aecffd5d-b644-4e4d-b13e-b392e78d4606 Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com> --- modules/billing/add_to_cart.php | 2 +- modules/billing/cart.php | 6 +- modules/billing/module.php | 19 +- modules/billing/order.php | 611 ++++++++++++++---------- modules/billing/serverlist.php | 101 ++-- modules/config_games/config_servers.php | 108 +++-- 6 files changed, 495 insertions(+), 352 deletions(-) diff --git a/modules/billing/add_to_cart.php b/modules/billing/add_to_cart.php index 74313637..2a8f5679 100644 --- a/modules/billing/add_to_cart.php +++ b/modules/billing/add_to_cart.php @@ -204,7 +204,7 @@ $sql = "INSERT INTO {$table_prefix}billing_invoices ( billing_status, invoice_date, due_date, description, invoice_duration, rate_type, rate_per_player, players, period_start, period_end, subtotal, total_due, payment_status, qty, coupon_id ) VALUES ( - 0, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, 0.00, 'USD', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0 + 0, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, 0.00, 'USD', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0 )"; $stmt = $db->prepare($sql); diff --git a/modules/billing/cart.php b/modules/billing/cart.php index 93e59523..be5740cc 100644 --- a/modules/billing/cart.php +++ b/modules/billing/cart.php @@ -541,7 +541,7 @@ $siteBase = $protocol . $host;

Your cart is empty

Browse our game servers and add them to your cart to get started!

- Browse Servers + Browse Servers
@@ -640,7 +640,7 @@ $siteBase = $protocol . $host;
- Continue Shopping + Continue Shopping My Account
@@ -672,7 +672,7 @@ $siteBase = $protocol . $host;
- Continue Shopping + Continue Shopping My Account
diff --git a/modules/billing/module.php b/modules/billing/module.php index 1c737175..dcfd5a14 100644 --- a/modules/billing/module.php +++ b/modules/billing/module.php @@ -24,8 +24,8 @@ // Module general information $module_title = "billing"; -$module_version = "3.4"; -$db_version = 5; +$module_version = "3.5"; +$db_version = 6; $module_required = FALSE; // Module description $module_description = "Billing storefront / provisioning integration. Public ordering runs as a standalone site; panel pages provide provisioning and admin order management."; @@ -386,4 +386,17 @@ $install_queries[5] = array( } ); -?> +// ----------------------------------------------------------------------- +// db_version 6 — Add server_os column to remote_servers for OS-aware +// game/service selection in the billing storefront. +// Default 'linux' preserves existing behaviour for all current installs. +// ----------------------------------------------------------------------- +$install_queries[6] = array( + function($db) { + $r = $db->resultQuery("SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'OGP_DB_PREFIXremote_servers' AND COLUMN_NAME = 'server_os'"); + if ($r && isset($r[0]['cnt']) && (int)$r[0]['cnt'] > 0) return true; + return (bool)$db->query("ALTER TABLE `OGP_DB_PREFIXremote_servers` ADD `server_os` ENUM('linux','windows','any') NOT NULL DEFAULT 'linux' AFTER `display_public_ip`"); + } +); + +?> \ No newline at end of file diff --git a/modules/billing/order.php b/modules/billing/order.php index 14b03283..596348e3 100644 --- a/modules/billing/order.php +++ b/modules/billing/order.php @@ -9,15 +9,13 @@ ", $_POST['description']); - $service = $_POST['service_id']; - - $change_description = "UPDATE {$table_prefix}billing_services - SET description ='".$new_description."' - WHERE service_id=".$service; - $save = $db->query($change_description); - } - ?> - - +if (isset($_POST['save']) && !empty($_POST['description'])) { + $new_description = str_replace("\\r\\n", "
", $_POST['description']); + $service = intval($_POST['service_id']); + $stmt = $db->prepare("UPDATE {$table_prefix}billing_services SET description = ? WHERE service_id = ?"); + if ($stmt) { + $stmt->bind_param("si", $new_description, $service); + $stmt->execute(); + $stmt->close(); + } +} +/** + * Derive OS ('linux'|'windows'|'any') from a game_key string. + * Checks for _win / _windows substrings; then _linux; else 'any'. + */ +function order_game_key_os(string $gameKey): string +{ + $lk = strtolower($gameKey); + if (str_contains($lk, '_win')) { + return 'windows'; + } + if (str_contains($lk, '_linux')) { + return 'linux'; + } + return 'any'; +} - - - - query($qry_services); - - if ($services_result === false) { - echo "

Unable to load service information. Please try again or contact support.

"; - error_log("billing order.php: query failed - " . $db->error . " | SQL: " . $qry_services); - billing_maybe_close_db($db); - include(__DIR__ . '/includes/footer.php'); - echo ''; - exit; - } - - // Fetch all rows into an array so foreach works correctly - $serviceRows = []; - while ($row = $services_result->fetch_assoc()) { - $serviceRows[] = $row; - } - $services_result->free(); - - if ($req_service_id !== 0 && empty($serviceRows)) { - error_log("billing order.php: service_id={$req_service_id} not found or not enabled"); - echo "

The requested service could not be found or is no longer available.

"; - echo "

Back to server list

"; - billing_maybe_close_db($db); - include(__DIR__ . '/includes/footer.php'); - echo ''; - exit; - } - -?> -
- -
- - - - -
- -
-query($qry_services); + +if ($services_result === false) { + // Fallback: query without join if config_homes doesn't exist in this context + $where_service_id_simple = str_replace('bs.', '', $where_service_id); + $qry_services = "SELECT *, NULL AS cfg_game_name, NULL AS cfg_game_key + FROM {$table_prefix}billing_services + {$where_service_id_simple} + ORDER BY service_name"; + $services_result = $db->query($qry_services); +} + +if ($services_result === false) { + echo "

Unable to load service information. Please try again or contact support.

"; + error_log("billing order.php: query failed - " . $db->error); + billing_maybe_close_db($db); + include(__DIR__ . '/includes/footer.php'); + echo ''; + exit; +} + +$serviceRows = []; +while ($row = $services_result->fetch_assoc()) { + $serviceRows[] = $row; +} +$services_result->free(); + +if ($req_service_id !== 0 && empty($serviceRows)) { + error_log("billing order.php: service_id={$req_service_id} not found or not enabled"); + echo "

The requested service could not be found or is no longer available.

"; + echo "

Back to server list

"; + billing_maybe_close_db($db); + include(__DIR__ . '/includes/footer.php'); + echo ''; + exit; +} + +// Check whether remote_servers has a server_os column (added by db_version 6 migration). +// We gracefully degrade: if the column is absent, all servers are treated as compatible. +$hasServerOsColumn = false; +$osColCheck = $db->query("SHOW COLUMNS FROM {$table_prefix}remote_servers LIKE 'server_os'"); +if ($osColCheck && $osColCheck->num_rows > 0) { + $hasServerOsColumn = true; + $osColCheck->free(); +} + +?> +
+ +
+ + +
+ +
+
-
- - - - -
- - - - -
+Order Now
- - - - - - -
- - -
- isAdmin($_SESSION['user_id'] ); - - $isAdmin = false; - if($isAdmin) - { - if(!isset($_POST['edit'])) - { - echo "

$row[description]

"; - echo "
". - "". - "". - ""; - } - else - { - echo "
". - "
". - "". - "". - ""; - } - } - else - echo "

$row[description]

"; - ?> -

-
- - - - - - - - - - + + + + + + + + +
Game Server Name - -
Location - query($rsQuery); - if ($rsResult) { - $firstServer = true; - while ($rs = $rsResult->fetch_assoc()) { - $rsID = (int)$rs['remote_server_id']; - $rsNAME = htmlspecialchars((string)$rs['remote_server_name'], ENT_QUOTES, 'UTF-8'); - $checked = $firstServer ? ' checked' : ''; - $available_server = true; - $firstServer = false; - echo "
\n" - . " \n" - . " \n" - . "
\n"; - } - } - } - ?> + service_id, 'windows' => service_id] +if ($svcGameOs !== 'any' && !empty($canonicalGameName)) { +$escapedName = $db->real_escape_string($canonicalGameName); +$siblingQuery = "SELECT bs.service_id, ch.game_key AS cfg_game_key + FROM {$table_prefix}billing_services bs + LEFT JOIN {$table_prefix}config_homes ch ON ch.home_cfg_id = bs.home_cfg_id + WHERE bs.enabled = 1 AND ch.game_name = '{$escapedName}'"; +$siblingResult = $db->query($siblingQuery); +if ($siblingResult) { +while ($sib = $siblingResult->fetch_assoc()) { +$sibOs = order_game_key_os((string)($sib['cfg_game_key'] ?? '')); +$osServiceMap[$sibOs] = (int)$sib['service_id']; +} +$siblingResult->free(); +} +} +// Always include the current service as a fallback +if (!isset($osServiceMap[$svcGameOs]) || $svcGameOs === 'any') { +$osServiceMap[$svcGameOs] = (int)$row['service_id']; +} +$osServiceMapJson = json_encode($osServiceMap, JSON_THROW_ON_ERROR); +?> +
+ + +
+" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "

"; +echo "" + . "" + . "" + . ""; +} else { +$descEditable = htmlspecialchars(str_replace("
", "\r\n", (string)($row['description'] ?? '')), ENT_QUOTES, 'UTF-8'); +echo "
" + . "
" + . "" + . "" + . "
"; +} +} else { +echo "

" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "

"; +} +?> +
+ + + + + + + + + + + + - - - - - - - - - - - -
Game Server Name + +
Location + -
Configure -
-
Player Slots
- -
Months
- +// Also gather allowed IDs from sibling OS-variant services +$allAllowedIds = []; +foreach (explode(',', $remoteIdsCsv) as $part) { +$part = trim($part); +if ($part !== '' && ctype_digit($part)) { +$allAllowedIds[] = (int)$part; +} +} +// Add IDs from sibling service variants so locations appear regardless of which +// service variant is the "primary" one shown to the user. +if (count($osServiceMap) > 1) { +foreach ($osServiceMap as $_os => $sibSvcId) { +if ($sibSvcId === (int)$row['service_id']) continue; +$sibRow = $db->query("SELECT remote_server_id FROM {$table_prefix}billing_services WHERE service_id = " . intval($sibSvcId) . " LIMIT 1"); +if ($sibRow && ($sibData = $sibRow->fetch_assoc())) { +foreach (explode(',', (string)($sibData['remote_server_id'] ?? '')) as $part) { +$part = trim($part); +if ($part !== '' && ctype_digit($part)) { +$allAllowedIds[] = (int)$part; +} +} +$sibRow->free(); +} +} +} +$allAllowedIds = array_unique($allAllowedIds); -

Player Slots:
- Price: $ USD
-
-

-
- - - - - - -
- - - - - - - - -
-
- -
-
- - +// OS-aware service variant map: {os: service_id} +var osServiceMap = ; + +function recalc() { +var slots = parseInt(slider.value, 10); +var months = parseInt(invoiceslider.value, 10); +output.innerHTML = slots; +invoiceDuration.innerHTML = "Duration: " + months + " month" + (months !== 1 ? "s" : ""); +price.innerHTML = "Total Price: $" + (slots * months * pricePerSlot).toFixed(2); +} +recalc(); +slider.oninput = recalc; +invoiceslider.oninput = recalc; + +// Update the hidden service_id based on the selected location's OS. +window.gspUpdateServiceId = function(radio) { +var os = radio.getAttribute('data-os') || 'any'; +var svcInput = document.getElementById('order_service_id'); +if (!svcInput) return; +// Pick the service for this OS, fall back to 'any', then first available +if (osServiceMap[os] !== undefined) { +svcInput.value = osServiceMap[os]; +} else if (osServiceMap['any'] !== undefined) { +svcInput.value = osServiceMap['any']; +} +// else keep the current value +}; + +// Trigger on page load for the pre-checked radio +var checked = document.querySelector('input[name="ip_id"]:checked'); +if (checked) { window.gspUpdateServiceId(checked); } +})(); + + + +
+ + + + + + +

No available server locations for this game.

+ + +
+
+ +
+
+ + diff --git a/modules/billing/serverlist.php b/modules/billing/serverlist.php index b235b642..e8887838 100644 --- a/modules/billing/serverlist.php +++ b/modules/billing/serverlist.php @@ -7,10 +7,6 @@ close(); } -// Fetch services +// Fetch services, joining config_homes to get canonical game_name and game_key for OS detection. +// LEFT JOIN so services without a linked config_homes entry still appear. $service_id = isset($_REQUEST['service_id']) ? intval($_REQUEST['service_id']) : 0; -$where_service_id = $service_id !== 0 - ? "WHERE enabled = 1 AND service_id = $service_id AND remote_server_id != '' AND remote_server_id IS NOT NULL" - : "WHERE enabled = 1 AND remote_server_id != '' AND remote_server_id IS NOT NULL"; -$qry_services = "SELECT * FROM {$table_prefix}billing_services $where_service_id ORDER BY service_name"; +if ($service_id !== 0) { + $where_clause = "WHERE bs.enabled = 1 AND bs.service_id = {$service_id} AND bs.remote_server_id != '' AND bs.remote_server_id IS NOT NULL"; +} else { + $where_clause = "WHERE bs.enabled = 1 AND bs.remote_server_id != '' AND bs.remote_server_id IS NOT NULL"; +} +$qry_services = "SELECT bs.*, ch.game_name AS cfg_game_name, ch.game_key AS cfg_game_key + FROM {$table_prefix}billing_services bs + LEFT JOIN {$table_prefix}config_homes ch ON ch.home_cfg_id = bs.home_cfg_id + {$where_clause} + ORDER BY bs.service_name"; $result_services = $db->query($qry_services); +if (!$result_services) { + // config_homes join may not exist on all installs; fall back to services-only query + $where_clause_fallback = str_replace('bs.', '', $where_clause); + $qry_services_fallback = "SELECT *, NULL AS cfg_game_name, NULL AS cfg_game_key + FROM {$table_prefix}billing_services + {$where_clause_fallback} + ORDER BY service_name"; + $result_services = $db->query($qry_services_fallback); +} + if (!$result_services) { echo ""; billing_maybe_close_db($db); return; } -// Fetch all service rows into an array so the template foreach works correctly +// Fetch all service rows and deduplicate by canonical game name so that +// arma3_linux64 and arma3_win64 (both named "Arma 3") appear only once. +// When a specific service_id is requested we skip deduplication. $serviceRows = []; +$seenCanonical = []; while ($row = $result_services->fetch_assoc()) { + if ($service_id !== 0) { + // Single-service detail view: always include without deduplication + $serviceRows[] = $row; + continue; + } + // Derive canonical display name: prefer config_homes game_name (consistent across OS + // variants), fall back to service_name. + $canonicalName = !empty($row['cfg_game_name']) + ? $row['cfg_game_name'] + : $row['service_name']; + + if (isset($seenCanonical[$canonicalName])) { + // Already have this game — skip the duplicate OS variant + continue; + } + $seenCanonical[$canonicalName] = true; $serviceRows[] = $row; } $result_services->free(); @@ -69,13 +101,18 @@ include(__DIR__ . '/includes/menu.php');
- - -
- -
+
+
+
@@ -84,44 +121,48 @@ include(__DIR__ . '/includes/menu.php');
- - -
- -
+ +
+
{$row['description']}

"; + echo "

" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "

"; echo "
- +
"; } else { - $desc = str_replace("
", "\r\n", $row['description']); + $desc = htmlspecialchars(str_replace("
", "\r\n", (string)($row['description'] ?? '')), ENT_QUOTES, 'UTF-8'); echo "
-
- +
+
"; } } else { - echo "

{$row['description']}

"; + echo "

" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "

"; } ?>
- + - + diff --git a/modules/config_games/config_servers.php b/modules/config_games/config_servers.php index 902be6c8..6ead9475 100644 --- a/modules/config_games/config_servers.php +++ b/modules/config_games/config_servers.php @@ -233,61 +233,61 @@ function config_games_print_editor_css() $printed = true; echo << -.xml-editor-wrapper{margin:20px 0;padding:12px;background:#111;border:1px solid #222;border-radius:8px} -.xml-node{border:1px solid #333;border-radius:6px;padding:12px;margin-bottom:10px;background:#181818} +.xml-editor-wrapper{margin:20px 0;padding:14px;background:#111;border:1px solid #222;border-radius:8px;font-size:1rem} +.xml-node{border:1px solid #333;border-radius:6px;padding:14px;margin-bottom:12px;background:#181818} .xml-node--required{border-left:3px solid #1c6dd0} -.xml-node__header{display:flex;justify-content:space-between;align-items:center;gap:12px;border-bottom:1px solid #2a2a2a;padding-bottom:6px;margin-bottom:8px} -.xml-node__title{font-weight:600;color:#f5f5f5} -.xml-node__title--required::after{content:" *";color:#e06c75;font-size:0.8rem} -.xml-node__path{font-size:0.85rem;color:#989898} -.xml-node__badge{font-size:0.72rem;padding:2px 6px;border-radius:3px;text-transform:uppercase;letter-spacing:0.05em;margin-left:6px} +.xml-node__header{display:flex;justify-content:space-between;align-items:center;gap:12px;border-bottom:1px solid #2a2a2a;padding-bottom:8px;margin-bottom:10px} +.xml-node__title{font-weight:600;color:#f5f5f5;font-size:1rem} +.xml-node__title--required::after{content:" *";color:#e06c75;font-size:0.85rem} +.xml-node__path{font-size:0.88rem;color:#b0b0b0} +.xml-node__badge{font-size:0.75rem;padding:2px 7px;border-radius:3px;text-transform:uppercase;letter-spacing:0.05em;margin-left:6px} .xml-node__badge--required{background:#1c3a6d;color:#7eb3f0} -.xml-node__badge--optional{background:#2a2a2a;color:#888} -.xml-node__body label{font-size:0.85rem;color:#bbb;display:block;margin-bottom:4px} -.xml-node__body input[type="text"], .xml-node__body textarea, .xml-node__body select{width:100%;padding:8px;border:1px solid #3a3a3a;border-radius:4px;background:#101010;color:#fff;font-family:monospace} -.xml-node__body textarea{min-height:120px} +.xml-node__badge--optional{background:#2a2a2a;color:#aaa} +.xml-node__body label{font-size:0.9rem;color:#d0d0d0;display:block;margin-bottom:5px} +.xml-node__body input[type="text"], .xml-node__body textarea, .xml-node__body select{width:100%;padding:8px 10px;border:1px solid #3a3a3a;border-radius:4px;background:#101010;color:#f0f0f0;font-family:monospace;font-size:0.93rem} +.xml-node__body textarea{min-height:130px;line-height:1.4} .xml-node__attributes{margin-top:8px} .xml-node__attributes .attr-row{display:flex;gap:8px;align-items:center;margin-bottom:6px} .xml-node__attributes .attr-row input[type="text"]{flex:1} -.xml-children{margin-top:10px;border-left:2px solid #2a2a2a;padding-left:12px} +.xml-children{margin-top:10px;border-left:2px solid #2a2a2a;padding-left:14px} .xml-actions{display:flex;justify-content:flex-end;margin-top:16px;padding:8px 18px 0} .xml-node__actions{display:flex;gap:8px;align-items:center} -.xml-node__apply{background:#1c6dd0;border:1px solid #114b99;color:#fff;padding:6px 12px;border-radius:4px;cursor:pointer} +.xml-node__apply{background:#1c6dd0;border:1px solid #114b99;color:#fff;padding:6px 14px;border-radius:4px;cursor:pointer;font-size:0.93rem} .xml-node__apply:hover{background:#1f7aec} -.xml-global-save{background:#1c6dd0;border:1px solid #114b99;color:#fff;padding:10px 28px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:0.03em;cursor:pointer;transition:background 0.2s ease,transform 0.2s ease;box-shadow:0 2px 6px rgba(0,0,0,0.35)} +.xml-global-save{background:#1c6dd0;border:1px solid #114b99;color:#fff;padding:10px 28px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:0.03em;cursor:pointer;transition:background 0.2s ease,transform 0.2s ease;box-shadow:0 2px 6px rgba(0,0,0,0.35);font-size:0.95rem} .xml-global-save:hover{background:#1f7aec;transform:translateY(-1px)} .xml-global-save--top{float:right;margin:0 18px 12px 0} -.xml-hint{font-size:0.85rem;color:#999;margin-top:4px} -.xml-validation-errors{background:#2d0f0f;border:1px solid #8b1c1c;border-radius:6px;padding:12px 16px;margin-bottom:14px;color:#f88} +.xml-hint{font-size:0.88rem;color:#b0b0b0;margin-top:5px} +.xml-validation-errors{background:#2d0f0f;border:1px solid #8b1c1c;border-radius:6px;padding:12px 16px;margin-bottom:14px;color:#ffaaaa;font-size:0.93rem} .xml-validation-errors ul{margin:6px 0 0 16px;padding:0} -.xml-raw-toggle{margin:8px 0 4px;color:#7eb3f0;cursor:pointer;font-size:0.9rem;text-decoration:underline;background:none;border:none;padding:0} +.xml-raw-toggle{margin:8px 0 4px;color:#7eb3f0;cursor:pointer;font-size:0.95rem;text-decoration:underline;background:none;border:none;padding:0} .xml-raw-section{margin-top:10px;display:none} -.xml-raw-section textarea{width:100%;min-height:300px;font-family:monospace;font-size:0.85rem;background:#0c0c0c;color:#eee;border:1px solid #3a3a3a;border-radius:4px;padding:8px} -.xml-raw-warning{background:#2d2200;border:1px solid #7a5a00;border-radius:4px;padding:8px 12px;color:#f0c050;font-size:0.85rem;margin-bottom:6px} -.xml-section-header{margin:20px 0 4px;font-size:0.8rem;color:#888;text-transform:uppercase;letter-spacing:0.1em;border-bottom:1px solid #2a2a2a;padding-bottom:4px} -.xml-node__desc{font-size:0.82rem;color:#aaa;background:#0e0e0e;border-left:3px solid #2a4a7a;padding:6px 10px;margin:6px 0 8px;border-radius:0 4px 4px 0} -.xml-node__options{margin:4px 0 4px 12px;padding:0;list-style:disc inside} -.xml-node__options li{margin-bottom:2px} -.xml-node__options code{color:#7eb3f0;background:rgba(30,100,200,0.12);padding:1px 4px;border-radius:3px} -.xml-node__example{display:block;margin-top:4px;color:#888} -.xml-node__example code{color:#a0d0a0;background:rgba(30,150,50,0.1);padding:1px 4px;border-radius:3px} -.xml-jump-link{display:inline-block;margin-bottom:12px;padding:6px 14px;background:#1c6dd0;color:#fff;border-radius:4px;text-decoration:none;font-size:0.9rem} +.xml-raw-section textarea{width:100%;min-height:320px;font-family:monospace;font-size:0.9rem;background:#0c0c0c;color:#f0f0f0;border:1px solid #3a3a3a;border-radius:4px;padding:10px;line-height:1.4} +.xml-raw-warning{background:#2d2200;border:1px solid #7a5a00;border-radius:4px;padding:10px 14px;color:#f0c050;font-size:0.9rem;margin-bottom:8px} +.xml-section-header{margin:22px 0 6px;font-size:0.85rem;color:#aaa;text-transform:uppercase;letter-spacing:0.1em;border-bottom:1px solid #2a2a2a;padding-bottom:5px} +.xml-node__desc{font-size:0.88rem;color:#c8c8c8;background:#0e0e0e;border-left:3px solid #2a4a7a;padding:8px 12px;margin:8px 0 10px;border-radius:0 4px 4px 0;line-height:1.5} +.xml-node__options{margin:6px 0 4px 14px;padding:0;list-style:disc inside} +.xml-node__options li{margin-bottom:3px;font-size:0.9rem} +.xml-node__options code{color:#7eb3f0;background:rgba(30,100,200,0.12);padding:1px 5px;border-radius:3px} +.xml-node__example{display:block;margin-top:6px;color:#aaa;font-size:0.88rem} +.xml-node__example code{color:#a0d0a0;background:rgba(30,150,50,0.1);padding:1px 5px;border-radius:3px} +.xml-jump-link{display:inline-block;margin-bottom:12px;padding:7px 16px;background:#1c6dd0;color:#fff;border-radius:4px;text-decoration:none;font-size:0.93rem} .xml-jump-link:hover{background:#1f7aec;text-decoration:none} -.xml-section-grid{display:flex;flex-direction:column;gap:14px;margin-bottom:18px} -.xml-section-block{border:1px solid #303030;border-radius:6px;background:#141414;padding:12px} -.xml-section-block__head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:8px} -.xml-section-block__title{font-size:1.02rem;color:#f0f0f0;font-weight:600} -.xml-section-block__meta{font-size:0.8rem;color:#9f9f9f} -.xml-section-block__desc{font-size:0.86rem;color:#b0b0b0;margin:0 0 10px} -.xml-section-block textarea{width:100%;min-height:170px;background:#0f0f0f;border:1px solid #3c3c3c;border-radius:4px;color:#f7f7f7;padding:8px;font-family:monospace;font-size:0.84rem} -.xml-section-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px} -.xml-btn{border:1px solid #3f3f3f;background:#222;color:#fff;padding:6px 10px;border-radius:4px;cursor:pointer} +.xml-section-grid{display:flex;flex-direction:column;gap:16px;margin-bottom:20px} +.xml-section-block{border:1px solid #303030;border-radius:6px;background:#141414;padding:14px} +.xml-section-block__head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:10px} +.xml-section-block__title{font-size:1.05rem;color:#f0f0f0;font-weight:600} +.xml-section-block__meta{font-size:0.83rem;color:#b0b0b0} +.xml-section-block__desc{font-size:0.9rem;color:#c8c8c8;margin:0 0 12px;line-height:1.5} +.xml-section-block textarea{width:100%;min-height:180px;background:#0f0f0f;border:1px solid #3c3c3c;border-radius:4px;color:#f7f7f7;padding:10px;font-family:monospace;font-size:0.9rem;line-height:1.4} +.xml-section-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px} +.xml-btn{border:1px solid #3f3f3f;background:#222;color:#e8e8e8;padding:7px 12px;border-radius:4px;cursor:pointer;font-size:0.9rem} .xml-btn:hover{background:#2a2a2a} -.xml-btn--primary{background:#1c6dd0;border-color:#114b99} +.xml-btn--primary{background:#1c6dd0;border-color:#114b99;color:#fff} .xml-btn--primary:hover{background:#1f7aec} -.xml-btn--danger{background:#6b1f1f;border-color:#8d2d2d} +.xml-btn--danger{background:#6b1f1f;border-color:#8d2d2d;color:#ffcccc} .xml-btn--danger:hover{background:#8d2d2d} -.xml-add-section{border:1px dashed #3a3a3a;border-radius:6px;padding:10px;margin-bottom:16px} +.xml-add-section{border:1px dashed #3a3a3a;border-radius:6px;padding:12px;margin-bottom:18px} .xml-add-section select{min-width:260px} CSS; @@ -730,6 +730,23 @@ function config_games_render_top_level_editor($home_cfg_id, $configFile) echo ""; echo ""; } + + // Render missing optional schema sections as informational cards with an Add button. + $descriptions = config_games_tag_descriptions(); + foreach ($optionalMissing as $missingName) { + $safeMissing = htmlspecialchars($missingName, ENT_QUOTES, 'UTF-8'); + $missingDesc = htmlspecialchars($descriptions[$missingName]['desc'] ?? 'Optional configuration section.', ENT_QUOTES, 'UTF-8'); + echo "
"; + echo "
{$safeMissing}
Optional — not in this file
"; + echo "

This section is not currently in this XML file. You can add it if needed.

"; + echo "

{$missingDesc}

"; + echo "
"; + echo ""; + echo ""; + echo "
"; + echo ""; + echo "
"; + } echo ""; } @@ -1134,22 +1151,11 @@ function exec_ogp_module() { echo "
"; config_games_render_top_level_editor($home_cfg_id, $config_file); - echo "
Open legacy detailed node editor (previous default editor)"; - echo "
"; - echo ""; - echo ""; - echo "
"; - echo config_games_render_editor($xml); - echo "
"; - echo "

★ = required field. Use the action dropdown to remove entire sections. Attribute values left blank will be removed. Script sections such as post_install are fully editable. Changes are validated against the schema before saving.

"; - echo ""; - echo "
"; - // Raw XML editor echo "
"; echo "

Full Raw XML Editor

"; echo "
Warning: Saving raw XML bypasses the guided editor. The file will be validated against the schema before saving. Invalid XML will be rejected.
"; - echo ""; + echo ""; echo "
"; echo "
"; echo "";
Game Server Name