fix: billing admin service list UI and customer server list display

- adminserverlist.php: fix th color (dark bg #2c3e50 + light text #f0f0f0)
  and add position:sticky to thead so header rows stay visible while scrolling
- adminserverlist.php: remove Out of Stock column from UI (thead, tbody),
  save handler (no longer reads/writes out_of_stock), and sync logic
  (soft-disable no longer sets out_of_stock = 1; new rows no longer insert it)
- adminserverlist.php: normalize price inputs to step=0.01 / 2 decimal places
- serverlist.php: fix foreach on mysqli_result cast bug that silently
  prevented all services from rendering; now uses fetch_assoc() loop
- serverlist.php: add IS NOT NULL guard alongside the != '' check

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/3474562e-25f4-4d89-a030-f227e11b609b

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-03 22:39:49 +00:00 committed by GitHub
parent d5557a0145
commit a484974c06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 34 additions and 34 deletions

View file

@ -6,16 +6,18 @@
<title>Admin Service Configuration - GSP</title>
<style>
.svc-table { border-collapse: collapse; width: 100%; }
.svc-table th, .svc-table td { border: 1px solid #ddd; padding: 6px 8px; vertical-align: middle; }
.svc-table th { background: #f5f5f5; white-space: nowrap; text-align: center; }
.svc-table th, .svc-table td { border: 1px solid #4a6080; padding: 6px 8px; vertical-align: middle; }
/* Sticky header: stays visible while scrolling; dark background with light text for readability */
.svc-table thead th { position: sticky; top: 0; z-index: 10; background: #2c3e50; color: #f0f0f0; white-space: nowrap; text-align: center; }
.svc-table thead th.game-name { text-align: left; }
.svc-table td.game-name { text-align: left; white-space: nowrap; }
.price-input { width: 80px; }
.slot-input { width: 60px; }
.desc-input { width: 160px; }
.img-input { width: 160px; }
.muted { color: #999; font-size: 0.85em; }
.flash-ok { background: #d4edda; border: 1px solid #c3e6cb; padding: 8px 12px; margin-bottom: 10px; border-radius: 4px; }
.flash-err { background: #f8d7da; border: 1px solid #f5c6cb; padding: 8px 12px; margin-bottom: 10px; border-radius: 4px; }
.flash-ok { background: #d4edda; border: 1px solid #c3e6cb; padding: 8px 12px; margin-bottom: 10px; border-radius: 4px; color: #155724; }
.flash-err { background: #f8d7da; border: 1px solid #f5c6cb; padding: 8px 12px; margin-bottom: 10px; border-radius: 4px; color: #721c24; }
.servers-cell { text-align: left; }
.server-cb-label { display: block; white-space: nowrap; margin: 2px 0; }
</style>
@ -41,7 +43,7 @@
* home_cfg_id home_cfg_id (sync key)
*
* Columns that are admin-editable and NEVER overwritten by sync:
* enabled, out_of_stock, slot_min_qty, slot_max_qty,
* enabled, slot_min_qty, slot_max_qty,
* price_daily, price_monthly, price_year,
* remote_server_id, description, img_url
*/
@ -78,7 +80,6 @@ function sync_billing_services(mysqli $db, string $prefix): array
'home_cfg_id' => "ADD COLUMN `home_cfg_id` INT(11) NOT NULL DEFAULT 0",
'description' => "ADD COLUMN `description` VARCHAR(1000) NOT NULL DEFAULT ''",
'img_url' => "ADD COLUMN `img_url` VARCHAR(255) NOT NULL DEFAULT ''",
'out_of_stock' => "ADD COLUMN `out_of_stock` TINYINT(1) NOT NULL DEFAULT 0",
'slot_min_qty' => "ADD COLUMN `slot_min_qty` INT(11) NOT NULL DEFAULT 1",
'slot_max_qty' => "ADD COLUMN `slot_max_qty` INT(11) NOT NULL DEFAULT 100",
'price_daily' => "ADD COLUMN `price_daily` FLOAT(15,4) NOT NULL DEFAULT 0",
@ -126,7 +127,7 @@ function sync_billing_services(mysqli $db, string $prefix): array
// Load existing billing_services indexed by home_cfg_id.
$existing = [];
$svcRes = $db->query(
"SELECT service_id, home_cfg_id, enabled, out_of_stock
"SELECT service_id, home_cfg_id, enabled
FROM `{$tableName}`"
);
if ($svcRes) {
@ -149,13 +150,13 @@ function sync_billing_services(mysqli $db, string $prefix): array
$db->query(
"INSERT INTO `{$tableName}`
(home_cfg_id, mod_cfg_id, service_name, description,
remote_server_id, enabled, out_of_stock,
remote_server_id, enabled,
price_daily, price_monthly, price_year,
slot_min_qty, slot_max_qty,
img_url, ftp, install_method, manual_url, access_rights)
VALUES
({$homeCfgId}, 0, '{$svcName}', '{$svcName}',
'', 0, 0,
'', 0,
0.00, 0.00, 0.00,
1, 100,
'', '', 'steamcmd', '', '')"
@ -169,7 +170,7 @@ function sync_billing_services(mysqli $db, string $prefix): array
$sid = (int)$svcRow['service_id'];
$db->query(
"UPDATE `{$tableName}`
SET enabled = 0, out_of_stock = 1
SET enabled = 0
WHERE service_id = {$sid} AND enabled = 1"
);
if ($db->affected_rows > 0) {
@ -206,10 +207,9 @@ if (isset($_POST['save_services'])) {
foreach ((array)$postedServices as $sid => $svcData) {
$sid = (int)$sid;
$enabled = isset($svcData['enabled']) ? 1 : 0;
$outOfStock = isset($svcData['out_of_stock']) ? 1 : 0;
$priceDaily = number_format((float)($svcData['price_daily'] ?? 0), 4, '.', '');
$priceMonthly = number_format((float)($svcData['price_monthly'] ?? 0), 4, '.', '');
$priceYear = number_format((float)($svcData['price_year'] ?? 0), 4, '.', '');
$priceDaily = number_format((float)($svcData['price_daily'] ?? 0), 2, '.', '');
$priceMonthly = number_format((float)($svcData['price_monthly'] ?? 0), 2, '.', '');
$priceYear = number_format((float)($svcData['price_year'] ?? 0), 2, '.', '');
$slotMin = max(1, (int)($svcData['slot_min_qty'] ?? 1));
$slotMax = max(1, (int)($svcData['slot_max_qty'] ?? 1));
if ($slotMax < $slotMin) { $slotMax = $slotMin; }
@ -229,7 +229,6 @@ if (isset($_POST['save_services'])) {
$db->query(
"UPDATE `{$table_prefix}billing_services`
SET enabled = {$enabled},
out_of_stock = {$outOfStock},
price_daily = '{$priceDaily}',
price_monthly = '{$priceMonthly}',
price_year = '{$priceYear}',
@ -260,7 +259,7 @@ while ($rsRes && ($row = $rsRes->fetch_assoc())) {
$services = [];
$svcRes = $db->query(
"SELECT bs.service_id, bs.service_name, bs.enabled, bs.out_of_stock,
"SELECT bs.service_id, bs.service_name, bs.enabled,
bs.price_daily, bs.price_monthly, bs.price_year,
bs.slot_min_qty, bs.slot_max_qty,
bs.remote_server_id, bs.description, bs.img_url,
@ -301,7 +300,6 @@ while ($svcRes && ($row = $svcRes->fetch_assoc())) {
<th class="game-name">Game Name</th>
<th>Config XML</th>
<th>Enabled</th>
<th>Out of Stock</th>
<th>Min Slots</th>
<th>Max Slots</th>
<th>Price / Day ($)</th>
@ -316,7 +314,6 @@ while ($svcRes && ($row = $svcRes->fetch_assoc())) {
<?php foreach ((array)$services as $svc):
$sid = (int)$svc['service_id'];
$svcEnabled = (int)$svc['enabled'];
$svcOutOfStock = (int)$svc['out_of_stock'];
$cfgFile = (string)($svc['home_cfg_file'] ?? '');
// Parse existing remote_server_id CSV into a set for fast checkbox lookup
@ -344,12 +341,6 @@ while ($svcRes && ($row = $svcRes->fetch_assoc())) {
<?php echo $svcEnabled ? 'checked' : ''; ?>>
</td>
<td style="text-align:center;">
<input type="hidden" name="svc[<?php echo $sid; ?>][out_of_stock]" value="0">
<input type="checkbox" name="svc[<?php echo $sid; ?>][out_of_stock]" value="1"
<?php echo $svcOutOfStock ? 'checked' : ''; ?>>
</td>
<td>
<input type="number" min="1" class="slot-input"
name="svc[<?php echo $sid; ?>][slot_min_qty]"
@ -363,21 +354,21 @@ while ($svcRes && ($row = $svcRes->fetch_assoc())) {
</td>
<td>
<input type="number" step="0.0001" min="0" class="price-input"
<input type="number" step="0.01" min="0" class="price-input"
name="svc[<?php echo $sid; ?>][price_daily]"
value="<?php echo h(number_format((float)$svc['price_daily'], 4, '.', '')); ?>">
value="<?php echo h(number_format((float)$svc['price_daily'], 2, '.', '')); ?>">
</td>
<td>
<input type="number" step="0.0001" min="0" class="price-input"
<input type="number" step="0.01" min="0" class="price-input"
name="svc[<?php echo $sid; ?>][price_monthly]"
value="<?php echo h(number_format((float)$svc['price_monthly'], 4, '.', '')); ?>">
value="<?php echo h(number_format((float)$svc['price_monthly'], 2, '.', '')); ?>">
</td>
<td>
<input type="number" step="0.0001" min="0" class="price-input"
<input type="number" step="0.01" min="0" class="price-input"
name="svc[<?php echo $sid; ?>][price_year]"
value="<?php echo h(number_format((float)$svc['price_year'], 4, '.', '')); ?>">
value="<?php echo h(number_format((float)$svc['price_year'], 2, '.', '')); ?>">
</td>
<td>

View file

@ -39,16 +39,25 @@ if (isset($_POST['save']) && !empty($_POST['description'])) {
// Fetch services
$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 != ''" : "WHERE enabled = 1 AND remote_server_id != ''";
$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";
$services = $db->query($qry_services);
$result_services = $db->query($qry_services);
if (!$services) {
if (!$result_services) {
echo "<meta http-equiv='refresh' content='1'>";
billing_maybe_close_db($db);
return;
}
// Fetch all service rows into an array so the template foreach works correctly
$serviceRows = [];
while ($row = $result_services->fetch_assoc()) {
$serviceRows[] = $row;
}
$result_services->free();
// Include top bar and menu
include(__DIR__ . '/includes/top.php');
include(__DIR__ . '/includes/menu.php');
@ -56,7 +65,7 @@ include(__DIR__ . '/includes/menu.php');
<!-- Services container: clearfix to contain floated service cards so footer clears correctly -->
<div class="clearfix container-wide">
<?php foreach ((array)$services as $row): ?>
<?php foreach ($serviceRows as $row): ?>
<?php if (!isset($_REQUEST['service_id'])): ?>
<!-- Service listing (all) -->
<div class="float-left p-30-20">