diff --git a/modules/administration/panel_update.php b/modules/administration/panel_update.php
index 1b822b5b..ed87a164 100644
--- a/modules/administration/panel_update.php
+++ b/modules/administration/panel_update.php
@@ -1098,14 +1098,14 @@ function gsp_panel_update_section()
$finished_at = date('Y-m-d H:i:s');
if ($result['success']) {
print_success(
- 'Panel updated to development version (' . htmlspecialchars($stable_branch) . '). '
+ 'Panel updated to GitHub Stable (' . htmlspecialchars($stable_branch) . '). '
. intval($result['files_copied']) . ' file(s) updated. Source: '
. htmlspecialchars($stable_branch) . ''
);
- 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(
'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,
isset($result['backup_dir']) ? $result['backup_dir'] . '/database.sql' : null,
isset($result['backup_dir']) ? $result['backup_dir'] . '/panel-files.tar.gz': null,
@@ -1113,10 +1113,10 @@ function gsp_panel_update_section()
);
} else {
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(
'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
);
}
@@ -1127,14 +1127,14 @@ function gsp_panel_update_section()
$finished_at = date('Y-m-d H:i:s');
if ($result['success']) {
print_success(
- 'Panel updated to cutting edge version (' . htmlspecialchars($unstable_branch) . '). '
+ 'Panel updated to GitHub Unstable (' . htmlspecialchars($unstable_branch) . '). '
. intval($result['files_copied']) . ' file(s) updated. Source: '
. htmlspecialchars($unstable_branch) . ''
);
- 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(
'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,
isset($result['backup_dir']) ? $result['backup_dir'] . '/database.sql' : null,
isset($result['backup_dir']) ? $result['backup_dir'] . '/panel-files.tar.gz': null,
@@ -1142,10 +1142,10 @@ function gsp_panel_update_section()
);
} else {
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(
'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
);
}
@@ -1282,32 +1282,34 @@ function gsp_panel_update_section()
echo "
\n";
- // ---- Development Version ------------------------------------------------
- echo "
Development Version
\n";
+ // ---- GitHub Stable -------------------------------------------------------
+ echo "GitHub Stable
\n";
+ echo "GitHub Stable should always match the latest official numbered release.
\n";
echo "\n";
echo "
\n";
- // ---- Cutting Edge Version -----------------------------------------------
- echo "Cutting Edge Version
\n";
+ // ---- GitHub Unstable -----------------------------------------------------
+ echo "GitHub Unstable
\n";
+ echo "GitHub Unstable represents the latest development branch and may be unstable.
\n";
echo ""
- . "⚠ Warning: The cutting edge version may be unstable or contain bugs. Use with caution in production.
\n";
+ . "⚠ Warning: GitHub Unstable may contain bugs or incomplete features. Use with caution in production.
\n";
echo "\n";
diff --git a/modules/billing/add_override_price_column.sql b/modules/billing/add_override_price_column.sql
new file mode 100644
index 00000000..704a4418
--- /dev/null
+++ b/modules/billing/add_override_price_column.sql
@@ -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`;
diff --git a/modules/billing/adminserverlist.php b/modules/billing/adminserverlist.php
index c083c1f3..1d0e6011 100644
--- a/modules/billing/adminserverlist.php
+++ b/modules/billing/adminserverlist.php
@@ -3,389 +3,287 @@
- Admin Server List - GameServers.World
+ Admin Server / Game Matrix - GameServers.World
+
Need the XML field reference? ";
-echo "Open XML Notes";
-echo "";
-
-/* show errors during setup */
-@ini_set('display_errors','1');
-error_reporting(E_ALL);
-function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
-function esc_mysqli($db, $v){ return $db->real_escape_string($v); }
-function fetch_all_assoc($db, $sql){
- $res = $db->query($sql);
- return $res ? $res->fetch_all(MYSQLI_ASSOC) : [];
+// Ensure the mapping table exists with the override_price column
+$db->query(
+ "CREATE TABLE IF NOT EXISTS `{$table_prefix}billing_service_remote_servers` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `service_id` INT(11) NOT NULL,
+ `remote_server_id` INT(11) NOT NULL,
+ `enabled` TINYINT(1) NOT NULL DEFAULT 1,
+ `override_price` DECIMAL(10,2) NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `svc_rs` (`service_id`, `remote_server_id`),
+ KEY `service_id` (`service_id`),
+ 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 = [];
+$flashType = 'ok';
-/* A) Update global server location enable flags */
-if (isset($_POST['update_remote_servers'])) {
- $enabledIds = array_map('intval', $_POST['rs'] ?? []);
- $enabledSet = array_flip($enabledIds);
- $allIds = fetch_all_assoc($db, "SELECT remote_server_id FROM {$table_prefix}remote_servers");
- foreach ((array)$allIds as $row) {
- $id = (int)$row['remote_server_id'];
- $e = isset($enabledSet[$id]) ? 1 : 0;
- if ($rsHasEnabled) {
- $db->query("UPDATE {$table_prefix}remote_servers SET enabled={$e} WHERE remote_server_id={$id}");
+/* -----------------------------------------------------------------------
+ SAVE: matrix form submitted
+----------------------------------------------------------------------- */
+if (isset($_POST['save_matrix'])) {
+ $postedServices = $_POST['svc'] ?? [];
+ $postedMappings = $_POST['map'] ?? [];
+
+ foreach ((array)$postedServices as $sid => $svcData) {
+ $sid = (int)$sid;
+ $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) {
- $flash[] = "Server locations updated.";
- } else {
- $flash[] = "Server locations updated (note: 'enabled' column missing from remote_servers — run add_remote_server_enabled_column.sql migration).";
- }
+
+ // Upsert mappings: for every service x server pair post data received
+ $allServerIds = [];
+ $rsRes = $db->query("SELECT remote_server_id FROM `{$table_prefix}remote_servers`");
+ 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){
- $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) */
+/* -----------------------------------------------------------------------
+ Remove a service
+----------------------------------------------------------------------- */
if (isset($_POST['remove_service'], $_POST['service_id_remove'])) {
- $sid = (int)$_POST['service_id_remove'];
- $db->query("DELETE FROM {$table_prefix}billing_services WHERE service_id={$sid}");
- $flash[] = "Service #{$sid} removed.";
+ $sid = (int)$_POST['service_id_remove'];
+ $db->query("DELETE FROM `{$table_prefix}billing_service_remote_servers` WHERE service_id = {$sid}");
+ $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).
-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");
-} else {
- $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");
+/* -----------------------------------------------------------------------
+ Load data
+----------------------------------------------------------------------- */
+$remoteServers = [];
+$rsRes = $db->query("SELECT remote_server_id, remote_server_name 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");
?>
-
- ".h($m)."
"; ?>
-
+
+
+
-
-
- ⚠ Schema notice: The table is missing the enabled column.
- Server location enable/disable is currently non-functional.
- Run modules/billing/add_remote_server_enabled_column.sql to add the column.
-
-
+Game × Server Matrix
+
+ Enable or disable each game for billing, set its base price and billing period, then
+ toggle availability per server and optionally override the price for that location.
+ Leave override blank to use the base price.
+
-Enable/Disable Server Locations (Global)
-
-
-
-
-Current Services
-
- No services found.
+
+ No billing services found. Add services first via the database or the panel.
-