feat: update wording (GitHub Stable/Unstable) + billing matrix redesign

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/4a9c8aab-3782-44a8-a5e4-01b50a813cc0

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-02 14:50:25 +00:00 committed by GitHub
parent bedffde371
commit b3d677035b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 283 additions and 382 deletions

View file

@ -1098,14 +1098,14 @@ function gsp_panel_update_section()
$finished_at = date('Y-m-d H:i:s'); $finished_at = date('Y-m-d H:i:s');
if ($result['success']) { if ($result['success']) {
print_success( print_success(
'Panel updated to development version (<strong>' . htmlspecialchars($stable_branch) . '</strong>). ' 'Panel updated to GitHub Stable (<strong>' . htmlspecialchars($stable_branch) . '</strong>). '
. intval($result['files_copied']) . ' file(s) updated. Source: <strong>' . intval($result['files_copied']) . ' file(s) updated. Source: <strong>'
. htmlspecialchars($stable_branch) . '</strong>' . htmlspecialchars($stable_branch) . '</strong>'
); );
gsp_update_log("Admin {$user_label} updated panel to stable branch {$stable_branch}"); gsp_update_log("Admin {$user_label} updated panel to GitHub Stable branch {$stable_branch}");
gsp_log_update_to_db( gsp_log_update_to_db(
'development', $stable_branch, 'success', 'development', $stable_branch, 'success',
'Updated to stable branch ' . $stable_branch . ' by ' . $_SESSION['users_login'], 'Updated to GitHub Stable branch ' . $stable_branch . ' by ' . $_SESSION['users_login'],
$result['backup_dir'] ?? null, $result['backup_dir'] ?? null,
isset($result['backup_dir']) ? $result['backup_dir'] . '/database.sql' : null, isset($result['backup_dir']) ? $result['backup_dir'] . '/database.sql' : null,
isset($result['backup_dir']) ? $result['backup_dir'] . '/panel-files.tar.gz': null, isset($result['backup_dir']) ? $result['backup_dir'] . '/panel-files.tar.gz': null,
@ -1113,10 +1113,10 @@ function gsp_panel_update_section()
); );
} else { } else {
print_failure('Update failed: ' . htmlspecialchars($result['error'])); print_failure('Update failed: ' . htmlspecialchars($result['error']));
gsp_update_log("Admin {$user_label} update to stable branch {$stable_branch} FAILED: {$result['error']}"); gsp_update_log("Admin {$user_label} update to GitHub Stable branch {$stable_branch} FAILED: {$result['error']}");
gsp_log_update_to_db( gsp_log_update_to_db(
'development', $stable_branch, 'failed', 'development', $stable_branch, 'failed',
'Update to stable branch ' . $stable_branch . ' failed: ' . $result['error'], 'Update to GitHub Stable branch ' . $stable_branch . ' failed: ' . $result['error'],
null, null, null, $started_at, $finished_at null, null, null, $started_at, $finished_at
); );
} }
@ -1127,14 +1127,14 @@ function gsp_panel_update_section()
$finished_at = date('Y-m-d H:i:s'); $finished_at = date('Y-m-d H:i:s');
if ($result['success']) { if ($result['success']) {
print_success( print_success(
'Panel updated to cutting edge version (<strong>' . htmlspecialchars($unstable_branch) . '</strong>). ' 'Panel updated to GitHub Unstable (<strong>' . htmlspecialchars($unstable_branch) . '</strong>). '
. intval($result['files_copied']) . ' file(s) updated. Source: <strong>' . intval($result['files_copied']) . ' file(s) updated. Source: <strong>'
. htmlspecialchars($unstable_branch) . '</strong>' . htmlspecialchars($unstable_branch) . '</strong>'
); );
gsp_update_log("Admin {$user_label} updated panel to unstable branch {$unstable_branch}"); gsp_update_log("Admin {$user_label} updated panel to GitHub Unstable branch {$unstable_branch}");
gsp_log_update_to_db( gsp_log_update_to_db(
'cutting-edge', $unstable_branch, 'success', 'cutting-edge', $unstable_branch, 'success',
'Updated to cutting-edge branch ' . $unstable_branch . ' by ' . $_SESSION['users_login'], 'Updated to GitHub Unstable branch ' . $unstable_branch . ' by ' . $_SESSION['users_login'],
$result['backup_dir'] ?? null, $result['backup_dir'] ?? null,
isset($result['backup_dir']) ? $result['backup_dir'] . '/database.sql' : null, isset($result['backup_dir']) ? $result['backup_dir'] . '/database.sql' : null,
isset($result['backup_dir']) ? $result['backup_dir'] . '/panel-files.tar.gz': null, isset($result['backup_dir']) ? $result['backup_dir'] . '/panel-files.tar.gz': null,
@ -1142,10 +1142,10 @@ function gsp_panel_update_section()
); );
} else { } else {
print_failure('Update failed: ' . htmlspecialchars($result['error'])); print_failure('Update failed: ' . htmlspecialchars($result['error']));
gsp_update_log("Admin {$user_label} update to unstable branch {$unstable_branch} FAILED: {$result['error']}"); gsp_update_log("Admin {$user_label} update to GitHub Unstable branch {$unstable_branch} FAILED: {$result['error']}");
gsp_log_update_to_db( gsp_log_update_to_db(
'cutting-edge', $unstable_branch, 'failed', 'cutting-edge', $unstable_branch, 'failed',
'Update to cutting-edge branch ' . $unstable_branch . ' failed: ' . $result['error'], 'Update to GitHub Unstable branch ' . $unstable_branch . ' failed: ' . $result['error'],
null, null, null, $started_at, $finished_at null, null, null, $started_at, $finished_at
); );
} }
@ -1282,32 +1282,34 @@ function gsp_panel_update_section()
echo "<br>\n"; echo "<br>\n";
// ---- Development Version ------------------------------------------------ // ---- GitHub Stable -------------------------------------------------------
echo "<h3>Development Version</h3>\n"; echo "<h3>GitHub Stable</h3>\n";
echo "<p>GitHub Stable should always match the latest official numbered release.</p>\n";
echo "<form method='POST'>\n"; echo "<form method='POST'>\n";
echo "<input type='hidden' name='gsp_update_action' value='update_stable'>\n"; echo "<input type='hidden' name='gsp_update_action' value='update_stable'>\n";
echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n"; echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n";
echo "<button type='submit'" echo "<button type='submit'"
. " onclick='return confirm(\"Back up and update the panel to the " . " onclick='return confirm(\"Back up and update the panel to GitHub Stable ("
. htmlspecialchars($stable_branch, ENT_QUOTES) . htmlspecialchars($stable_branch, ENT_QUOTES)
. " branch. Continue?\");'>" . "). Continue?\");'>"
. "Update to Development Version</button>"; . "Update to GitHub Stable</button>";
echo " <span style='margin-left:10px;color:#666;'>Branch: " echo " <span style='margin-left:10px;color:#666;'>Branch: "
. htmlspecialchars($stable_branch) . "</span>\n"; . htmlspecialchars($stable_branch) . "</span>\n";
echo "</form>\n"; echo "</form>\n";
echo "<br>\n"; echo "<br>\n";
// ---- Cutting Edge Version ----------------------------------------------- // ---- GitHub Unstable -----------------------------------------------------
echo "<h3>Cutting Edge Version</h3>\n"; echo "<h3>GitHub Unstable</h3>\n";
echo "<p>GitHub Unstable represents the latest development branch and may be unstable.</p>\n";
echo "<p class='failure' style='display:inline-block;padding:5px 10px;'>" echo "<p class='failure' style='display:inline-block;padding:5px 10px;'>"
. "&#9888; Warning: The cutting edge version may be unstable or contain bugs. Use with caution in production.</p><br><br>\n"; . "&#9888; Warning: GitHub Unstable may contain bugs or incomplete features. Use with caution in production.</p><br><br>\n";
echo "<form method='POST'>\n"; echo "<form method='POST'>\n";
echo "<input type='hidden' name='gsp_update_action' value='update_unstable'>\n"; echo "<input type='hidden' name='gsp_update_action' value='update_unstable'>\n";
echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n"; echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n";
echo "<button type='submit'" echo "<button type='submit'"
. " onclick='return confirm(\"WARNING: This is the cutting-edge (unstable) branch and may contain bugs.\\n\\nBack up and update anyway?\");'>" . " onclick='return confirm(\"WARNING: This is GitHub Unstable and may contain bugs.\\n\\nBack up and update anyway?\");'>"
. "Update to Cutting Edge Version</button>"; . "Update to GitHub Unstable</button>";
echo " <span style='margin-left:10px;color:#666;'>Branch: " echo " <span style='margin-left:10px;color:#666;'>Branch: "
. htmlspecialchars($unstable_branch) . "</span>\n"; . htmlspecialchars($unstable_branch) . "</span>\n";
echo "</form>\n"; echo "</form>\n";

View file

@ -0,0 +1,15 @@
-- Migration: add override_price to billing_service_remote_servers
-- Run once on existing installs that already have the mapping table (db_version 2)
-- but are missing the override_price column (added in db_version 3 / module v3.1).
--
-- Replace 'gsp_' with your actual table prefix if it differs.
--
-- This statement is safe to run multiple times only if your MySQL version supports
-- ADD COLUMN IF NOT EXISTS (MySQL 8.0.3+). On older versions, check first:
-- SHOW COLUMNS FROM gsp_billing_service_remote_servers LIKE 'override_price';
ALTER TABLE `gsp_billing_service_remote_servers`
ADD COLUMN IF NOT EXISTS `override_price` DECIMAL(10,2) NULL AFTER `enabled`;
-- If your MySQL is older than 8.0.3, use the conditional form instead:
-- ALTER TABLE `gsp_billing_service_remote_servers` ADD COLUMN `override_price` DECIMAL(10,2) NULL AFTER `enabled`;

View file

@ -3,389 +3,287 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Server List - GameServers.World</title> <title>Admin Server / Game Matrix - GameServers.World</title>
<style>
.matrix-table { border-collapse: collapse; width: 100%; }
.matrix-table th, .matrix-table td { border: 1px solid #ddd; padding: 6px 8px; vertical-align: middle; text-align: center; }
.matrix-table th { background: #f5f5f5; white-space: nowrap; }
.matrix-table td.game-name { text-align: left; white-space: nowrap; }
.override-input { width: 72px; margin-top: 4px; }
.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; }
</style>
</head> </head>
<body> <body>
<?php <?php
// gameservers.world admin — mysqli only, bulk + per-row update, image base URL + small button // Admin matrix page: game × server availability + price overrides
/* === SITE_BASE_URL is loaded from includes/config.inc.php; leave empty to use relative paths === */
// Include billing bootstrap (loads config and DB helper)
require_once(__DIR__ . '/bootstrap.php'); require_once(__DIR__ . '/bootstrap.php');
$siteBaseUrl = isset($SITE_BASE_URL) ? trim((string)$SITE_BASE_URL) : '';
// Protect this page: require admin
require_once(__DIR__ . '/includes/admin_auth.php'); require_once(__DIR__ . '/includes/admin_auth.php');
// Create database connection function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null); $db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
if (!$db) { if (!$db) {
die("Connection failed: " . mysqli_connect_error()); die("Connection failed: " . mysqli_connect_error());
} }
// Include top bar and menu
include(__DIR__ . '/includes/top.php'); include(__DIR__ . '/includes/top.php');
include(__DIR__ . '/includes/menu.php'); include(__DIR__ . '/includes/menu.php');
echo "<div class='panel mb-12'><strong>Need the XML field reference?</strong> "; // Ensure the mapping table exists with the override_price column
echo "<a href=\"docs/xml_notes.php\" target=\"_blank\" rel=\"noopener\">Open XML Notes</a>"; $db->query(
echo "</div>"; "CREATE TABLE IF NOT EXISTS `{$table_prefix}billing_service_remote_servers` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
/* show errors during setup */ `service_id` INT(11) NOT NULL,
@ini_set('display_errors','1'); `remote_server_id` INT(11) NOT NULL,
error_reporting(E_ALL); `enabled` TINYINT(1) NOT NULL DEFAULT 1,
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } `override_price` DECIMAL(10,2) NULL,
function esc_mysqli($db, $v){ return $db->real_escape_string($v); } PRIMARY KEY (`id`),
function fetch_all_assoc($db, $sql){ UNIQUE KEY `svc_rs` (`service_id`, `remote_server_id`),
$res = $db->query($sql); KEY `service_id` (`service_id`),
return $res ? $res->fetch_all(MYSQLI_ASSOC) : []; KEY `remote_server_id` (`remote_server_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
// Add override_price if this is an older install that already has the table without it
$chk = $db->query("SHOW COLUMNS FROM `{$table_prefix}billing_service_remote_servers` LIKE 'override_price'");
if ($chk && $chk->num_rows === 0) {
$db->query("ALTER TABLE `{$table_prefix}billing_service_remote_servers` ADD COLUMN `override_price` DECIMAL(10,2) NULL");
} }
function col_exists($db, $table, $col){
$res = $db->query("SHOW COLUMNS FROM `$table` LIKE '".$db->real_escape_string($col)."'");
return ($res && $res->num_rows > 0);
}
function parse_id_list($s){
$tokens = preg_split('/\s+/', trim((string)$s));
$out = [];
foreach ((array)$tokens as $t) {
if ($t === '') continue;
if (preg_match('/^\d+$/', $t)) $out[] = (int)$t;
}
return array_values(array_unique($out));
}
/* URL helpers for image preview */
function is_abs_url($u){ return (bool)preg_match('~^(?:https?:)?//|^data:~i', (string)$u); }
function join_base($base, $path){
$base = rtrim((string)$base, '/');
$path = ltrim((string)$path, '/');
return $base !== '' ? $base.'/'.$path : $path;
}
/* which column holds space-separated locations */
$locationCol = col_exists($db, "{$table_prefix}billing_services", 'remote_server_id') ? 'remote_server_id' :
(col_exists($db, "{$table_prefix}billing_services", 'remote_server') ? 'remote_server' : 'remote_server_id');
/* whether gsp_remote_servers has an 'enabled' column (may be missing on older installs) */
$rsHasEnabled = col_exists($db, "{$table_prefix}remote_servers", 'enabled');
$flash = []; $flash = [];
$flashType = 'ok';
/* A) Update global server location enable flags */ /* -----------------------------------------------------------------------
if (isset($_POST['update_remote_servers'])) { SAVE: matrix form submitted
$enabledIds = array_map('intval', $_POST['rs'] ?? []); ----------------------------------------------------------------------- */
$enabledSet = array_flip($enabledIds); if (isset($_POST['save_matrix'])) {
$allIds = fetch_all_assoc($db, "SELECT remote_server_id FROM {$table_prefix}remote_servers"); $postedServices = $_POST['svc'] ?? [];
foreach ((array)$allIds as $row) { $postedMappings = $_POST['map'] ?? [];
$id = (int)$row['remote_server_id'];
$e = isset($enabledSet[$id]) ? 1 : 0; foreach ((array)$postedServices as $sid => $svcData) {
if ($rsHasEnabled) { $sid = (int)$sid;
$db->query("UPDATE {$table_prefix}remote_servers SET enabled={$e} WHERE remote_server_id={$id}"); $enabled = isset($svcData['enabled']) ? 1 : 0;
$base_price = number_format((float)($svcData['base_price'] ?? 0), 2, '.', '');
$period = in_array($svcData['period'] ?? 'monthly', ['daily','monthly','yearly'], true)
? $svcData['period'] : 'monthly';
$price_col = $period === 'daily' ? 'price_daily' : ($period === 'yearly' ? 'price_year' : 'price_monthly');
$base_esc = $db->real_escape_string($base_price);
$period_esc = $db->real_escape_string($period);
$db->query(
"UPDATE `{$table_prefix}billing_services`
SET enabled = {$enabled},
`{$price_col}` = '{$base_esc}'
WHERE service_id = {$sid}"
);
} }
}
if ($rsHasEnabled) { // Upsert mappings: for every service x server pair post data received
$flash[] = "Server locations updated."; $allServerIds = [];
} else { $rsRes = $db->query("SELECT remote_server_id FROM `{$table_prefix}remote_servers`");
$flash[] = "Server locations updated (note: 'enabled' column missing from remote_servers — run add_remote_server_enabled_column.sql migration)."; while ($rsRes && ($rsRow = $rsRes->fetch_assoc())) {
} $allServerIds[] = (int)$rsRow['remote_server_id'];
}
foreach ((array)$postedServices as $sid => $ignored) {
$sid = (int)$sid;
foreach ($allServerIds as $rid) {
$mapEnabled = isset($postedMappings[$sid][$rid]['enabled']) ? 1 : 0;
$ovRaw = $postedMappings[$sid][$rid]['override_price'] ?? '';
$override = (trim($ovRaw) === '') ? 'NULL' : "'" . $db->real_escape_string(number_format((float)$ovRaw, 2, '.', '')) . "'";
$db->query(
"INSERT INTO `{$table_prefix}billing_service_remote_servers`
(service_id, remote_server_id, enabled, override_price)
VALUES ({$sid}, {$rid}, {$mapEnabled}, {$override})
ON DUPLICATE KEY UPDATE
enabled = VALUES(enabled),
override_price = VALUES(override_price)"
);
}
}
$flash[] = "Matrix saved successfully.";
} }
/* helper: update one service row from posted array */ /* -----------------------------------------------------------------------
function update_service_row(mysqli $db, string $locationCol, int $sid, array $svc){ Remove a service
$name = esc_mysqli($db, trim($svc['service_name'] ?? '')); ----------------------------------------------------------------------- */
$priceMonthly = number_format((float)($svc['price_monthly'] ?? 0), 2, '.', '');
$priceYearly = number_format((float)($svc['price_year'] ?? 0), 2, '.', '');
$priceDaily = number_format((float)($svc['price_daily'] ?? 0), 2, '.', '');
$priceMonthEsc = esc_mysqli($db, $priceMonthly);
$priceYearEsc = esc_mysqli($db, $priceYearly);
$priceDailyEsc = esc_mysqli($db, $priceDaily);
$img = esc_mysqli($db, trim($svc['img_url'] ?? ''));
$en = !empty($svc['enabled']) ? 1 : 0;
$minSlots = max(1, (int)($svc['slot_min_qty'] ?? 1));
$maxSlots = max($minSlots, (int)($svc['slot_max_qty'] ?? $minSlots));
$selected = [];
if (!empty($svc['locations']) && is_array($svc['locations'])) {
$selected = array_map('intval', $svc['locations']);
$selected = array_values(array_unique($selected));
}
$primary = isset($svc['primary_location']) ? (int)$svc['primary_location'] : 0;
if ($primary && in_array($primary, $selected, true)) {
$selected = array_values(array_diff($selected, [$primary]));
array_unshift($selected, $primary);
}
$locList = implode(' ', $selected);
$locListEsc = esc_mysqli($db, $locList);
$sql = "UPDATE {$table_prefix}billing_services
SET service_name='{$name}',
`{$locationCol}`='{$locListEsc}',
slot_min_qty={$minSlots},
slot_max_qty={$maxSlots},
price_daily='{$priceDailyEsc}',
price_monthly='{$priceMonthEsc}',
price_year='{$priceYearEsc}',
img_url='{$img}',
enabled={$en}
WHERE service_id={$sid}";
$db->query($sql);
}
/* B1) PER-ROW UPDATE */
if (isset($_POST['update_single']) && isset($_POST['service']) && is_array($_POST['service'])) {
$sid = (int)$_POST['update_single'];
if (isset($_POST['service'][$sid])) {
update_service_row($db, $locationCol, $sid, $_POST['service'][$sid]);
$flash[] = "Service #{$sid} updated.";
}
}
/* B2) BULK UPDATE (single button at bottom) */
if (isset($_POST['bulk_update']) && !empty($_POST['service']) && is_array($_POST['service'])) {
foreach ((array)$_POST['service'] as $sid => $svc) {
update_service_row($db, $locationCol, (int)$sid, (array)$svc);
}
$flash[] = "All edited services have been updated.";
}
/* C) Remove a service (separate small form) */
if (isset($_POST['remove_service'], $_POST['service_id_remove'])) { if (isset($_POST['remove_service'], $_POST['service_id_remove'])) {
$sid = (int)$_POST['service_id_remove']; $sid = (int)$_POST['service_id_remove'];
$db->query("DELETE FROM {$table_prefix}billing_services WHERE service_id={$sid}"); $db->query("DELETE FROM `{$table_prefix}billing_service_remote_servers` WHERE service_id = {$sid}");
$flash[] = "Service #{$sid} removed."; $db->query("DELETE FROM `{$table_prefix}billing_services` WHERE service_id = {$sid}");
$flash[] = "Service #{$sid} removed.";
} }
/* fetch data for UI */ /* -----------------------------------------------------------------------
// Build remote-servers query — include `enabled` only when the column exists (older installs may be missing it). Load data
if ($rsHasEnabled) { ----------------------------------------------------------------------- */
$remoteServers = fetch_all_assoc($db, "SELECT remote_server_id, remote_server_name, enabled FROM {$table_prefix}remote_servers ORDER BY remote_server_name"); $remoteServers = [];
} else { $rsRes = $db->query("SELECT remote_server_id, remote_server_name FROM `{$table_prefix}remote_servers` ORDER BY remote_server_name");
$remoteServers = fetch_all_assoc($db, "SELECT remote_server_id, remote_server_name, 1 AS enabled FROM {$table_prefix}remote_servers ORDER BY remote_server_name"); while ($rsRes && ($row = $rsRes->fetch_assoc())) {
$remoteServers[] = $row;
}
$services = [];
$svcRes = $db->query(
"SELECT service_id, service_name, enabled, price_daily, price_monthly, price_year
FROM `{$table_prefix}billing_services`
ORDER BY service_name"
);
while ($svcRes && ($row = $svcRes->fetch_assoc())) {
$services[] = $row;
}
// Load existing mappings into a lookup: $mappings[$service_id][$remote_server_id] = ['enabled'=>..,'override_price'=>..]
$mappings = [];
$mapRes = $db->query(
"SELECT service_id, remote_server_id, enabled, override_price
FROM `{$table_prefix}billing_service_remote_servers`"
);
while ($mapRes && ($row = $mapRes->fetch_assoc())) {
$mappings[(int)$row['service_id']][(int)$row['remote_server_id']] = [
'enabled' => (int)$row['enabled'],
'override_price' => $row['override_price'],
];
} }
$services = fetch_all_assoc($db, "SELECT service_id, service_name, `{$locationCol}` AS locs, slot_min_qty, slot_max_qty, price_daily, price_monthly, price_year, img_url, enabled FROM {$table_prefix}billing_services ORDER BY service_name");
?> ?>
<?php if ($flash): ?> <?php foreach ((array)$flash as $msg): ?>
<div class="panel" style="margin-bottom:12px"><?php foreach ((array)$flash as $m) echo "<div>".h($m)."</div>"; ?></div> <div class="flash-<?php echo $flashType; ?>"><?php echo h($msg); ?></div>
<?php endif; ?> <?php endforeach; ?>
<?php if (!$rsHasEnabled): ?> <h2>Game &times; Server Matrix</h2>
<div class="panel" style="margin-bottom:12px;background:#fff3cd;border:1px solid #ffc107;"> <p class="muted">
<strong> Schema notice:</strong> The <code><?php echo h("{$table_prefix}remote_servers"); ?></code> table is missing the <code>enabled</code> column. Enable or disable each game for billing, set its base price and billing period, then
Server location enable/disable is currently non-functional. toggle availability per server and optionally override the price for that location.
Run <code>modules/billing/add_remote_server_enabled_column.sql</code> to add the column. Leave override blank to use the base price.
</div> </p>
<?php endif; ?>
<h2>Enable/Disable Server Locations (Global)</h2> <?php if (empty($services)): ?>
<form method="post" action=""> <p>No billing services found. Add services first via the database or the panel.</p>
<input type="hidden" name="update_remote_servers" value="1">
<div style="display:flex;flex-wrap:wrap;gap:10px;">
<?php foreach ((array)$remoteServers as $rs): ?>
<label class="loc-label min-w-240">
<input type="checkbox" name="rs[]" value="<?php echo (int)$rs['remote_server_id']; ?>" <?php echo ((int)$rs['enabled']===1?'checked':''); ?>>
<b><?php echo h($rs['remote_server_name']); ?></b>
<small class="muted">(ID: <?php echo (int)$rs['remote_server_id']; ?>)</small>
</label>
<?php endforeach; ?>
</div>
<div style="margin-top:10px;"><button type="submit">Update Enabled Servers</button></div>
</form>
<hr>
<h2>Current Services</h2>
<?php if (!$services): ?>
<p>No services found.</p>
<?php else: ?> <?php else: ?>
<!-- SINGLE BULK FORM FOR ALL SERVICES -->
<form method="post" action=""> <form method="post" action="">
<input type="hidden" name="save_matrix" value="1">
<table class="center" style="text-align:center;width:100%;border-collapse:collapse;"> <div style="overflow-x:auto;">
<table class="matrix-table">
<thead> <thead>
<tr> <tr>
<th>Enabled</th> <th class="game-name">Game</th>
<th>Service Name <small class="muted">(ID below)</small></th> <th>Enabled</th>
<th>Min Slots</th> <th>Base Price ($)</th>
<th>Max Slots</th> <th>Period</th>
<th>Price (Daily)</th> <?php foreach ((array)$remoteServers as $rs): ?>
<th>Price (Monthly)</th> <th><?php echo h($rs['remote_server_name']); ?><br>
<th>Price (Year)</th> <span class="muted">#<?php echo (int)$rs['remote_server_id']; ?></span>
<th>Thumbnail URL</th> </th>
<th>Preview</th> <?php endforeach; ?>
<th>Update Row</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ((array)$services as $row): ?> <?php foreach ((array)$services as $svc):
<?php $sid = (int)$svc['service_id'];
$sid = (int)$row['service_id']; $svcEnabled = (int)$svc['enabled'];
$selected = parse_id_list($row['locs'] ?? ''); // Determine current base price and period from existing columns
$primary = $selected[0] ?? 0; // first ID is "primary" if ((float)$svc['price_monthly'] > 0) {
$selSet = array_flip($selected); $basePrice = number_format((float)$svc['price_monthly'], 2, '.', '');
$imgUrl = trim((string)$row['img_url']); $period = 'monthly';
$displayUrl = ''; } elseif ((float)$svc['price_daily'] > 0) {
if ($imgUrl !== '') { $basePrice = number_format((float)$svc['price_daily'], 2, '.', '');
if (is_abs_url($imgUrl)) { $period = 'daily';
$displayUrl = $imgUrl; } elseif ((float)$svc['price_year'] > 0) {
} elseif ($siteBaseUrl !== '') { $basePrice = number_format((float)$svc['price_year'], 2, '.', '');
$displayUrl = join_base($siteBaseUrl, $imgUrl); $period = 'yearly';
} else { } else {
// Use relative path (local folder) $basePrice = '0.00';
$displayUrl = $imgUrl; $period = 'monthly';
} }
} ?>
?>
<!-- MAIN ROW (no bottom border) -->
<tr> <tr>
<!-- Enabled first --> <td class="game-name">
<td> <?php echo h($svc['service_name']); ?>
<input type="hidden" name="service[<?php echo $sid; ?>][enabled]" value="0"> <div class="muted">ID: <?php echo $sid; ?></div>
<input type="checkbox" name="service[<?php echo $sid; ?>][enabled]" value="1" <?php echo ((int)$row['enabled']===1?'checked':''); ?>>
</td>
<!-- Service name (with tiny ID under it) -->
<td>
<input type="text" name="service[<?php echo $sid; ?>][service_name]" value="<?php echo h($row['service_name']); ?>" class="min-w-260">
<div class="small-muted">ID: <?php echo $sid; ?></div>
</td> </td>
<td> <td>
<input type="number" name="service[<?php echo $sid; ?>][slot_min_qty]" value="<?php echo (int)$row['slot_min_qty']; ?>" min="1" step="1" class="w-90"> <input type="hidden" name="svc[<?php echo $sid; ?>][enabled]" value="0">
<input type="checkbox" name="svc[<?php echo $sid; ?>][enabled]" value="1"
<?php echo $svcEnabled ? 'checked' : ''; ?>>
</td> </td>
<td> <td>
<input type="number" name="service[<?php echo $sid; ?>][slot_max_qty]" value="<?php echo (int)$row['slot_max_qty']; ?>" min="1" step="1" class="w-90"> <input type="number" step="0.01" min="0"
name="svc[<?php echo $sid; ?>][base_price]"
value="<?php echo h($basePrice); ?>"
style="width:90px;">
</td> </td>
<td> <td>
<input type="text" name="service[<?php echo $sid; ?>][price_daily]" value="<?php echo h(number_format((float)$row['price_daily'], 2, '.', '')); ?>" size="8"> <select name="svc[<?php echo $sid; ?>][period]">
<option value="monthly" <?php echo $period === 'monthly' ? 'selected' : ''; ?>>Monthly</option>
<option value="daily" <?php echo $period === 'daily' ? 'selected' : ''; ?>>Daily</option>
<option value="yearly" <?php echo $period === 'yearly' ? 'selected' : ''; ?>>Yearly</option>
</select>
</td> </td>
<td> <?php foreach ((array)$remoteServers as $rs):
<input type="text" name="service[<?php echo $sid; ?>][price_monthly]" value="<?php echo h($row['price_monthly']); ?>" size="8"> $rid = (int)$rs['remote_server_id'];
</td> $mapEntry = $mappings[$sid][$rid] ?? ['enabled' => 0, 'override_price' => null];
$mapEnabled = (int)$mapEntry['enabled'];
<td> $ovPrice = $mapEntry['override_price'];
<input type="text" name="service[<?php echo $sid; ?>][price_year]" value="<?php echo h(number_format((float)$row['price_year'], 2, '.', '')); ?>" size="8"> ?>
</td> <td>
<input type="hidden" name="map[<?php echo $sid; ?>][<?php echo $rid; ?>][enabled]" value="0">
<!-- Thumbnail URL input --> <input type="checkbox" name="map[<?php echo $sid; ?>][<?php echo $rid; ?>][enabled]" value="1"
<td> <?php echo $mapEnabled ? 'checked' : ''; ?>>
<input type="text" name="service[<?php echo $sid; ?>][img_url]" value="<?php echo h($row['img_url']); ?>" class="min-w-240"> <br>
</td> <input type="number" step="0.01" min="0" placeholder="override"
class="override-input"
<!-- Preview (uses BASE + relative path) --> name="map[<?php echo $sid; ?>][<?php echo $rid; ?>][override_price]"
<td> value="<?php echo ($ovPrice !== null ? h(number_format((float)$ovPrice, 2, '.', '')) : ''); ?>">
<?php if ($displayUrl !== ''): ?> </td>
<img src="<?php echo h($displayUrl); ?>" alt="preview" loading="lazy" class="img-preview" onerror="this.style.display='none'"> <?php endforeach; ?>
<?php else: ?>
<span class="muted">(no image)</span>
<?php endif; ?>
</td>
<!-- Per-row Update (smaller) -->
<td>
<button type="submit" name="update_single" value="<?php echo $sid; ?>" class="btn-small">Update Row</button>
</td>
</tr> </tr>
<!-- LOCATIONS ROW (single bottom divider) -->
<tr>
<td colspan="8" style="border-bottom:1px solid #f0f0f0; padding:8px 6px; text-align:left;">
<div class="locs-box" data-sid="<?php echo $sid; ?>" style="display:flex; flex-wrap:wrap; gap:8px;">
<?php foreach ((array)$remoteServers as $rs): ?>
<?php
$rid = (int)$rs['remote_server_id'];
$isChecked = isset($selSet[$rid]);
$isPrimary = ($primary === $rid);
?>
<label class="loc-label">
<input type="checkbox" class="locchk" data-sid="<?php echo $sid; ?>"
name="service[<?php echo $sid; ?>][locations][]" value="<?php echo $rid; ?>"
<?php echo $isChecked ? 'checked' : ''; ?> class="mr-6">
<?php echo h($rs['remote_server_name']); ?> (<?php echo $rid; ?>)
<span style="margin-left:10px;">
<input type="radio" class="locprim" data-sid="<?php echo $sid; ?>"
name="service[<?php echo $sid; ?>][primary_location]" value="<?php echo $rid; ?>"
<?php echo $isPrimary ? 'checked' : ''; ?> <?php echo $isChecked ? '' : 'disabled'; ?>>
<small>Primary</small>
</span>
<?php if ((int)$rs['enabled'] === 0): ?>
<small class="text-danger ml-8">[Globally disabled]</small>
<?php endif; ?>
</label>
<?php endforeach; ?>
</div>
</td>
</tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
<div style="margin-top:14px; text-align:right;">
<button type="submit" name="bulk_update" value="1">Update All</button>
</div> </div>
<div style="margin-top:14px;">
<button type="submit">Save Matrix</button>
</div>
</form>
<h3 style="margin-top:28px;">Remove a Service</h3>
<form method="post" action="" style="display:flex;gap:8px;align-items:center;">
<input type="hidden" name="remove_service" value="1">
<select name="service_id_remove">
<?php foreach ((array)$services as $s): ?>
<option value="<?php echo (int)$s['service_id']; ?>">
<?php echo h($s['service_name']); ?> (ID: <?php echo (int)$s['service_id']; ?>)
</option>
<?php endforeach; ?>
</select>
<button type="submit" onclick="return confirm('Remove this service and all its server mappings? This cannot be undone.')">Remove</button>
</form> </form>
<h3 style="margin-top:20px;">Remove a Service</h3>
<form method="post" action="" style="display:flex;gap:8px;align-items:center;">
<input type="hidden" name="remove_service" value="1">
<select name="service_id_remove">
<?php foreach ((array)$services as $s): ?>
<option value="<?php echo (int)$s['service_id']; ?>">
<?php echo h($s['service_name']); ?> (ID: <?php echo (int)$s['service_id']; ?>)
</option>
<?php endforeach; ?>
</select>
<button type="submit" onclick="return confirm('Remove this service? This cannot be undone.')">Remove</button>
</form>
<?php endif; ?> <?php endif; ?>
<div class="panel" style="margin-top:20px;"> <div class="panel" style="margin-top:20px;">
<h3>Environment</h3> <p><strong>Legend:</strong> Checkbox = server is available for this game.
<table class="cart-table"> Override price = customer pays this amount instead of the base price for that location.
<tr><th>Site Base URL</th><td><?php echo $siteBaseUrl !== '' ? h($siteBaseUrl) : '(empty — using relative paths)'; ?></td></tr> Leave override blank to use the game base price.</p>
<tr><th>Data directory</th><td><?php echo isset($SITE_DATA_DIR) ? h($SITE_DATA_DIR) : '(unset)'; ?></td></tr> <p class="muted">
<tr><th>PHP SAPI</th><td><?php echo h(PHP_SAPI); ?></td></tr> Availability is controlled entirely by <code><?php echo h("{$table_prefix}billing_service_remote_servers"); ?></code>.
<tr><th>Writable?</th><td><?php echo (isset($SITE_DATA_DIR) && is_writable($SITE_DATA_DIR)) ? 'yes' : 'no'; ?></td></tr> No entry or <code>enabled = 0</code> means the server is not offered for that game.
<tr><th>XML Reference</th><td><a href="/modules/billing/docs/xml_notes.php" target="_blank" rel="noopener">Open XML Notes</a></td></tr> </p>
</table>
</div> </div>
<!-- JS: Per-row: enable/disable Primary radios based on whether that location is checked --> <?php billing_maybe_close_db($db); ?>
<script>
document.querySelectorAll('.locs-box').forEach(function(box){
const sid = box.getAttribute('data-sid');
const checks = box.querySelectorAll('input.locchk[data-sid="'+sid+'"]');
function refreshRadios() {
checks.forEach(function(chk){
const rid = chk.value;
const rad = box.querySelector('input.locprim[data-sid="'+sid+'"][value="'+rid+'"]');
if (!rad) return;
if (chk.checked) {
rad.disabled = false;
} else {
if (rad.checked) rad.checked = false;
rad.disabled = true;
}
});
}
checks.forEach(chk => chk.addEventListener('change', refreshRadios));
refreshRadios();
});
</script>
<?php
// Close database connection safely
billing_maybe_close_db($db);
?>
</body> </body>
</html> </html>

View file

@ -24,8 +24,8 @@
// Module general information // Module general information
$module_title = "billing"; $module_title = "billing";
$module_version = "3.0"; $module_version = "3.1";
$db_version = 2; $db_version = 3;
$module_required = FALSE; $module_required = FALSE;
// Module description // Module description
$module_description = "Billing storefront / provisioning integration. Public ordering runs as a standalone site; panel pages provide provisioning and admin order management."; $module_description = "Billing storefront / provisioning integration. Public ordering runs as a standalone site; panel pages provide provisioning and admin order management.";
@ -173,4 +173,9 @@ $install_queries[1] = array(
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;" ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
); );
// Version 3: Add override_price to service-to-server mapping table
$install_queries[2] = array(
"ALTER TABLE `".OGP_DB_PREFIX."billing_service_remote_servers` ADD COLUMN `override_price` DECIMAL(10,2) NULL AFTER `enabled`"
);
?> ?>

View file

@ -188,46 +188,27 @@ if ($row['price_monthly'] == 0.0) {
<tr> <tr>
<td align="right"><b>Location</b></td> <td align="right"><b>Location</b></td>
<td align="left"> <td align="left">
<?php <?php
//loop through multiple remote server ID stored in services 'remote_server_ip' as text // Fetch servers enabled for this game via the billing_service_remote_servers mapping table.
//change WHERE clause to IS IN clause // Only servers with an enabled mapping row are shown; remote_servers.enabled is not used.
$rsiArray = explode(" ", $row['remote_server_id']);
$rsi = implode(",",$rsiArray);
//get the out of stock into an array and see if the rsID is in that array
$available_server = false; $available_server = false;
//loop through each of the assigned servers and see if its disabled $sid_order = (int)$row['service_id'];
foreach ((array)$rsiArray as $rsi) $mappedQuery = "SELECT m.remote_server_id, m.override_price, r.remote_server_name
{ FROM {$table_prefix}billing_service_remote_servers m
$query = "SELECT * FROM {$table_prefix}remote_servers WHERE remote_server_id = ".$rsi; JOIN {$table_prefix}remote_servers r
$result = $db->query($query); ON r.remote_server_id = m.remote_server_id
foreach ((array)$result as $rs) WHERE m.service_id = {$sid_order} AND m.enabled = 1
{ ORDER BY r.remote_server_name";
$mappedResult = $db->query($mappedQuery);
$rsID =$rs['remote_server_id']; if ($mappedResult) {
$rsNAME = $rs['remote_server_name']; while ($rs = $mappedResult->fetch_assoc()) {
//echo "<option value='$rsID'>$rsNAME</option>"; $rsID = (int)$rs['remote_server_id'];
// add disabled to lable and input if $rsID is in out_of_stock $rsNAME = htmlspecialchars((string)$rs['remote_server_name'], ENT_QUOTES, 'UTF-8');
$is_unavailable = ""; $available_server = true;
$service_text_color = ""; echo "<div>\n"
. " <input type='radio' name='ip_id' id='rs_{$rsID}' value='{$rsID}' required>\n"
. " <label for='rs_{$rsID}'>{$rsNAME}</label>\n"
if($rs['enabled']==0) . "</div>\n";
{
$is_unavailable = "disabled";
$service_text_color = "red";
}
if($is_unavailable == "")
{
$available_server = true;
}
//default radio button
// //<input type='radio' $is_unavailable name='ip_id' id='$rsID' value='$rsID' >
echo "<div>
<input type='radio' $is_unavailable name='ip_id' id='$rsID' value='$rsID' required>
<label for '$rsID' $is_unavailable ><span style='color:$service_text_color'>$rsNAME </span></label>
</div>";
} }
} }
?> ?>