Fix README, storefront mobile layout, and cart pricing consistency

Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/5e161382-08ef-43a9-8cb3-d6fadad18c00

Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-07 12:43:41 +00:00 committed by GitHub
parent 7c170ced51
commit e0b843897d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 428 additions and 63 deletions

View file

@ -1,6 +1,7 @@
# Changelog
## 2026-05-07
- **README + storefront mobile/cart pricing fixes:** Rewrote the root README for GSP positioning, hardened storefront mobile responsiveness (login, order, cart, shared header guardrails), fixed add-to-cart price persistence for low-decimal paid items, aligned cart/free-checkout math to `total_due` values, and refreshed the canonical storefront footer timestamp.
- **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

101
README.md
View file

@ -1,30 +1,91 @@
# GSP Windows Agent
# GameServerPanel (GSP)
Cygwin-based agent that lets the GameServer Panel manage Windows Server 2019/2022 hosts. It mirrors the Linux agent feature set: signed RPC transport, GNU Screen session management, and SteamCMD-aware installers.
GSP is a modern game server hosting panel and commercial-ready hosting platform for teams that need billing, automation, and multi-node operations in one stack.
## Highlights
## What GSP is
- One-click installer (`Install/onceinstall_agent.bat`) that bootstraps Cygwin, required packages, and the `gameserver` service account.
- Task Scheduler entry that keeps the agent running after reboots.
- Helper scripts (`agent_conf.sh`, `rebase_post_ins.bat`, etc.) for maintaining the environment.
- Markdown documentation under [`documentation/agent-guide.md`](documentation/agent-guide.md).
GSP is the actively maintained GameServerPanel project. It is a modernized evolution of legacy Open Game Panel concepts, expanded for current hosting workflows and provider operations.
## Quick start
## Why GSP exists
1. Clone or download the repository to `C:\\gsp-agent`.
2. Right-click `Install\\onceinstall_agent.bat` → “Run as administrator”.
3. Open the bundled Cygwin terminal and configure the agent:
```bash
cd /OGP
bash agent_conf.sh -p "gameserverPassword"
```
4. Edit `C:\\OGP\\Cfg\\Config.pm` (match the settings you entered in the GSP web panel) and start the “OGP agent start on boot” scheduled task.
Traditional game panel workflows often require manual setup, disconnected billing flows, and custom glue code between storefronts and provisioning.
GSP exists to provide a stronger foundation for automated service delivery, consistent customer experience, and safer long-term operations.
## Related repositories
## Core features
- [GSP](https://github.com/GameServerPanel/GSP) PHP panel that issues commands to the agents.
- [GSP-Agent-Linux](https://github.com/GameServerPanel/GSP-Agent-Linux) Linux counterpart with systemd service files.
- Unified panel + storefront architecture
- Shared customer sessions between website and panel surfaces
- Billing-aware server lifecycle management
- Multi-node service placement across locations
- XML-driven game metadata and install configuration
## Automated provisioning
GSP includes work toward fully automated provisioning pipelines that connect order/payment events to server creation, home assignment, and post-provision workflows.
## Storefront, cart, billing, and PayPal support
The billing module provides the foundation for:
- Product catalog and order configuration
- Cart and invoice handling
- Coupon/discount workflows
- PayPal checkout and capture integration
## Steam Workshop management
GSP is designed to support Steam Workshop-enabled game operations, including profile-driven defaults and per-server workshop management flows.
## XML / game configuration management
Game definitions are XML-based so catalog, install metadata, and operational settings can stay centralized and extensible without hardcoded per-title logic.
## Multi-location and OS-aware deployment
GSP includes work toward OS-aware service routing and multi-location hosting so providers can target the right node/runtime combinations per game and region.
## Database migration safety
The project uses versioned module migrations and idempotent upgrade patterns, with guarded schema checks where needed, to reduce upgrade risk across diverse installs.
## Security improvements
Current code includes hardened session handling, credential verification updates, CSRF protection in key admin paths, and safer billing/provisioning validation patterns.
## Hosting provider benefits
- Faster order-to-server delivery
- Better control over node/location availability
- Reduced manual operations overhead
- Cleaner upgrade path for production environments
## Customer experience improvements
- More consistent ordering and checkout flows
- Shared login/session behavior across surfaces
- Better visibility into orders, renewals, and account actions
## Technology stack
- PHP 8.x (actively modernized for current compatibility needs)
- MySQL/MariaDB-backed data model
- XML-based game configuration system
- PayPal REST integration for storefront checkout
## Roadmap and future goals
GSP continues to focus on:
- Stronger automation and provisioning reliability
- Expanded storefront UX and mobile usability
- Broader game/workshop tooling improvements
- Operational observability and admin quality-of-life features
## Contributing
Send pull requests through GitHub. Test installer changes on a clean Windows Server VM, keep batch files in ASCII, and update `documentation/agent-guide.md` whenever you modify the workflow.
Pull requests are welcome.
Please keep changes production-safe, follow existing GSP patterns, and avoid introducing legacy compatibility shortcuts that conflict with current architecture.
## License
GSP is distributed under the GNU General Public License v2. See [`LICENSE`](LICENSE) and [`COPYING`](COPYING).

View file

@ -6,3 +6,4 @@
- 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.
- Add a storefront visual-regression check at 375px and 430px breakpoints covering login, order, and cart pages to prevent mobile overflow regressions.

View file

@ -54,6 +54,35 @@ function billing_cents_to_money(int $cents): float
return $cents / 100;
}
function billing_rate_from_service(mysqli $db, string $table_prefix, int $service_id, string $rate_type): float
{
if ($service_id <= 0) {
return 0.0;
}
$stmt = $db->prepare("SELECT price_daily, price_monthly, price_year FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
if (!$stmt) {
return 0.0;
}
$stmt->bind_param('i', $service_id);
$stmt->execute();
$stmt->bind_result($price_daily, $price_monthly, $price_year);
$rate = 0.0;
if ($stmt->fetch()) {
if ($rate_type === 'daily') {
$rate = floatval($price_daily);
} elseif ($rate_type === 'yearly') {
$rate = floatval($price_year);
} else {
$rate = floatval($price_monthly);
}
}
$stmt->close();
return $rate;
}
function billing_fail_add_to_cart(string $message, array $context = []): void
{
site_log_error('add_to_cart_failed', array_merge(['message' => $message], $context));
@ -97,6 +126,9 @@ $ip_id = isset($_POST['ip_id']) ? intval($_POST['ip_id']) : 0;
$max_players = isset($_POST['max_players']) ? intval($_POST['max_players']) : 0;
$qty = isset($_POST['qty']) ? intval($_POST['qty']) : 1;
$invoice_duration = isset($_POST['invoice_duration']) ? $_POST['invoice_duration'] : 'month';
$display_service_id = isset($_POST['display_service_id']) ? intval($_POST['display_service_id']) : 0;
$display_rate = isset($_POST['display_rate']) ? floatval($_POST['display_rate']) : 0.0;
$posted_total = isset($_POST['calculated_total']) ? floatval($_POST['calculated_total']) : 0.0;
$remote_control_password = isset($_POST['remote_control_password']) ? trim((string)$_POST['remote_control_password']) : '';
$ftp_password = isset($_POST['ftp_password']) ? trim((string)$_POST['ftp_password']) : '';
@ -168,6 +200,17 @@ if ($service_id > 0) {
}
}
if ($base_rate <= 0 && $display_service_id > 0) {
$fallback_rate = billing_rate_from_service($db, $table_prefix, $display_service_id, $durationInfo['rate_type']);
if ($fallback_rate > 0) {
$base_rate = $fallback_rate;
}
}
if ($base_rate <= 0 && $display_rate > 0) {
$base_rate = $display_rate;
}
if ($remote_control_password === '' || strcasecmp($remote_control_password, 'ChangeMe') === 0) {
$remote_control_password = billing_generate_password();
}
@ -181,7 +224,12 @@ $status = 'due'; // Invoice status: due (unpaid), paid
$payment_status = 'unpaid';
$qty = max(1, $qty);
$max_players = max(1, $max_players);
$subtotal_cents = billing_money_to_cents((float)$base_rate * $max_players * $qty);
$rate_per_player_cents = max(0, billing_money_to_cents($base_rate));
$subtotal_cents = $rate_per_player_cents * $max_players * $qty;
$posted_total_cents = max(0, billing_money_to_cents($posted_total));
if ($subtotal_cents <= 0 && $posted_total_cents > 0 && $base_rate > 0) {
$subtotal_cents = $posted_total_cents;
}
$subtotal = billing_cents_to_money($subtotal_cents);
$amount = $subtotal;
$period_end = date('Y-m-d H:i:s', strtotime('+' . ($durationInfo['days'] * $qty) . ' days'));

View file

@ -317,11 +317,13 @@ $siteBase = $protocol . $host;
}
.cart-container {
max-width: 900px;
margin: 40px auto;
margin: 24px auto;
background: white;
padding: 30px;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
width: min(100%, calc(100% - 24px));
box-sizing: border-box;
}
h1 {
color: #333;
@ -359,6 +361,7 @@ $siteBase = $protocol . $host;
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
table-layout: fixed;
}
.cart-table th {
background: #f8f9fa;
@ -418,6 +421,7 @@ $siteBase = $protocol . $host;
display: flex;
gap: 10px;
align-items: flex-end;
flex-wrap: wrap;
}
.coupon-form > div {
flex: 1;
@ -524,6 +528,86 @@ $siteBase = $protocol . $host;
}
.action-buttons {
margin-top: 30px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.cart-table-wrap {
width: 100%;
max-width: 100%;
overflow-x: auto;
}
@media (max-width: 768px) {
.cart-container {
width: min(100%, calc(100% - 12px));
padding: 14px;
margin: 12px auto;
}
h1 {
font-size: 1.5rem;
margin-bottom: 18px;
}
.cart-table thead {
display: none;
}
.cart-table,
.cart-table tbody,
.cart-table tr,
.cart-table td {
display: block;
width: 100%;
}
.cart-table tr {
border: 1px solid #dee2e6;
border-radius: 8px;
margin-bottom: 12px;
padding: 6px 8px;
background: #fff;
}
.cart-table td {
border: 0;
padding: 6px 4px;
text-align: left !important;
}
.cart-table td[data-label]::before {
content: attr(data-label) ": ";
font-weight: 600;
color: #495057;
}
.coupon-form {
flex-direction: column;
align-items: stretch;
}
.coupon-form button {
width: 100%;
}
.coupon-applied {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.cart-total {
text-align: left;
}
.cart-total-row {
display: flex;
justify-content: space-between;
gap: 10px;
}
.cart-total-label,
.cart-total-amount,
.subtotal-amount,
.discount-amount {
font-size: 1rem;
margin-right: 0;
}
.btn {
width: 100%;
text-align: center;
}
.action-buttons {
margin-top: 16px;
}
}
</style>
<?php // Font Awesome for small icon buttons ?>
@ -562,6 +646,7 @@ $siteBase = $protocol . $host;
<a href="/serverlist.php" class="btn">Browse Servers</a>
</div>
<?php else: ?>
<div class="cart-table-wrap">
<table class="cart-table">
<thead>
<tr>
@ -576,20 +661,20 @@ $siteBase = $protocol . $host;
<tbody>
<?php foreach ((array)$invoices as $inv): ?>
<tr>
<td>
<td data-label="Game Server">
<div class="game-name"><?php echo htmlspecialchars($inv['game_name'] ?? 'Game Server'); ?></div>
<div class="server-name"><?php echo htmlspecialchars($inv['home_name']); ?></div>
<?php if (!empty($inv['description'])): ?>
<div class="description"><?php echo htmlspecialchars($inv['description']); ?></div>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($inv['invoice_duration']); ?></td>
<td><?php echo intval($inv['qty']); ?>x</td>
<td><span class="status-badge"><?php echo htmlspecialchars(strtoupper($inv['status'])); ?></span></td>
<td style="text-align: right;">
<span class="price">$<?php echo number_format(floatval($inv['amount']), 2); ?></span>
<td data-label="Duration"><?php echo htmlspecialchars((string)($inv['invoice_duration'] ?? 'month')); ?></td>
<td data-label="Quantity"><?php echo intval($inv['qty'] ?? 1); ?>x</td>
<td data-label="Status"><span class="status-badge"><?php echo htmlspecialchars(strtoupper((string)($inv['status'] ?? 'due'))); ?></span></td>
<td data-label="Price" style="text-align: right;">
<span class="price">$<?php echo number_format(floatval($inv['total_due'] ?? $inv['amount'] ?? 0), 2); ?></span>
</td>
<td style="text-align: right;">
<td data-label="Action" style="text-align: right;">
<button type="button" class="btn btn-secondary btn-small" title="Remove" onclick="removeInvoice(<?php echo intval($inv['invoice_id']); ?>)">
<i class="fa-solid fa-trash"></i>
</button>
@ -598,6 +683,7 @@ $siteBase = $protocol . $host;
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Coupon Section -->
<div class="coupon-section">

View file

@ -85,7 +85,7 @@ if ($couponCode !== '') {
// Calculate total and verify it is $0 after discount
$totalAmountCents = 0;
foreach ($invoices as $inv) {
$lineAmount = (float)($inv['amount'] ?? 0);
$lineAmount = (float)($inv['total_due'] ?? $inv['amount'] ?? 0);
$totalAmountCents += billing_free_money_to_cents($lineAmount);
}
$discountAmountCents = (int) round($totalAmountCents * ($discountPct / 100.0));

View file

@ -1,8 +1,21 @@
/* Global font family - legible sans-serif stack */
html,
body {
width: 100%;
max-width: 100%;
overflow-x: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
#gsw-site {
width: 100%;
max-width: 100%;
overflow-x: hidden;
}
.gsw-top{display:flex;align-items:center;gap:12px;padding:12px 24px;background:#fff;border-bottom:1px solid rgba(0,0,0,0.05);}
.gsw-top img{height:40px;width:auto;display:block}
.gsw-top .gsw-site-name{font-weight:700;font-size:1.1rem;color:#333}
@ -12,7 +25,7 @@ body {
.gsw-header{display:flex;flex-direction:column;align-items:stretch;padding:0;background:transparent;margin-bottom:18px;}
/* Top row: contains left (logo/title) and right (login) divs as separate blocks */
#gsw-site .gsw-header-top{display:flex;flex-direction:row;justify-content:space-between;align-items:center;padding:12px 20px;background:#0b3b6f !important;backdrop-filter:blur(6px);box-shadow:0 2px 6px rgba(0,0,0,0.18);width:100%;}
#gsw-site .gsw-header-top{display:flex;flex-direction:row;justify-content:space-between;align-items:center;padding:12px 20px;background:#0b3b6f !important;backdrop-filter:blur(6px);box-shadow:0 2px 6px rgba(0,0,0,0.18);width:100%;max-width:100%;}
/* Left div: logo + title, takes up available space */
#gsw-site .gsw-header-left{flex:1 1 auto;display:flex;align-items:center;font-weight:700;font-size:1.4rem;color:#fff;padding-left:8px;}
@ -25,8 +38,8 @@ body {
.gsw-header-left a{color:#fff;text-decoration:none;}
/* Bottom row: centered navigation menu */
#gsw-site .gsw-header-bottom{display:flex;justify-content:center;padding:10px 20px;background:#0b3b6f !important;width:100%;}
.gsw-header-nav{display:flex;gap:22px;align-items:center;}
#gsw-site .gsw-header-bottom{display:flex;justify-content:center;padding:10px 20px;background:#0b3b6f !important;width:100%;max-width:100%;}
.gsw-header-nav{display:flex;gap:22px;align-items:center;max-width:100%;}
.gsw-nav-link{color:#fff;text-decoration:none;font-size:0.98rem;transition:opacity 0.2s;padding:6px 8px;border-radius:6px;}
.gsw-nav-link:hover{opacity:0.9;text-decoration:underline;background:rgba(255,255,255,0.03);}
/* My Account link styling - larger font in middle of menu */
@ -79,7 +92,7 @@ input, textarea, select, button { color: #fff; background: #11141f; border: 1px
.cart-total-value{padding:1rem 1.5rem; text-align:left; border-top:2px solid rgba(255,255,255,0.06); font-weight:600; color:#fff; font-size:1.1rem}
/* Utility classes */
.container-wide{width:100%; max-width:1000px; margin:28px auto;}
.container-wide{width:100%; max-width:1000px; margin:28px auto; padding-inline:12px; box-sizing:border-box;}
.panel{background:rgba(0,0,0,0.25); padding:16px; border-radius:8px}
.muted{color:rgba(255,255,255,0.6)}
.center{text-align:center}
@ -243,12 +256,12 @@ input, textarea, select, button { color: #fff; background: #11141f; border: 1px
/* Navigation: wrap and stack for easier tapping */
#gsw-site .gsw-header-bottom{padding:8px 12px}
.gsw-header-nav{flex-direction:column;align-items:stretch;gap:10px;width:100%}
.gsw-header-nav{flex-direction:column;align-items:stretch;gap:10px;width:100%;max-width:100%}
.gsw-nav-link{display:block;padding:12px 10px;border-radius:8px}
.gsw-nav-link-myaccount{font-size:1rem}
/* Make main panel use full width with reduced padding */
.site-panel{padding:0.75rem;margin:8px;border-radius:0.5rem}
.site-panel{padding:0.75rem;margin:8px;border-radius:0.5rem;max-width:100%}
/* Tables and cart spacing adjustments */
.cart-table th, .cart-table td{padding:0.6rem 0.8rem}
@ -264,7 +277,7 @@ input, textarea, select, button { color: #fff; background: #11141f; border: 1px
.server-item{padding:12px}
/* Forms: make inputs and action buttons full width */
.form-group input, .form-group textarea, .form-group select{width:100%;box-sizing:border-box}
.form-group input, .form-group textarea, .form-group select{width:100%;box-sizing:border-box;max-width:100%}
/* Invoice items: stack label and amount for readability */
.invoice-item{flex-direction:column;align-items:flex-start;gap:8px}
@ -385,3 +398,26 @@ input, textarea, select, button { color: #fff; background: #11141f; border: 1px
#gsw-site .server-card img,
.server-list img{max-width:100%;height:auto;display:block;object-fit:cover}
#gsw-site img,
#gsw-site video,
#gsw-site iframe,
#gsw-site canvas,
#gsw-site svg {
max-width: 100%;
height: auto;
}
#gsw-site table {
max-width: 100%;
}
#gsw-site input,
#gsw-site select,
#gsw-site textarea,
#gsw-site button,
#gsw-site .btn,
#gsw-site .gsw-btn,
#gsw-site .gsw-btn-secondary {
max-width: 100%;
box-sizing: border-box;
}

View file

@ -2,10 +2,6 @@
// Start a separate session for the website (not the panel session)
session_name("opengamepanel_web");
session_start();
// Enable error display for debugging the white screen issue. Remove or gate in production.
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// We'll compute a site root below (up to /_website) and define a strict sanitizer after config is loaded
@ -144,21 +140,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
box-sizing: border-box;
}
html,
body {
width: 100%;
max-width: 100%;
overflow-x: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: block;
padding: 0; /* we'll handle padding in content wrapper */
margin: 0;
padding: 0;
}
/* content area below the top/menu; aligns the login box to the right */
.content{
display:flex;
align-items:center;
justify-content:flex-end;
min-height: calc(100vh - 140px); /* leave room for header/menu */
padding:20px;
justify-content:center;
min-height: calc(100vh - 220px);
padding: 24px 16px 32px;
}
.login-container {
@ -167,7 +170,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.28);
width: 100%;
max-width: 420px;
padding: 40px;
padding: 32px 28px;
border: 1px solid rgba(0,0,0,0.06);
}
@ -261,6 +264,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
.footer-links {
margin-top: 24px;
text-align: center;
word-break: break-word;
}
.footer-links a {
@ -279,6 +283,65 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
color: #999;
font-size: 0.85rem;
}
.login-links {
margin-top: 12px;
text-align: center;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 6px 10px;
line-height: 1.5;
}
.login-links a {
color: #667eea;
text-decoration: none;
}
.login-links a:hover {
text-decoration: underline;
}
@media (max-width: 600px) {
.content {
min-height: auto;
padding: 14px 10px 20px;
}
.login-container {
max-width: 100%;
padding: 20px 16px;
border-radius: 10px;
}
.login-header {
margin-bottom: 18px;
}
.login-header h1 {
font-size: 1.35rem;
}
.login-header p,
.form-group label,
.form-group input,
.btn-login {
font-size: 0.95rem;
}
.form-group {
margin-bottom: 14px;
}
.form-group input {
padding: 10px 12px;
}
.btn-login {
padding: 11px 12px;
}
}
</style>
</head>
<body>
@ -319,8 +382,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<button type="submit" name="login" class="btn-login">Sign In</button>
</form>
<div class="center mt-12">
<a href="register.php">Register</a> |
<div class="login-links">
<a href="register.php">Register</a>
<span aria-hidden="true">|</span>
<a href="forgot_password.php">Forgot Password?</a>
</div>
@ -335,4 +399,3 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
</body>
<?php include(__DIR__ . '/includes/footer.php'); ?>
</html>

View file

@ -4,6 +4,57 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Order Server - GameServers.World</title>
<link rel="stylesheet" href="css/header.css">
<style>
body { margin: 0; padding: 0; }
.order-shell { width: min(1100px, 100% - 24px); margin: 20px auto 28px; }
.order-catalog-item { margin: 0 0 20px; }
.order-layout { display: flex; gap: 18px; align-items: flex-start; flex-wrap: wrap; }
.order-media { flex: 0 1 310px; max-width: 100%; }
.order-media img { width: 100%; max-width: 280px; height: auto; border-radius: 8px; display: block; }
.order-media-title { margin: 10px 0 6px; text-align: center; }
.order-media-desc { color: #c6c6c6; max-width: 100%; word-break: break-word; }
.order-form-card { flex: 1 1 500px; max-width: 100%; background: rgba(0,0,0,0.25); border-radius: 10px; padding: 14px; }
.order-form-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.order-form-table td { padding: 8px 6px; vertical-align: top; }
.order-form-table td:first-child { width: 34%; }
.order-form-table input[type="text"],
.order-form-table input[type="number"],
.order-form-table select,
.order-form-table textarea {
width: 100%;
min-height: 40px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.2);
padding: 8px 10px;
}
.order-form-table input[type="radio"] { margin-right: 8px; }
.location-option { margin-bottom: 8px; }
.slidecontainer { max-width: 100%; }
.slidecontainer .slider { width: 100%; }
.order-pricing { line-height: 1.5; word-break: break-word; }
.order-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.order-actions .gsw-btn,
.order-actions .gsw-btn-secondary { width: auto; }
.order-back-form { margin: 0; }
@media (max-width: 768px) {
.order-shell { width: min(100%, calc(100% - 16px)); margin: 12px auto 20px; }
.order-layout { gap: 12px; }
.order-media { flex: 1 1 100%; display: flex; flex-direction: column; align-items: center; }
.order-media img { max-width: min(100%, 260px); }
.order-form-card { flex: 1 1 100%; padding: 10px; }
.order-form-table,
.order-form-table tbody,
.order-form-table tr,
.order-form-table td { display: block; width: 100%; }
.order-form-table td:first-child { width: 100%; padding-bottom: 2px; }
.order-form-table td { padding: 6px 4px; }
.order-actions { flex-direction: column; }
.order-actions .gsw-btn,
.order-actions .gsw-btn-secondary { width: 100%; text-align: center; }
}
</style>
</head>
<body>
<?php
@ -143,6 +194,7 @@ if ($osColCheck && $osColCheck->num_rows > 0) {
}
?>
<div class="order-shell">
<div class="clearfix">
<?php
foreach ($serviceRows as $row)
@ -150,7 +202,7 @@ foreach ($serviceRows as $row)
if (!isset($_REQUEST['service_id']))
{
?>
<div class="float-left p-30-20">
<div class="float-left p-30-20 order-catalog-item">
<?php
$imgSrc = billing_image_url((string)($row['img_url'] ?? ''));
if ($imgSrc === '') { $imgSrc = '/images/games/default_server.png'; }
@ -216,14 +268,15 @@ $osServiceMap[$svcGameOs] = (int)$row['service_id'];
$osServiceMapJson = json_encode($osServiceMap, JSON_THROW_ON_ERROR);
?>
<div class="float-left decorative-bottom">
<div class="order-layout">
<div class="order-media decorative-bottom">
<?php
$imgSrc = billing_image_url((string)($row['img_url'] ?? ''));
if ($imgSrc === '') { $imgSrc = '/images/games/default_server.png'; }
?>
<img src="<?php echo htmlspecialchars($imgSrc, ENT_QUOTES, 'UTF-8'); ?>" width="230" height="112"
<img src="<?php echo htmlspecialchars($imgSrc, ENT_QUOTES, 'UTF-8'); ?>" alt="<?php echo htmlspecialchars($canonicalGameName, ENT_QUOTES, 'UTF-8'); ?>"
onerror="this.src='/images/games/default_server.png'; this.onerror=null;">
<center><b><?php echo htmlspecialchars($canonicalGameName, ENT_QUOTES, 'UTF-8'); ?></b></center>
<center class="order-media-title"><b><?php echo htmlspecialchars($canonicalGameName, ENT_QUOTES, 'UTF-8'); ?></b></center>
<?php
$isAdmin = false;
if ($isAdmin) {
@ -242,16 +295,20 @@ echo "<form action='' method='post'>"
. "</form>";
}
} else {
echo "<p style='color:gray;width:280px;'>" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "</p>";
echo "<p class='order-media-desc'>" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "</p>";
}
?>
</div>
<table class="float-left">
<div class="order-form-card">
<table class="order-form-table">
<form method="post" action="add_to_cart.php">
<!-- service_id is updated by JS when the location OS changes -->
<input type="hidden" id="order_service_id" name="service_id" value="<?php echo intval($row['service_id']); ?>">
<input type="hidden" name="display_service_id" value="<?php echo intval($row['service_id']); ?>">
<input type="hidden" name="remote_control_password" value="">
<input type="hidden" name="ftp_password" value="">
<input type="hidden" name="ftp_password" value="">
<input type="hidden" name="display_rate" id="displayRateInput" value="<?php echo number_format(floatval($row['price_monthly']), 2, '.', ''); ?>">
<input type="hidden" name="calculated_total" id="calculatedTotalInput" value="">
<tr>
<td align="right"><b>Game Server Name</b> </td>
<td align="left">
@ -318,7 +375,7 @@ continue; // Incompatible OS variant with no fallback service
$available_server = true;
$firstServer = false;
$safeOs = htmlspecialchars($rsOs, ENT_QUOTES, 'UTF-8');
echo "<div>\n"
echo "<div class='location-option'>\n"
. " <input type='radio' name='ip_id' id='rs_{$rsID}' value='{$rsID}' data-os='{$safeOs}' required{$checked} onchange='gspUpdateServiceId(this)'>\n"
. " <label for='rs_{$rsID}'>{$rsNAME}</label>\n"
. "</div>\n";
@ -338,7 +395,7 @@ $rsResult->free();
<center><b>Months</b></center>
<input type="range" name="qty" min="1" max="24" value="1" class="slider" id="invoiceRange">
<p>Player Slots: <span id="playerSlots"></span><br>
<p class="order-pricing">Player Slots: <span id="playerSlots"></span><br>
<span>Price: $<?php echo number_format(floatval($row['price_monthly']), 2); ?> USD</span><br>
<span id="invoiceDuration"></span><br>
<span id="totalPrice"></span></p>
@ -361,7 +418,12 @@ 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);
var total = (slots * months * pricePerSlot).toFixed(2);
price.innerHTML = "Total Price: $" + total;
var totalInput = document.getElementById("calculatedTotalInput");
if (totalInput) {
totalInput.value = total;
}
}
recalc();
slider.oninput = recalc;
@ -398,7 +460,9 @@ if (checked) { window.gspUpdateServiceId(checked); }
$is_logged_in = (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) || (isset($_SESSION['website_username']) && !empty($_SESSION['website_username']));
?>
<?php if ($available_server && $is_logged_in): ?>
<div class="order-actions">
<button type="submit" name="add_to_cart" class="gsw-btn">Add to Cart</button>
</div>
<?php elseif (!$is_logged_in): ?>
<div class="login-placeholder">Please <a href="login.php">login</a> to order</div>
<?php else: ?>
@ -409,17 +473,22 @@ $is_logged_in = (isset($_SESSION['website_user_id']) && !empty($_SESSION['websit
</tr>
<tr>
<td align="left" colspan="2">
<form action="serverlist.php" method="GET">
<form action="serverlist.php" method="GET" class="order-back-form">
<div class="order-actions">
<button class="gsw-btn-secondary">Back to List</button>
</div>
</form>
</td>
</tr>
</table>
</div>
</div>
<?php
}
}
?>
</div>
</div>
<?php
// Close database connection
billing_maybe_close_db($db);

View file

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