diff --git a/Panel/modules/website/README.md b/Panel/modules/website/README.md index 8e0a3bf7..07c02c7f 100644 --- a/Panel/modules/website/README.md +++ b/Panel/modules/website/README.md @@ -20,6 +20,7 @@ Panel/modules/website/ checkout.php payment_success.php payment_cancel.php + server_status.php docs.php login.php register.php @@ -67,6 +68,7 @@ Panel/modules/website/ game_servers.php cart.php documentation.php + server_status.php pricing.php locations.php panel_features.php @@ -96,10 +98,7 @@ Primary navigation: - Home - Game Servers -- Pricing -- Locations - Panel Features -- Documentation - Support Account area: @@ -117,13 +116,15 @@ Action links: external link that opens the operational GSP Panel at the configured Panel URL. The homepage should not duplicate the header Control Panel action with extra -promotional Control Panel blocks. Pricing and location summaries on the homepage -must use customer-facing sales language rather than internal planning notes. +promotional Control Panel blocks. Pricing belongs on the catalog and order pages. +The homepage location summary must use customer-facing language and link to the +website-branded `server_status.php` page. Footer placement: - one modest `Control Panel` link under account/services - one `Panel Features` informational link in the explore links +- no public documentation links in shared navigation, footer, support, or server catalog actions ## Public copy policy @@ -223,7 +224,7 @@ More detail: `docs/modules/website_billing_rebuild.md`. The public game-server catalog is a compact row/list view. It should show customer-facing service names, user-friendly platform labels, short descriptions, -pricing, ordering, and documentation actions. Raw XML/config keys are used +per-slot monthly pricing, and ordering actions. Raw XML/config keys are used internally for service lookup and documentation routing, but should not be shown as the public platform label. @@ -239,6 +240,23 @@ platform label. The staff service editor uses expandable rows so service ID, enabled status, display name, platform, price, slot range, image status, and locations are easier to scan. +Catalog and order pages display `price_monthly` as a per-slot monthly price +using wording such as `$0.50 per slot / monthly`. Cart and invoice totals +multiply that per-slot price by selected slots and billing duration. + +Order-page location labels are resolved from the current `remote_servers` table. +The helper prefers customer-facing fields such as `display_name`, +`location_name`, `server_location`, `region`, `remote_server_name`, and +`hostname`, with public IP fields used only as a fallback. Generic `Location N` +labels should not appear when usable database names exist. + +## Website server status + +`server_status.php` is a Gameservers.World-branded status page. It reads active +remote-server rows from the configured Panel database and displays +customer-facing location, address, and availability status with a return link to +the website. It does not send visitors to the Panel status page. + ## Documentation source Customer documentation is read from the existing billing docs directory: @@ -246,6 +264,8 @@ Customer documentation is read from the existing billing docs directory: - `Panel/modules/billing/docs/` This keeps the website portable without duplicating the documentation tree. +The docs implementation may remain available internally, but documentation is +not presented as a public top-level navigation item in the current website menu. ## Deployment diff --git a/Panel/modules/website/assets/css/site.css b/Panel/modules/website/assets/css/site.css index b0c755a3..a7e7f984 100644 --- a/Panel/modules/website/assets/css/site.css +++ b/Panel/modules/website/assets/css/site.css @@ -990,6 +990,46 @@ textarea { border-collapse: collapse; } +.status-page-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 18px; +} + +.status-table-wrap { + overflow-x: auto; + border: 1px solid var(--line); + border-radius: var(--radius); +} + +.status-table { + width: 100%; + border-collapse: collapse; + min-width: 520px; +} + +.status-table th, +.status-table td { + padding: 14px 16px; + border-bottom: 1px solid var(--line); + text-align: left; +} + +.status-table th { + color: #b8cae5; + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.status-table tr:last-child td { + border-bottom: 0; +} + .doc-view th, .doc-view td { padding: 10px 12px; @@ -1072,7 +1112,7 @@ textarea { } } -@media (max-width: 820px) { +@media (max-width: 980px) { .header-shell { grid-template-columns: auto auto; gap: 16px; @@ -1197,6 +1237,41 @@ textarea { flex: 1 1 160px; } + .status-table { + min-width: 0; + } + + .status-table thead { + display: none; + } + + .status-table, + .status-table tbody, + .status-table tr, + .status-table td { + display: block; + width: 100%; + } + + .status-table tr { + padding: 12px 0; + border-bottom: 1px solid var(--line); + } + + .status-table td { + border-bottom: 0; + padding: 6px 12px; + } + + .status-table td::before { + content: attr(data-label) ": "; + color: #b8cae5; + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; + } + .staff-service-row summary::after { justify-self: start; } diff --git a/Panel/modules/website/includes/billing.php b/Panel/modules/website/includes/billing.php index eaf0f7da..5c1dcef3 100644 --- a/Panel/modules/website/includes/billing.php +++ b/Panel/modules/website/includes/billing.php @@ -439,7 +439,7 @@ function website_create_invoice_from_cart(int $userId): ?int 'remote_server_id' => (int)$locationId, 'slots' => $slots, 'duration_months' => $durationMonths, - 'price' => $price * $durationMonths, + 'price' => $price * $slots * $durationMonths, 'price_monthly' => $price, ]; $validItems[] = $line; diff --git a/Panel/modules/website/includes/bootstrap.php b/Panel/modules/website/includes/bootstrap.php index c0f1fcaa..cb2d5d55 100644 --- a/Panel/modules/website/includes/bootstrap.php +++ b/Panel/modules/website/includes/bootstrap.php @@ -783,17 +783,91 @@ function website_service_locations(array $service): array } $locations = []; + $remoteServers = website_remote_server_labels(); foreach (preg_split('/[\s,]+/', $raw) ?: [] as $remoteServerId) { $remoteServerId = trim($remoteServerId); if ($remoteServerId === '' || !ctype_digit($remoteServerId)) { continue; } - $locations[$remoteServerId] = 'Location ' . $remoteServerId; + $locations[$remoteServerId] = $remoteServers[(int)$remoteServerId] ?? 'Server Location ' . $remoteServerId; } return $locations; } +function website_remote_server_labels(): array +{ + static $labels = null; + if ($labels !== null) { + return $labels; + } + + $labels = []; + $db = website_db(); + if (!$db instanceof mysqli) { + return $labels; + } + + $table = website_table_prefix() . 'remote_servers'; + if (!website_table_exists($table)) { + return $labels; + } + + $columns = website_table_columns($table); + $candidateColumns = [ + 'display_name', + 'location_name', + 'server_location', + 'region', + 'remote_server_name', + 'hostname', + 'agent_ip', + 'display_public_ip', + ]; + $selectParts = ['`remote_server_id`']; + foreach ($candidateColumns as $column) { + if (isset($columns[$column])) { + $safeColumn = str_replace('`', '``', $column); + $selectParts[] = "`{$safeColumn}`"; + } + } + + $safeTable = str_replace('`', '``', $table); + $result = @$db->query('SELECT ' . implode(', ', $selectParts) . " FROM `{$safeTable}` ORDER BY `remote_server_id` ASC"); + if (!$result instanceof mysqli_result) { + return $labels; + } + + while ($row = $result->fetch_assoc()) { + $id = (int)($row['remote_server_id'] ?? 0); + if ($id <= 0) { + continue; + } + $parts = []; + foreach (['display_name', 'location_name', 'server_location', 'region', 'remote_server_name', 'hostname'] as $column) { + $value = trim((string)($row[$column] ?? '')); + if ($value !== '' && !in_array($value, $parts, true)) { + $parts[] = $value; + } + } + if (empty($parts)) { + foreach (['display_public_ip', 'agent_ip'] as $column) { + $value = trim((string)($row[$column] ?? '')); + if ($value !== '') { + $parts[] = $value; + break; + } + } + } + if (!empty($parts)) { + $labels[$id] = implode(' - ', array_slice($parts, 0, 2)); + } + } + $result->free(); + + return $labels; +} + function website_cart_items(): array { website_start_session(); diff --git a/Panel/modules/website/includes/footer.php b/Panel/modules/website/includes/footer.php index fa006311..61296138 100644 --- a/Panel/modules/website/includes/footer.php +++ b/Panel/modules/website/includes/footer.php @@ -27,10 +27,7 @@ $currentUser = website_current_user();

Explore

@@ -61,7 +58,6 @@ $currentUser = website_current_user();

Help and Projects

diff --git a/Panel/modules/website/pages/order.php b/Panel/modules/website/pages/order.php index b6154b28..2125d53f 100644 --- a/Panel/modules/website/pages/order.php +++ b/Panel/modules/website/pages/order.php @@ -27,11 +27,12 @@ $platformLabel = website_service_platform($service); $description = trim((string)($service['description'] ?? '')); $price = (float)($service['price_monthly'] ?? 0); $selectedSlots = max((int)$minSlots, (int)($_POST['slots'] ?? $minSlots)); +$monthlyEstimate = $price > 0 ? $price * $selectedSlots : 0.0; ?>

Configure

-

+

@@ -45,18 +46,17 @@ $selectedSlots = max((int)$minSlots, (int)($_POST['slots'] ?? $minSlots));

Plan

Platform:

-

0 ? 'Catalog price: $' . website_escape(number_format($price, 2)) . ' / month' : 'Catalog price: contact for pricing' ?>

+

0 ? 'Price: $' . website_escape(number_format($price, 2)) . ' per slot / monthly' : 'Price: contact for pricing' ?>

Slots and location

Minimum slots:

0 ? 'Maximum slots: ' . website_escape((string)$maxSlots) : 'Maximum slots depend on game and available location capacity.' ?>

-

Location availability is confirmed before payment or provisioning.

-

Checkout boundary

-

You can add this server to your cart before logging in. Payment and final provisioning must complete server-side before the Panel creates a running game server.

-

Return to the game-server catalog or contact support if this package should be available.

+

Estimated monthly total

+

0 ? '$' . website_escape(number_format($monthlyEstimate, 2)) . ' / monthly' : 'Contact for pricing' ?>

+

Based on selected slots.

@@ -83,7 +83,7 @@ $selectedSlots = max((int)$minSlots, (int)($_POST['slots'] ?? $minSlots)); -

The website stores this selection in your cart and revalidates service, slots, location, and price during checkout.

+

You can review your selection in the cart before checkout.

Back to Catalog diff --git a/Panel/modules/website/pages/server_status.php b/Panel/modules/website/pages/server_status.php new file mode 100644 index 00000000..7a32dffd --- /dev/null +++ b/Panel/modules/website/pages/server_status.php @@ -0,0 +1,56 @@ + +
+
+

Server Status

+

Check current availability for configured Gameservers.World hosting locations.

+
+
+ +
+
+
+ + + +
+

Status is unavailable

+

We could not load hosting location status right now. Please contact support if you need help choosing a location.

+ +
+ +
+ + + + + + + + + + + + + + + + + +
LocationAddressStatus
+
+ +
+
+
diff --git a/Panel/modules/website/pages/support.php b/Panel/modules/website/pages/support.php index 3c7b3d1a..cc945836 100644 --- a/Panel/modules/website/pages/support.php +++ b/Panel/modules/website/pages/support.php @@ -34,7 +34,7 @@ $loginUnavailable = $loginUnavailable ?? false;

Modding and troubleshooting

Use the documentation set for server guides, panel workflows, Workshop help, and technical troubleshooting across current games, older multiplayer titles, and community-maintained servers.

- Open Docs + Browse Game Servers
diff --git a/Panel/modules/website/server_status.php b/Panel/modules/website/server_status.php new file mode 100644 index 00000000..b6e720d6 --- /dev/null +++ b/Panel/modules/website/server_status.php @@ -0,0 +1,97 @@ +query('SELECT ' . implode(', ', $select) . " FROM `{$safeTable}` ORDER BY `remote_server_id` ASC"); + if (!$result instanceof mysqli_result) { + return []; + } + + $rows = []; + while ($row = $result->fetch_assoc()) { + if (isset($row['enabled']) && (int)$row['enabled'] !== 1) { + continue; + } + $nameParts = []; + foreach (['display_name', 'location_name', 'server_location', 'region', 'remote_server_name', 'hostname'] as $column) { + $value = trim((string)($row[$column] ?? '')); + if ($value !== '' && !in_array($value, $nameParts, true)) { + $nameParts[] = $value; + } + } + $agentIp = trim((string)($row['agent_ip'] ?? $row['display_public_ip'] ?? '')); + $displayIp = trim((string)($row['display_public_ip'] ?? $agentIp)); + $agentPort = (int)($row['agent_port'] ?? 0); + $reachable = website_status_socket_reachable($agentIp, $agentPort); + if ($reachable === true) { + $status = 'Online'; + $statusClass = ''; + } elseif ($reachable === false) { + $status = 'Unavailable'; + $statusClass = 'status-badge-muted'; + } else { + $status = 'Status pending'; + $statusClass = 'status-badge-neutral'; + } + $rows[] = [ + 'name' => $nameParts !== [] ? implode(' - ', array_slice($nameParts, 0, 2)) : 'Hosting location', + 'address' => $displayIp, + 'status' => $status, + 'status_class' => $statusClass, + ]; + } + $result->free(); + return $rows; +} + +website_render( + 'server_status.php', + [ + 'activePage' => 'status', + 'pageTitle' => 'Server Status - Gameservers.World', + 'metaDescription' => 'View current Gameservers.World hosting location availability.', + 'canonicalPath' => 'server_status.php', + 'checkedAt' => gmdate('Y-m-d H:i:s') . ' UTC', + 'statusRows' => website_status_locations(), + ] +); diff --git a/docs/modules/website.md b/docs/modules/website.md index 2d441260..ce80d6bc 100644 --- a/docs/modules/website.md +++ b/docs/modules/website.md @@ -73,7 +73,7 @@ Paid orders appear in the website provisioning queue. The queue is the handoff p The public `serverlist.php` catalog uses a compact row/list layout so many services can be scanned quickly. Customer-facing rows show the display name, -derived platform label, short description, price, order action, and documentation +derived platform label, short description, per-slot monthly price, and order action. Raw XML/config keys such as `*_linux64` or `*_win32` are internal identifiers and should not be prominent public labels. @@ -100,10 +100,7 @@ Primary links: - Home - Game Servers -- Pricing -- Locations - Panel Features -- Documentation - Support Account area: @@ -123,6 +120,9 @@ opens the operational Panel at the configured Panel URL. The footer should contain only one modest `Control Panel` link. Public pages, especially the homepage, must not repeat Control Panel promotions unnecessarily. +Pricing and locations are not top-level public navigation items. Game-specific +pricing is shown on the catalog and order pages, and locations are summarized on +the homepage with a link to the website-branded server status page. Control Panel links point directly to the configured Panel domain. `My Servers` opens a website customer page that summarizes website orders and links to the @@ -146,6 +146,22 @@ Avoid public wording such as: Use direct customer-facing language instead. +## Pricing, Locations, and Status + +Catalog and order pages display `price_monthly` as per-slot monthly pricing, for +example `$0.50 per slot / monthly`. Cart and invoice calculations multiply the +per-slot price by selected slot count and billing duration. + +Order-page location labels are resolved from `remote_servers` using the best +available customer-facing fields: `display_name`, `location_name`, +`server_location`, `region`, `remote_server_name`, or `hostname`. Public IP +fields are fallback labels only. Generic labels such as `Location 2` should not +be shown when a real database name is available. + +`server_status.php` is the website status page. It uses the configured Panel +database as its remote-server source, keeps the Gameservers.World theme, and +returns users to the website rather than the Panel dashboard. + ## Deployment Recommended: