= website_escape($description !== '' ? $description : 'Virtual private hosting with full configuration access, mod support, GSP panel management, and optional custom engineering help.') ?>
-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();