fixed world site

This commit is contained in:
Frank Harris 2026-06-19 10:42:10 -05:00
parent 26fd3364ac
commit e2e56dbaf2
14 changed files with 365 additions and 56 deletions

View file

@ -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

View file

@ -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;
}

View file

@ -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;

View file

@ -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();

View file

@ -27,10 +27,7 @@ $currentUser = website_current_user();
<h2>Explore</h2>
<ul class="footer-links">
<li><a href="<?= website_escape(website_url('serverlist.php')) ?>">Game Servers</a></li>
<li><a href="<?= website_escape(website_url('pricing.php')) ?>">Pricing</a></li>
<li><a href="<?= website_escape(website_url('locations.php')) ?>">Locations</a></li>
<li><a href="<?= website_escape(website_url('panel_features.php')) ?>">Panel Features</a></li>
<li><a href="<?= website_escape(website_url('docs.php')) ?>">Documentation</a></li>
<li><a href="<?= website_escape(website_url('support.php')) ?>">Support</a></li>
</ul>
</div>
@ -61,7 +58,6 @@ $currentUser = website_current_user();
<div>
<h2>Help and Projects</h2>
<ul class="footer-links">
<li><a href="<?= website_escape(website_url('docs.php')) ?>">Server Guides</a></li>
<li><a href="<?= website_escape(website_custom_project_url()) ?>">Request Custom Work</a></li>
<?php if ($discordUrl !== ''): ?>
<li><a href="<?= website_escape($discordUrl) ?>" target="_blank" rel="noopener noreferrer">Discord</a></li>

View file

@ -8,10 +8,7 @@ $cartCount = website_cart_count();
$primaryLinks = [
['key' => 'home', 'label' => 'Home', 'href' => website_url('index.php')],
['key' => 'servers', 'label' => 'Game Servers', 'href' => website_url('serverlist.php')],
['key' => 'pricing', 'label' => 'Pricing', 'href' => website_url('pricing.php')],
['key' => 'locations', 'label' => 'Locations', 'href' => website_url('locations.php')],
['key' => 'panel-features', 'label' => 'Panel Features', 'href' => website_url('panel_features.php')],
['key' => 'docs', 'label' => 'Documentation', 'href' => website_url('docs.php')],
['key' => 'support', 'label' => 'Support', 'href' => website_url('support.php')],
];
if ($currentUser) {

View file

@ -45,7 +45,7 @@ if (website_request_method() === 'POST') {
$error = 'Select a valid billing duration.';
} else {
$priceMonthly = (float)($service['price_monthly'] ?? 0);
$monthlyTotal = $priceMonthly > 0 ? $priceMonthly : 0.0;
$monthlyTotal = $priceMonthly > 0 ? $priceMonthly * (int)$slots : 0.0;
$lineTotal = $monthlyTotal * (int)$durationMonths;
website_cart_add([
'service_id' => (int)$service['service_id'],

View file

@ -58,8 +58,7 @@ $fixedCapNote = trim((string)($pricing['fixed_cap_note'] ?? ''));
$description = trim((string)($service['description'] ?? ''));
$price = (float)($service['price_monthly'] ?? 0);
$orderHref = website_order_url((string)$service['service_id']);
$docSlug = trim((string)($service['cfg_game_key'] ?? ''));
$hasDoc = $docSlug !== '' && website_doc_path($docSlug) !== null;
$priceLabel = $price > 0 ? '$' . number_format($price, 2) . ' per slot / monthly' : 'Contact for pricing';
?>
<article class="catalog-row" role="listitem">
<div class="catalog-server">
@ -70,14 +69,9 @@ $fixedCapNote = trim((string)($pricing['fixed_cap_note'] ?? ''));
<span class="status-badge status-badge-neutral"><?= website_escape($platformLabel) ?></span>
</div>
<p class="catalog-description" data-label="Description"><?= website_escape($description !== '' ? $description : 'Virtual private hosting with full configuration access, mod support, GSP panel management, and optional custom engineering help.') ?></p>
<div class="catalog-price" data-label="Price"><?= $price > 0 ? '$' . number_format($price, 2) . ' / month' : 'Contact for pricing' ?></div>
<div class="catalog-price" data-label="Price"><?= website_escape($priceLabel) ?></div>
<div class="catalog-actions">
<a class="button button-primary" href="<?= website_escape($orderHref) ?>">Order Now</a>
<?php if ($hasDoc): ?>
<a class="button button-secondary" href="<?= website_escape(documentation_url($docSlug)) ?>">Documentation</a>
<?php else: ?>
<a class="button button-secondary" href="<?= website_escape(website_url('docs.php')) ?>">Documentation</a>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>

View file

@ -4,14 +4,7 @@ declare(strict_types=1);
$discordUrl = trim((string)website_config('discord_url', ''));
$customProjectUrl = website_custom_project_url();
$pricing = website_config('pricing', []);
$platform = website_config('platform', []);
$standardSlotStart = (float)($pricing['standard_slot_start'] ?? 0.50);
$legacyMonthlyStart = (float)($pricing['legacy_monthly_start'] ?? 4.00);
$standardMinSlots = (int)($pricing['standard_min_slots'] ?? 16);
$pricingDisclaimer = trim((string)($pricing['disclaimer'] ?? 'Actual prices are reflected on each server order page.'));
$customWorkNote = trim((string)($pricing['custom_work_note'] ?? 'Custom development and complex integration work are quoted separately.'));
$fixedCapNote = trim((string)($pricing['fixed_cap_note'] ?? 'Some fixed-cap or small co-op games use different slot limits.'));
$platformCapacityNote = trim((string)($platform['capacity_note'] ?? 'We actively monitor host capacity and avoid overcrowding servers onto overloaded machines.'));
$popularGames = ['Arma 3', 'DayZ', 'CS 1.6', 'TF2', 'Garry\'s Mod', 'Rust', 'Valheim'];
$locationSummary = ['USA East', 'USA Central', 'USA West', 'Europe'];
@ -80,6 +73,7 @@ $locationSummary = ['USA East', 'USA Central', 'USA West', 'Europe'];
$serviceName = website_service_name($service);
$platformLabel = website_service_platform($service);
$price = (float)($service['price_monthly'] ?? 0);
$priceLabel = $price > 0 ? '$' . number_format($price, 2) . ' per slot / monthly' : 'See order page';
$orderUrl = website_order_url((string)$service['service_id']);
?>
<article class="service-card">
@ -87,7 +81,7 @@ $locationSummary = ['USA East', 'USA Central', 'USA West', 'Europe'];
<header>
<h3><?= website_escape($serviceName) ?></h3>
<div class="service-meta">Platform: <?= website_escape($platformLabel) ?></div>
<div class="service-price"><?= $price > 0 ? '$' . number_format($price, 2) . ' / month' : 'See order page' ?></div>
<div class="service-price"><?= website_escape($priceLabel) ?></div>
</header>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape($orderUrl) ?>">Order</a>
@ -103,16 +97,6 @@ $locationSummary = ['USA East', 'USA Central', 'USA West', 'Europe'];
<section class="section">
<div class="container">
<div class="summary-grid">
<article class="summary-card">
<div class="kicker">Pricing summary</div>
<h3>Flexible hosting for different communities.</h3>
<p>Choose a game, player capacity, region, and available options for the server you want to run. Standard slot-based hosting starts at $<?= website_escape(number_format($standardSlotStart, 2)) ?> per slot, with most standard plans beginning at <?= website_escape((string)$standardMinSlots) ?> slots.</p>
<p>Selected lightweight legacy servers may start around $<?= website_escape(number_format($legacyMonthlyStart, 2)) ?> per month. <?= website_escape($fixedCapNote) ?></p>
<p><?= website_escape($customWorkNote) ?></p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(website_url('serverlist.php')) ?>">View Game Servers</a>
</div>
</article>
<article class="summary-card">
<div class="kicker">Locations</div>
<h3>Hosting locations for your players.</h3>
@ -123,7 +107,7 @@ $locationSummary = ['USA East', 'USA Central', 'USA West', 'Europe'];
<?php endforeach; ?>
</div>
<div class="card-actions">
<a class="button button-secondary" href="<?= website_escape(website_url('locations.php')) ?>">View Locations</a>
<a class="button button-secondary" href="<?= website_escape(website_url('server_status.php')) ?>">View Server Status</a>
</div>
</article>
</div>

View file

@ -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;
?>
<section class="page-heading">
<div class="container">
<h1>Configure <?= website_escape($serviceName) ?></h1>
<p><?= website_escape($description !== '' ? $description : 'Configure this server package before adding it to your cart. Login is only required when you proceed to checkout.') ?></p>
<p><?= website_escape($description !== '' ? $description : 'Choose your slot count, hosting location, and billing term before adding this server to your cart.') ?></p>
</div>
</section>
@ -45,18 +46,17 @@ $selectedSlots = max((int)$minSlots, (int)($_POST['slots'] ?? $minSlots));
<h3>Plan</h3>
<p><strong><?= website_escape($serviceName) ?></strong></p>
<p>Platform: <?= website_escape($platformLabel) ?></p>
<p><?= $price > 0 ? 'Catalog price: $' . website_escape(number_format($price, 2)) . ' / month' : 'Catalog price: contact for pricing' ?></p>
<p><?= $price > 0 ? 'Price: $' . website_escape(number_format($price, 2)) . ' per slot / monthly' : 'Price: contact for pricing' ?></p>
</article>
<article class="summary-card">
<h3>Slots and location</h3>
<p>Minimum slots: <?= website_escape((string)$minSlots) ?></p>
<p><?= $maxSlots > 0 ? 'Maximum slots: ' . website_escape((string)$maxSlots) : 'Maximum slots depend on game and available location capacity.' ?></p>
<p>Location availability is confirmed before payment or provisioning.</p>
</article>
<article class="summary-card">
<h3>Checkout boundary</h3>
<p>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.</p>
<p class="muted">Return to the game-server catalog or contact support if this package should be available.</p>
<h3>Estimated monthly total</h3>
<p><?= $monthlyEstimate > 0 ? '$' . website_escape(number_format($monthlyEstimate, 2)) . ' / monthly' : 'Contact for pricing' ?></p>
<p class="muted">Based on <?= website_escape((string)$selectedSlots) ?> selected slots.</p>
</article>
</div>
@ -83,7 +83,7 @@ $selectedSlots = max((int)$minSlots, (int)($_POST['slots'] ?? $minSlots));
<?php endforeach; ?>
</select>
<p class="muted">The website stores this selection in your cart and revalidates service, slots, location, and price during checkout.</p>
<p class="muted">You can review your selection in the cart before checkout.</p>
<div class="card-actions">
<button class="button button-primary" type="submit">Add to Cart</button>
<a class="button button-secondary" href="<?= website_escape(website_url('serverlist.php')) ?>">Back to Catalog</a>

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
?>
<section class="page-heading">
<div class="container">
<h1>Server Status</h1>
<p>Check current availability for configured Gameservers.World hosting locations.</p>
</div>
</section>
<section class="section">
<div class="container">
<div class="summary-card">
<div class="status-page-header">
<p class="muted"><strong>Checked:</strong> <?= website_escape((string)$checkedAt) ?></p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(website_url('server_status.php')) ?>">Refresh Status</a>
<a class="button button-secondary" href="<?= website_escape(website_url('index.php')) ?>">Back to Gameservers.World</a>
</div>
</div>
<?php if (empty($statusRows)): ?>
<div class="empty-state">
<h2>Status is unavailable</h2>
<p>We could not load hosting location status right now. Please contact support if you need help choosing a location.</p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(website_url('support.php')) ?>">Contact Support</a>
<a class="button button-secondary" href="<?= website_escape(website_url('serverlist.php')) ?>">Browse Game Servers</a>
</div>
</div>
<?php else: ?>
<div class="status-table-wrap">
<table class="status-table">
<thead>
<tr>
<th>Location</th>
<th>Address</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($statusRows as $row): ?>
<tr>
<td data-label="Location"><?= website_escape((string)$row['name']) ?></td>
<td data-label="Address"><?= website_escape((string)$row['address']) ?></td>
<td data-label="Status"><span class="status-badge <?= website_escape((string)$row['status_class']) ?>"><?= website_escape((string)$row['status']) ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</section>

View file

@ -34,7 +34,7 @@ $loginUnavailable = $loginUnavailable ?? false;
<h3>Modding and troubleshooting</h3>
<p>Use the documentation set for server guides, panel workflows, Workshop help, and technical troubleshooting across current games, older multiplayer titles, and community-maintained servers.</p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(website_url('docs.php')) ?>">Open Docs</a>
<a class="button button-primary" href="<?= website_escape(website_url('serverlist.php')) ?>">Browse Game Servers</a>
</div>
</article>
<article class="support-card">

View file

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
function website_status_socket_reachable(string $host, int $port, float $timeout = 1.5): ?bool
{
if ($host === '' || $port <= 0) {
return null;
}
$errno = 0;
$errstr = '';
$socket = @fsockopen($host, $port, $errno, $errstr, $timeout);
if ($socket === false) {
return false;
}
fclose($socket);
return true;
}
function website_status_locations(): array
{
$db = website_db();
if (!$db instanceof mysqli) {
return [];
}
$table = website_table_prefix() . 'remote_servers';
if (!website_table_exists($table)) {
return [];
}
$columns = website_table_columns($table);
$select = ['`remote_server_id`'];
foreach (['display_name', 'location_name', 'server_location', 'region', 'remote_server_name', 'hostname', 'agent_ip', 'display_public_ip', 'agent_port', 'enabled'] as $column) {
if (isset($columns[$column])) {
$safeColumn = str_replace('`', '``', $column);
$select[] = "`{$safeColumn}`";
}
}
$safeTable = str_replace('`', '``', $table);
$result = @$db->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(),
]
);

View file

@ -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: