Merge pull request #137 from GameServerPanel/copilot/fix-add-to-cart-schema-mismatch

Harden billing cart/invoice flows for schema drift, fix zero-total logic, and canonicalize storefront OS variants
This commit is contained in:
Frank Harris 2026-05-06 19:39:23 -05:00 committed by GitHub
commit 7c170ced51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 214 additions and 89 deletions

View file

@ -1,5 +1,8 @@
# Changelog
## 2026-05-07
- **Billing/cart/storefront stability pass:** Hardened `add_to_cart.php` to build schema-compatible invoice inserts dynamically (including legacy installs missing `period_start`), fixed free-checkout DB close handling so wrapper objects are never passed to `mysqli_close()`, switched cart/free-total decisions to cent-based math so low nonzero prices (e.g. $0.02) never show as FREE, improved canonical game deduplication + OS variant matching in storefront list/order pages, and aligned Steam Workshop behavior labels with the new restart/update wording.
## 2026-05-06
- **Panel settings language defaults:** Added missing English labels/help text for `login_ban_time`, `allow_setting_cpu_affinity`, `regex_invalid_file_name_chars`, `discord_invite_url`, `discord_webhook_main`, and `discord_webhook_admin`; language lookup now loads English fallback strings when the active locale is missing a key so settings pages stop rendering raw `_key_` tokens.
- **Config XML section editor redesign:** Added a top-level section-based XML editor in `config_games` with per-section Validate/Save/Reset actions, optional-section add/remove controls, required-section removal protection, and schema validation for section updates; kept the existing detailed node editor and full raw XML editor for advanced edits.

View file

@ -5,3 +5,4 @@
- Add Workshop result preview thumbnails and author links in the picker for easier browsing.
- Add a lightweight admin UI report that flags remaining PHP files still relying on legacy PHP 7 constructs not covered by the automated compatibility pass.
- Add a side-by-side before/after diff preview panel to the config_games top-level XML section editor before section saves.
- Add an integration smoke test that exercises paid checkout, free checkout, and add-to-cart on installs with/without `period_start` to prevent billing schema drift regressions.

View file

@ -44,6 +44,23 @@ function billing_normalize_duration(string $duration): array
}
}
function billing_money_to_cents(float $amount): int
{
return (int) round($amount * 100);
}
function billing_cents_to_money(int $cents): float
{
return $cents / 100;
}
function billing_fail_add_to_cart(string $message, array $context = []): void
{
site_log_error('add_to_cart_failed', array_merge(['message' => $message], $context));
header('Location: /cart.php?error=add_to_cart');
exit;
}
// Immediate request tracing log (helps confirm the script is hit)
@mkdir(__DIR__ . '/logs', 0775, true);
$trace_file = __DIR__ . '/logs/add_to_cart_requests.log';
@ -86,12 +103,13 @@ $ftp_password = isset($_POST['ftp_password']) ? trim((string)$_POST['ftp_passwor
// Price lookup: try to find service price_monthly
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
if (!$db) {
// Log connection error and exit
// Log connection error and return user to cart with a friendly error flag
@mkdir(__DIR__ . '/logs', 0775, true);
$trace = __DIR__ . '/logs/add_to_cart.log';
file_put_contents($trace, date('c') . " - mysqli_connect failed: " . mysqli_connect_error() . "\n", FILE_APPEND);
die('DB connection failed');
billing_fail_add_to_cart('DB connection failed');
} else {
mysqli_set_charset($db, 'utf8mb4');
// Log that config was loaded (mask password)
@mkdir(__DIR__ . '/logs', 0775, true);
$trace = __DIR__ . '/logs/add_to_cart.log';
@ -163,7 +181,8 @@ $status = 'due'; // Invoice status: due (unpaid), paid
$payment_status = 'unpaid';
$qty = max(1, $qty);
$max_players = max(1, $max_players);
$subtotal = round($base_rate * $max_players * $qty, 2);
$subtotal_cents = billing_money_to_cents((float)$base_rate * $max_players * $qty);
$subtotal = billing_cents_to_money($subtotal_cents);
$amount = $subtotal;
$period_end = date('Y-m-d H:i:s', strtotime('+' . ($durationInfo['days'] * $qty) . ' days'));
@ -193,55 +212,89 @@ $due_date = $due_dt->format('Y-m-d H:i:s');
// Escape values
$esc_user_id = intval($user_id);
$esc_service_id = intval($service_id);
$esc_home_name = mysqli_real_escape_string($db, $home_name);
$esc_ip_id = intval($ip_id);
$esc_max_players = intval($max_players);
$esc_qty = intval($qty);
$description = trim(($service_name !== '' ? $service_name : 'Game Server') . ': ' . $home_name);
$sql = "INSERT INTO {$table_prefix}billing_invoices (
order_id, user_id, service_id, home_id, home_name, ip, max_players, remote_control_password,
ftp_password, customer_name, customer_email, amount, discount_amount, currency, status,
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
)";
$invoiceTable = $table_prefix . 'billing_invoices';
$invoiceColumns = [];
$columnsResult = mysqli_query($db, "SHOW COLUMNS FROM `{$invoiceTable}`");
if (!$columnsResult) {
billing_fail_add_to_cart('Could not inspect billing invoice schema', ['table' => $invoiceTable, 'error' => mysqli_error($db)]);
}
while ($col = mysqli_fetch_assoc($columnsResult)) {
$invoiceColumns[$col['Field']] = true;
}
mysqli_free_result($columnsResult);
$invoice_duration = $durationInfo['invoice_duration'];
$rate_type = $durationInfo['rate_type'];
$rowData = [
'order_id' => 0,
'user_id' => $esc_user_id,
'service_id' => $esc_service_id,
'home_id' => 0,
'home_name' => $home_name,
'ip' => $esc_ip_id,
'max_players' => $esc_max_players,
'remote_control_password' => $remote_control_password,
'ftp_password' => $ftp_password,
'customer_name' => $customer_name,
'customer_email' => $customer_email,
'amount' => $amount,
'discount_amount' => 0.00,
'currency' => 'USD',
'status' => $status,
'billing_status' => $status,
'invoice_date' => $now,
'due_date' => $due_date,
'description' => $description,
'invoice_duration' => $invoice_duration,
'rate_type' => $rate_type,
'rate_per_player' => (float)$base_rate,
'players' => $max_players,
'period_start' => $now,
'period_end' => $period_end,
'subtotal' => $subtotal,
'total_due' => $amount,
'payment_status' => $payment_status,
'qty' => $esc_qty,
'coupon_id' => 0,
];
$insertColumns = [];
$placeholders = [];
$bindTypes = '';
$bindValues = [];
foreach ($rowData as $column => $value) {
if (!isset($invoiceColumns[$column])) {
continue;
}
$insertColumns[] = "`{$column}`";
$placeholders[] = '?';
if (is_int($value)) {
$bindTypes .= 'i';
} elseif (is_float($value)) {
$bindTypes .= 'd';
} else {
$bindTypes .= 's';
}
$bindValues[] = $value;
}
if (empty($insertColumns)) {
billing_fail_add_to_cart('No compatible invoice columns were found for insert', ['table' => $invoiceTable]);
}
$sql = "INSERT INTO `{$invoiceTable}` (" . implode(', ', $insertColumns) . ")
VALUES (" . implode(', ', $placeholders) . ")";
$stmt = $db->prepare($sql);
$res = false;
$err_no = 0;
$err = '';
if ($stmt) {
$invoice_duration = $durationInfo['invoice_duration'];
$rate_type = $durationInfo['rate_type'];
$stmt->bind_param(
'iisiissssdsssssssdissddsi',
$esc_user_id,
$esc_service_id,
$home_name,
$esc_ip_id,
$esc_max_players,
$remote_control_password,
$ftp_password,
$customer_name,
$customer_email,
$amount,
$status,
$status,
$now,
$due_date,
$description,
$invoice_duration,
$rate_type,
$base_rate,
$max_players,
$now,
$period_end,
$subtotal,
$amount,
$payment_status,
$esc_qty
);
$stmt->bind_param($bindTypes, ...$bindValues);
$res = @$stmt->execute();
$err_no = mysqli_errno($db);
$err = mysqli_error($db);
@ -272,8 +325,7 @@ if (!$res || $err_no > 0) {
site_log_warn('billing_invoices_exists', ['exists'=>$tbl_exists]);
file_put_contents($logfile, date('c') . " - Table exists check: {$tbl_exists}\n", FILE_APPEND);
// Show user-friendly error
die("Error adding to cart: " . htmlspecialchars($err) . ". Please contact support.");
billing_fail_add_to_cart('Invoice insert failed', ['errno' => $err_no, 'error' => $err]);
} else {
$insert_id = mysqli_insert_id($db);
$affected = mysqli_affected_rows($db);

View file

@ -14,6 +14,16 @@ if (session_status() === PHP_SESSION_NONE) {
// Load configuration
require_once(__DIR__ . '/bootstrap.php');
function billing_cart_money_to_cents(float $amount): int
{
return (int) round($amount * 100);
}
function billing_cart_cents_to_money(int $cents): float
{
return $cents / 100;
}
// Variables from config.inc.php (helps IDEs understand scope)
/** @var string $db_host Database host */
/** @var string $db_user Database user */
@ -45,7 +55,9 @@ $db_error = '';
// Initialize variables
$invoices = [];
$total_amount = 0.00;
$total_amount_cents = 0;
$discount_amount = 0.00;
$discount_amount_cents = 0;
$coupon_discount_percent = 0;
$applied_coupon = null;
$error_message = '';
@ -69,12 +81,14 @@ if (!$db) {
if ($result) {
while ($row = mysqli_fetch_assoc($result)) {
$invoices[] = $row;
$total_amount += floatval($row['amount']);
$lineAmount = (float)($row['total_due'] ?? $row['amount'] ?? 0);
$total_amount_cents += billing_cart_money_to_cents($lineAmount);
}
mysqli_free_result($result);
}
$cart_empty = (count((array)$invoices) === 0);
$total_amount = billing_cart_cents_to_money($total_amount_cents);
}
// Handle coupon application
@ -165,7 +179,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_coupon'])) {
}
// Re-validate coupon from session if present
if (empty($applied_coupon) && isset($_SESSION['cart_coupon_code'])) {
if ($db && empty($applied_coupon) && isset($_SESSION['cart_coupon_code'])) {
$coupon_code = $_SESSION['cart_coupon_code'];
$safe_code = mysqli_real_escape_string($db, $coupon_code);
$coupon_query = "SELECT * FROM {$table_prefix}billing_coupons
@ -247,10 +261,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_invoice']) &&
// Calculate discount
if ($applied_coupon && $coupon_discount_percent > 0) {
$discount_amount = $total_amount * ($coupon_discount_percent / 100);
$discount_amount_cents = (int) round($total_amount_cents * ($coupon_discount_percent / 100));
$discount_amount_cents = min($discount_amount_cents, $total_amount_cents);
}
$final_amount = $total_amount - $discount_amount;
$discount_amount = billing_cart_cents_to_money($discount_amount_cents);
$final_amount_cents = max(0, $total_amount_cents - $discount_amount_cents);
$final_amount = billing_cart_cents_to_money($final_amount_cents);
// PayPal configuration (from config)
$client_id = function_exists('gsp_paypal_get_client_id') ? gsp_paypal_get_client_id() : ($paypal_client_id ?? '');
@ -263,7 +280,8 @@ foreach ((array)$invoices as $inv) {
$game_display = !empty($inv['game_name']) ? $inv['game_name'] : 'Game Server';
$qty = max(1, intval($inv['qty']));
$paypal_invoice_ids[] = intval($inv['invoice_id']);
$lineAmount = (float)($inv['total_due'] ?? $inv['amount'] ?? 0);
$lineAmountCents = billing_cart_money_to_cents((float)($inv['total_due'] ?? $inv['amount'] ?? 0));
$lineAmount = billing_cart_cents_to_money($lineAmountCents);
$paypal_items[] = [
'name' => $inv['home_name'] . ' (' . $game_display . ')',
'description' => $inv['description'] ?? '',
@ -626,7 +644,7 @@ $siteBase = $protocol . $host;
</div>
<!-- Checkout Section -->
<?php if ($final_amount <= 0.00): ?>
<?php if ($final_amount_cents === 0): ?>
<!-- Zero-dollar checkout: coupon covers the full amount, no PayPal needed -->
<div class="checkout-section">
<h3>🎉 Complete Your Free Order</h3>
@ -688,7 +706,7 @@ $siteBase = $protocol . $host;
}
</script>
<?php if ($final_amount > 0.00 && !empty($client_id)): ?>
<?php if ($final_amount_cents > 0 && !empty($client_id)): ?>
<script>
function showPaymentError(msg) {
var statusDiv = document.getElementById('status-message');

View file

@ -17,6 +17,11 @@ if (session_status() === PHP_SESSION_NONE) {
require_once __DIR__ . '/bootstrap.php';
require_once __DIR__ . '/includes/login_required.php';
function billing_free_money_to_cents(float $amount): int
{
return (int) round($amount * 100);
}
$userId = intval($_SESSION['website_user_id'] ?? $_SESSION['user_id'] ?? 0);
if ($userId <= 0) {
header('Location: /login.php');
@ -30,15 +35,15 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
}
// DB connection
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
if (!$db) {
die('<p>Database connection failed. Please <a href="/order.php">return to the shop</a> or contact support.</p>');
$mysqli = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
if (!$mysqli) {
die('<p>Database connection failed. Please <a href="/serverlist.php">return to the shop</a> or contact support.</p>');
}
mysqli_set_charset($db, 'utf8mb4');
mysqli_set_charset($mysqli, 'utf8mb4');
// Fetch unpaid invoices for this user (prepared statement)
$invoices = [];
$stmt = mysqli_prepare($db, "SELECT * FROM {$table_prefix}billing_invoices
$stmt = mysqli_prepare($mysqli, "SELECT * FROM {$table_prefix}billing_invoices
WHERE user_id = ?
AND (status = 'due' OR status = '')
AND (payment_status IS NULL OR payment_status NOT IN ('paid','cancelled','refunded'))
@ -54,7 +59,9 @@ if ($stmt) {
}
if (empty($invoices)) {
mysqli_close($db);
if ($mysqli instanceof mysqli) {
mysqli_close($mysqli);
}
header('Location: /cart.php?msg=empty');
exit;
}
@ -65,8 +72,8 @@ $couponCode = trim($_POST['coupon_code'] ?? $_SESSION['cart_coupon_code'] ?? '')
$discountPct = 0.0;
if ($couponCode !== '') {
$safe = mysqli_real_escape_string($db, $couponCode);
$cr = mysqli_query($db, "SELECT * FROM {$table_prefix}billing_coupons
$safe = mysqli_real_escape_string($mysqli, $couponCode);
$cr = mysqli_query($mysqli, "SELECT * FROM {$table_prefix}billing_coupons
WHERE code = '$safe' AND is_active = 1 LIMIT 1");
if ($cr && mysqli_num_rows($cr) === 1) {
$coupon = mysqli_fetch_assoc($cr);
@ -76,16 +83,20 @@ if ($couponCode !== '') {
}
// Calculate total and verify it is $0 after discount
$totalAmount = 0.0;
$totalAmountCents = 0;
foreach ($invoices as $inv) {
$totalAmount += (float)($inv['amount'] ?? 0);
$lineAmount = (float)($inv['amount'] ?? 0);
$totalAmountCents += billing_free_money_to_cents($lineAmount);
}
$discountAmount = $totalAmount * ($discountPct / 100.0);
$finalAmount = round($totalAmount - $discountAmount, 2);
$discountAmountCents = (int) round($totalAmountCents * ($discountPct / 100.0));
$discountAmountCents = min($discountAmountCents, $totalAmountCents);
$finalAmountCents = max(0, $totalAmountCents - $discountAmountCents);
if ($finalAmount > 0.00) {
if ($finalAmountCents !== 0) {
// Coupon no longer covers the full amount — redirect to cart
mysqli_close($db);
if ($mysqli instanceof mysqli) {
mysqli_close($mysqli);
}
header('Location: /cart.php?msg=coupon_insufficient');
exit;
}
@ -97,7 +108,7 @@ $txid = 'free-' . time() . '-' . $userId;
require_once __DIR__ . '/classes/BillingRepository.php';
require_once __DIR__ . '/classes/BillingService.php';
$repo = new BillingRepository($db, $table_prefix);
$repo = new BillingRepository($mysqli, $table_prefix);
$newOrderIds = [];
$duration_meta = static function (array $invoice): array {
$duration = strtolower((string)($invoice['invoice_duration'] ?? $invoice['rate_type'] ?? 'month'));
@ -213,7 +224,7 @@ foreach ($invoices as $inv) {
}
if ($couponId > 0 && !empty($invoices)) {
mysqli_query($db, "UPDATE {$table_prefix}billing_coupons
mysqli_query($mysqli, "UPDATE {$table_prefix}billing_coupons
SET current_uses = current_uses + 1
WHERE coupon_id = " . intval($couponId));
}
@ -234,7 +245,9 @@ if (!empty($newOrderIds)) {
// If panel bootstrap fails the order is Active and admins can provision via the orders panel.
}
mysqli_close($db);
if ($mysqli instanceof mysqli) {
mysqli_close($mysqli);
}
header('Location: /payment_success.php?order_id=' . urlencode($txid));
exit;

View file

@ -68,6 +68,21 @@ function order_game_key_os(string $gameKey): string
return 'any';
}
function order_price_is_free($value): bool
{
return ((int) round(((float)$value) * 100)) === 0;
}
function order_canonical_game_key(string $gameKey): string
{
$gameKey = strtolower(trim($gameKey));
if ($gameKey === '') {
return '';
}
$canonical = preg_replace('/_(linux|linux32|linux64|win|win32|win64|windows|windows32|windows64)$/i', '', $gameKey);
return $canonical !== '' ? $canonical : $gameKey;
}
// --- Fetch the requested service with config_homes join for canonical game info ---
$req_service_id = intval($_REQUEST['service_id'] ?? 0);
if ($req_service_id !== 0) {
@ -146,7 +161,7 @@ if ($imgSrc === '') { $imgSrc = '/images/games/default_server.png'; }
<?php echo htmlspecialchars((string)($row['cfg_game_name'] ?? $row['service_name']), ENT_QUOTES, 'UTF-8'); ?>
<br>
<?php
if (floatval($row['price_monthly']) == 0.0) {
if (order_price_is_free($row['price_monthly'] ?? 0)) {
echo "FREE";
} else {
echo "$" . number_format(floatval($row['price_monthly']), 2) . " Monthly";
@ -164,20 +179,30 @@ echo "$" . number_format(floatval($row['price_monthly']), 2) . " Monthly";
$svcGameKey = (string)($row['cfg_game_key'] ?? '');
$svcGameOs = order_game_key_os($svcGameKey);
$canonicalGameName = (string)($row['cfg_game_name'] ?? $row['service_name']);
$canonicalGameKey = order_canonical_game_key($svcGameKey);
// Build map of OS variant service IDs for JS-based automatic selection.
// Look for sibling services that share the same cfg_game_name (canonical) but differ in OS.
// e.g. if current service is arma3_linux64, find the arma3_win64 service too.
$osServiceMap = []; // ['linux' => 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}'";
if ($svcGameOs !== 'any' && (!empty($canonicalGameName) || !empty($canonicalGameKey))) {
$siblingQuery = "SELECT bs.service_id, ch.game_key AS cfg_game_key, ch.game_name AS cfg_game_name
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";
$siblingResult = $db->query($siblingQuery);
if ($siblingResult) {
while ($sib = $siblingResult->fetch_assoc()) {
$sibGameKey = (string)($sib['cfg_game_key'] ?? '');
$sibCanonical = order_canonical_game_key($sibGameKey);
$sibName = (string)($sib['cfg_game_name'] ?? '');
if ($canonicalGameKey !== '') {
if ($sibCanonical !== $canonicalGameKey) {
continue;
}
} elseif ($canonicalGameName !== '' && strcasecmp($sibName, $canonicalGameName) !== 0) {
continue;
}
$sibOs = order_game_key_os((string)($sib['cfg_game_key'] ?? ''));
$osServiceMap[$sibOs] = (int)$sib['service_id'];
}

View file

@ -23,6 +23,22 @@ if (!$db) {
die("Connection failed: " . mysqli_connect_error());
}
function billing_service_price_is_free($value): bool
{
return ((int) round(((float)$value) * 100)) === 0;
}
function billing_canonical_game_identity(array $row): string
{
$gameKey = strtolower(trim((string)($row['cfg_game_key'] ?? '')));
if ($gameKey !== '') {
$canonicalKey = preg_replace('/_(linux|linux32|linux64|win|win32|win64|windows|windows32|windows64)$/i', '', $gameKey);
return 'key:' . ($canonicalKey !== '' ? $canonicalKey : $gameKey);
}
$gameName = strtolower(trim((string)($row['cfg_game_name'] ?? $row['service_name'] ?? '')));
return 'name:' . $gameName;
}
// Save new description if admin
if (isset($_POST['save']) && !empty($_POST['description'])) {
$new_description = str_replace("\\r\\n", "<br>", $_POST['description']);
@ -80,15 +96,12 @@ while ($row = $result_services->fetch_assoc()) {
}
// 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])) {
$canonicalIdentity = billing_canonical_game_identity($row);
if (isset($seenCanonical[$canonicalIdentity])) {
// Already have this game — skip the duplicate OS variant
continue;
}
$seenCanonical[$canonicalName] = true;
$seenCanonical[$canonicalIdentity] = true;
$serviceRows[] = $row;
}
$result_services->free();
@ -115,7 +128,7 @@ include(__DIR__ . '/includes/menu.php');
onerror="this.src='/images/games/default_server.png'; this.onerror=null;"><br>
<strong><?php echo htmlspecialchars((string)($row['cfg_game_name'] ?? $row['service_name']), ENT_QUOTES, 'UTF-8'); ?></strong><br>
<?php
echo (floatval($row['price_monthly']) == 0.0) ? "FREE" : "$" . number_format(floatval($row['price_monthly']), 2) . " Monthly";
echo billing_service_price_is_free($row['price_monthly'] ?? 0) ? "FREE" : "$" . number_format((float)$row['price_monthly'], 2) . " Monthly";
?>
<br>

View file

@ -1 +1 @@
Last Updated at 3:52pm on 2026-05-06
Last Updated at 12:31am on 2026-05-07

View file

@ -314,7 +314,7 @@ function sw_admin_edit_form(array $profile, array $detected = array(), $showDete
<span>Default Install / Update Mode</span>
<select name="default_update_mode">
<option value="manual" <?= (($profile['default_update_mode'] ?? 'manual') === 'manual') ? 'selected' : '' ?>>Manual only (safe default)</option>
<option value="on_restart" <?= (($profile['default_update_mode'] ?? 'manual') === 'on_restart') ? 'selected' : '' ?>>On next server restart</option>
<option value="on_restart" <?= (($profile['default_update_mode'] ?? 'manual') === 'on_restart') ? 'selected' : '' ?>>On next restart</option>
<option value="before_start" <?= (($profile['default_update_mode'] ?? 'manual') === 'before_start') ? 'selected' : '' ?>>Before every server start</option>
<option value="scheduled" <?= (($profile['default_update_mode'] ?? 'manual') === 'scheduled') ? 'selected' : '' ?>>Scheduled update check</option>
</select>
@ -323,8 +323,8 @@ function sw_admin_edit_form(array $profile, array $detected = array(), $showDete
<span>Default Restart Behavior</span>
<select name="default_restart_behavior">
<option value="none" <?= (($profile['default_restart_behavior'] ?? 'none') === 'none') ? 'selected' : '' ?>>Do not restart automatically (safe default)</option>
<option value="if_empty" <?= (($profile['default_restart_behavior'] ?? 'none') === 'if_empty') ? 'selected' : '' ?>>Restart only if server is empty</option>
<option value="immediate" <?= (($profile['default_restart_behavior'] ?? 'none') === 'immediate') ? 'selected' : '' ?>>Restart immediately after warning</option>
<option value="if_empty" <?= (($profile['default_restart_behavior'] ?? 'none') === 'if_empty') ? 'selected' : '' ?>>Restart if empty</option>
<option value="immediate" <?= (($profile['default_restart_behavior'] ?? 'none') === 'immediate') ? 'selected' : '' ?>>Restart after warning</option>
<option value="next_restart" <?= (($profile['default_restart_behavior'] ?? 'none') === 'next_restart') ? 'selected' : '' ?>>Install on next manual restart only</option>
</select>
</label>

View file

@ -584,7 +584,7 @@ function sw_user_render($db, $home_id, array $home, array $profile)
<td style="padding:8px;">
<select name="update_mode" style="width:100%;">
<option value="manual" <?= ($settings['update_mode'] === 'manual') ? 'selected' : '' ?>>Manual only</option>
<option value="on_restart" <?= ($settings['update_mode'] === 'on_restart') ? 'selected' : '' ?>>On next server restart</option>
<option value="on_restart" <?= ($settings['update_mode'] === 'on_restart') ? 'selected' : '' ?>>On next restart</option>
<option value="before_start" <?= ($settings['update_mode'] === 'before_start') ? 'selected' : '' ?>>Before every server start</option>
<option value="scheduled" <?= ($settings['update_mode'] === 'scheduled') ? 'selected' : '' ?>>Scheduled update check</option>
</select>
@ -602,8 +602,8 @@ function sw_user_render($db, $home_id, array $home, array $profile)
<td style="padding:8px;">
<select name="restart_behavior" style="width:100%;">
<option value="none" <?= ($settings['restart_behavior'] === 'none') ? 'selected' : '' ?>>Do not restart automatically</option>
<option value="if_empty" <?= ($settings['restart_behavior'] === 'if_empty') ? 'selected' : '' ?>>Restart only if server is empty</option>
<option value="immediate" <?= ($settings['restart_behavior'] === 'immediate') ? 'selected' : '' ?>>Restart immediately after warning</option>
<option value="if_empty" <?= ($settings['restart_behavior'] === 'if_empty') ? 'selected' : '' ?>>Restart if empty</option>
<option value="immediate" <?= ($settings['restart_behavior'] === 'immediate') ? 'selected' : '' ?>>Restart after warning</option>
<option value="next_restart" <?= ($settings['restart_behavior'] === 'next_restart') ? 'selected' : '' ?>>Install on next manual restart only</option>
</select>
</td>