fixed gameserver display

This commit is contained in:
Frank Harris 2026-06-17 19:40:20 -05:00
parent 325feb7f25
commit a28d3e1a4f
9 changed files with 482 additions and 36 deletions

View file

@ -189,6 +189,26 @@ secrets are masked after save.
More detail: `docs/modules/website_billing_rebuild.md`.
## Service catalog display
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
internally for service lookup and documentation routing, but should not be shown
as the public platform label.
`website_service_platform()` derives platform labels from service/config values:
- Linux: `linux`, `linux32`, `linux64`
- Windows: `win`, `win32`, `win64`, `windows`
- Cross-platform: both Linux and Windows markers
- Unknown: no recognizable platform marker
The order/configuration page keeps the card layout but displays the same derived
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.
## Documentation source
Customer documentation is read from the existing billing docs directory:

View file

@ -596,6 +596,18 @@ textarea {
font-weight: 600;
}
.status-badge-neutral {
background: rgba(84, 166, 255, 0.1);
border-color: rgba(84, 166, 255, 0.22);
color: #cbe1ff;
}
.status-badge-muted {
background: rgba(147, 168, 203, 0.08);
border-color: rgba(147, 168, 203, 0.18);
color: var(--muted);
}
.alert {
padding: 16px 18px;
border-radius: 6px;
@ -628,17 +640,31 @@ textarea {
font-weight: 700;
}
.website-form input {
.website-form input,
.website-form select,
.website-form textarea {
width: 100%;
min-height: 44px;
border: 1px solid var(--line-strong);
border-radius: 6px;
background: rgba(5, 17, 32, 0.78);
color: var(--text);
}
.website-form input,
.website-form select {
min-height: 44px;
padding: 0 12px;
}
.website-form input:focus {
.website-form textarea {
min-height: 96px;
padding: 10px 12px;
resize: vertical;
}
.website-form input:focus,
.website-form select:focus,
.website-form textarea:focus {
outline: 2px solid rgba(88, 171, 255, 0.45);
outline-offset: 2px;
}
@ -662,6 +688,190 @@ textarea {
color: var(--muted);
}
.catalog-list,
.staff-service-list {
display: grid;
gap: 8px;
}
.catalog-list-heading,
.catalog-row,
.staff-service-heading,
.staff-service-row summary {
display: grid;
align-items: center;
gap: 14px;
}
.catalog-list-heading,
.staff-service-heading {
padding: 0 16px 8px;
color: #b8cae5;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.catalog-list-heading,
.catalog-row {
grid-template-columns: minmax(190px, 1.15fr) 130px minmax(260px, 1.8fr) 130px minmax(210px, auto);
}
.catalog-row,
.staff-service-row {
background: linear-gradient(180deg, rgba(13, 24, 43, 0.92) 0%, rgba(10, 18, 32, 0.92) 100%);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.catalog-row {
padding: 14px 16px;
}
.catalog-server {
display: grid;
grid-template-columns: 56px minmax(0, 1fr);
align-items: center;
gap: 12px;
min-width: 0;
}
.catalog-thumb {
width: 56px;
height: 42px;
object-fit: cover;
border: 1px solid var(--line);
border-radius: 6px;
background: #08111f;
}
.catalog-server h3,
.catalog-description {
margin: 0;
}
.catalog-server h3 {
font-size: 1rem;
}
.catalog-description {
color: var(--muted);
}
.catalog-price {
color: var(--accent-strong);
font-weight: 800;
}
.catalog-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
.catalog-actions .button {
min-height: 38px;
padding: 0 12px;
}
.staff-services-form {
padding: 0;
border: 0;
background: transparent;
}
.staff-service-heading,
.staff-service-row summary {
grid-template-columns: 70px 100px minmax(170px, 1.2fr) 100px 100px 90px 80px 90px 60px;
}
.staff-service-row summary {
min-height: 58px;
padding: 12px 16px;
cursor: pointer;
list-style: none;
}
.staff-service-row summary::-webkit-details-marker {
display: none;
}
.staff-service-row summary::after {
content: "Edit";
justify-self: end;
color: var(--accent-strong);
font-weight: 800;
}
.staff-service-row[open] summary {
border-bottom: 1px solid var(--line);
}
.staff-service-row[open] summary::after {
content: "Close";
}
.staff-service-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 800;
}
.staff-service-editor {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
padding: 16px;
}
.staff-service-editor label,
.staff-service-editor fieldset {
display: grid;
gap: 8px;
}
.staff-service-editor fieldset {
margin: 0;
padding: 12px;
border: 1px solid var(--line);
border-radius: 6px;
}
.staff-service-editor legend,
.staff-service-editor label > span:first-child {
color: var(--text);
font-weight: 800;
}
.staff-editor-wide {
grid-column: 1 / -1;
}
.checkbox-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px 12px;
}
.checkbox-grid label {
display: flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-weight: 600;
}
.checkbox-grid input[type="checkbox"],
.staff-service-editor input[type="checkbox"] {
width: auto;
min-height: 0;
}
.panel-preview {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(260px, 0.85fr);
@ -894,6 +1104,49 @@ textarea {
margin-top: 4px;
}
.catalog-list-heading,
.staff-service-heading {
display: none;
}
.catalog-row,
.staff-service-row summary {
grid-template-columns: 1fr;
align-items: stretch;
gap: 10px;
}
.catalog-row > *[data-label]::before,
.staff-service-row summary > span[data-label]::before {
content: attr(data-label) ": ";
color: #b8cae5;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.catalog-actions {
justify-content: stretch;
}
.catalog-actions .button {
flex: 1 1 160px;
}
.staff-service-row summary::after {
justify-self: start;
}
.staff-service-name {
white-space: normal;
}
.staff-service-editor,
.checkbox-grid {
grid-template-columns: 1fr;
}
.hero,
.page-heading {
padding-top: 42px;

View file

@ -721,6 +721,37 @@ function website_service_name(array $service): string
return $name === '' ? 'Game Server' : $name;
}
function website_service_platform(array $service): string
{
$parts = [];
foreach (['cfg_game_key', 'cfg_file', 'home_cfg_file', 'config_key', 'service_name'] as $key) {
$value = trim((string)($service[$key] ?? ''));
if ($value !== '') {
$parts[] = $value;
}
}
$source = strtolower(implode(' ', $parts));
if ($source === '') {
return 'Unknown';
}
$hasLinux = preg_match('/(^|[_\W])linux(32|64)?($|[_\W])|linux32|linux64/', $source) === 1;
$hasWindows = preg_match('/(^|[_\W])(win|windows)(32|64)?($|[_\W])|win32|win64/', $source) === 1;
if ($hasLinux && $hasWindows) {
return 'Cross-platform';
}
if ($hasLinux) {
return 'Linux';
}
if ($hasWindows) {
return 'Windows';
}
return 'Unknown';
}
function website_service_min_slots(array $service): int
{
foreach (['slot_min_qty', 'min_slots', 'minimum_slots', 'slots_min'] as $column) {

View file

@ -43,27 +43,37 @@ $fixedCapNote = trim((string)($pricing['fixed_cap_note'] ?? ''));
<section class="section">
<div class="container">
<?php if ($hasBilling && !empty($services)): ?>
<div class="service-grid">
<div class="catalog-list" role="list">
<div class="catalog-list-heading" aria-hidden="true">
<span>Server</span>
<span>Platform</span>
<span>Description</span>
<span>Price</span>
<span>Actions</span>
</div>
<?php foreach ($services as $service): ?>
<?php
$serviceName = trim((string)($service['cfg_game_name'] ?? $service['service_name'] ?? 'Game Server'));
$serviceName = website_service_name($service);
$platformLabel = website_service_platform($service);
$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;
?>
<article class="service-card">
<img src="<?= website_escape(website_service_image_url((string)($service['img_url'] ?? ''))) ?>" alt="<?= website_escape($serviceName) ?>">
<header>
<article class="catalog-row" role="listitem">
<div class="catalog-server">
<img class="catalog-thumb" src="<?= website_escape(website_service_image_url((string)($service['img_url'] ?? ''))) ?>" alt="" loading="lazy">
<h3><?= website_escape($serviceName) ?></h3>
<div class="service-meta"><?= website_escape((string)($service['cfg_game_key'] ?? '')) ?></div>
</header>
<p><?= website_escape($description !== '' ? $description : 'Virtual private hosting with full configuration access, mod support, GSP panel management, and optional custom engineering help.') ?></p>
<div class="section-divider"></div>
<div class="service-price"><?= $price > 0 ? '$' . number_format($price, 2) . ' / month' : 'Contact for pricing' ?></div>
<div class="card-actions">
</div>
<div class="catalog-platform" data-label="Platform">
<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-actions">
<a class="button button-primary" href="<?= website_escape($orderHref) ?>">Order Now</a>
<?php if ($docSlug !== '' && website_doc_path($docSlug) !== null): ?>
<?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>

View file

@ -78,7 +78,8 @@ $locationSummary = ['USA East', 'USA Central', 'USA West', 'Europe'];
<div class="service-grid service-grid-compact">
<?php foreach ($services as $service): ?>
<?php
$serviceName = trim((string)($service['cfg_game_name'] ?? $service['service_name'] ?? 'Game Server'));
$serviceName = website_service_name($service);
$platformLabel = website_service_platform($service);
$price = (float)($service['price_monthly'] ?? 0);
$orderUrl = website_order_url((string)$service['service_id']);
?>
@ -86,6 +87,7 @@ $locationSummary = ['USA East', 'USA Central', 'USA West', 'Europe'];
<img src="<?= website_escape(website_service_image_url((string)($service['img_url'] ?? ''))) ?>" alt="<?= website_escape($serviceName) ?>">
<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>
</header>
<div class="card-actions">

View file

@ -22,7 +22,8 @@ if ($service === null):
return;
endif;
$serviceName = trim((string)($service['cfg_game_name'] ?? $service['service_name'] ?? 'Game Server'));
$serviceName = website_service_name($service);
$platformLabel = website_service_platform($service);
$description = trim((string)($service['description'] ?? ''));
$price = (float)($service['price_monthly'] ?? 0);
$selectedSlots = max((int)$minSlots, (int)($_POST['slots'] ?? $minSlots));
@ -43,7 +44,7 @@ $selectedSlots = max((int)$minSlots, (int)($_POST['slots'] ?? $minSlots));
<article class="summary-card">
<h3>Plan</h3>
<p><strong><?= website_escape($serviceName) ?></strong></p>
<p>Service ID: <?= website_escape((string)$service['service_id']) ?></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>
</article>
<article class="summary-card">

View file

@ -47,11 +47,13 @@ $platformSummary = trim((string)($platform['summary'] ?? ''));
<div class="service-grid">
<?php foreach ($services as $service): ?>
<?php
$serviceName = trim((string)($service['cfg_game_name'] ?? $service['service_name'] ?? 'Game Server'));
$serviceName = website_service_name($service);
$platformLabel = website_service_platform($service);
$price = (float)($service['price_monthly'] ?? 0);
?>
<article class="service-card">
<h3><?= website_escape($serviceName) ?></h3>
<div class="service-meta">Platform: <?= website_escape($platformLabel) ?></div>
<p><?= website_escape(trim((string)($service['description'] ?? '')) ?: 'Virtual private hosting with full configuration access, GSP access, daily backups, and practical support.') ?></p>
<div class="section-divider"></div>
<div class="service-price"><?= $price > 0 ? '$' . number_format($price, 2) . ' / month' : 'Contact for pricing' ?></div>

View file

@ -1,18 +1,123 @@
<?php declare(strict_types=1); ?>
<section class="page-heading"><div class="container"><h1>Manage Game Services</h1><p>Update public catalog status, prices, slot limits, images, and location availability.</p></div></section>
<section class="section"><div class="container">
<?php if ($message): ?><div class="alert info"><?= website_escape($message) ?></div><?php endif; ?><?php if ($error): ?><div class="alert warning"><?= website_escape($error) ?></div><?php endif; ?>
<?php if (empty($services)): ?><div class="empty-state"><h2>No services found</h2><p>Run migrations and import or create billing services before editing catalog data.</p></div><?php else: ?>
<form method="post" class="website-form"><?= website_csrf_field() ?><div class="summary-grid">
<?php foreach ($services as $service): $sid=(int)$service['service_id']; $selected=array_flip(preg_split('/\s+/', trim((string)$service['remote_server_id'])) ?: []); ?>
<article class="summary-card"><h3>#<?= $sid ?> <?= website_escape(website_service_name($service)) ?></h3>
<label><input type="checkbox" name="service[<?= $sid ?>][enabled]" value="1" <?= ((int)($service['enabled']??1)===1?'checked':'') ?>> Enabled</label>
<label>Name</label><input name="service[<?= $sid ?>][service_name]" value="<?= website_escape((string)($service['service_name']??'')) ?>">
<label>Description</label><textarea name="service[<?= $sid ?>][description]"><?= website_escape((string)($service['description']??'')) ?></textarea>
<label>Min Slots</label><input type="number" name="service[<?= $sid ?>][slot_min_qty]" value="<?= website_escape((string)website_service_min_slots($service)) ?>">
<label>Max Slots</label><input type="number" name="service[<?= $sid ?>][slot_max_qty]" value="<?= website_escape((string)website_service_max_slots($service)) ?>">
<label>Monthly Price</label><input type="number" step="0.01" name="service[<?= $sid ?>][price_monthly]" value="<?= website_escape((string)($service['price_monthly']??0)) ?>">
<label>Image URL</label><input name="service[<?= $sid ?>][img_url]" value="<?= website_escape((string)($service['img_url']??'')) ?>">
<label>Locations</label><?php foreach ($remoteServers as $rs): $rid=(string)$rs['remote_server_id']; ?><label><input type="checkbox" name="service[<?= $sid ?>][locations][]" value="<?= website_escape($rid) ?>" <?= isset($selected[$rid])?'checked':'' ?>> <?= website_escape((string)$rs['remote_server_name']) ?></label><?php endforeach; ?>
</article><?php endforeach; ?></div><button class="button button-primary" type="submit">Save Services</button></form><?php endif; ?>
</div></section>
<section class="page-heading">
<div class="container">
<h1>Manage Game Services</h1>
<p>Update public catalog status, prices, slot limits, images, and location availability.</p>
</div>
</section>
<section class="section">
<div class="container">
<?php if ($message): ?><div class="alert info"><?= website_escape($message) ?></div><?php endif; ?>
<?php if ($error): ?><div class="alert warning"><?= website_escape($error) ?></div><?php endif; ?>
<?php if (empty($services)): ?>
<div class="empty-state">
<h2>No services found</h2>
<p>Run migrations and import or create billing services before editing catalog data.</p>
</div>
<?php else: ?>
<form method="post" class="website-form staff-services-form">
<?= website_csrf_field() ?>
<div class="staff-service-list">
<div class="staff-service-heading" aria-hidden="true">
<span>ID</span>
<span>Status</span>
<span>Service</span>
<span>Platform</span>
<span>Price</span>
<span>Slots</span>
<span>Image</span>
<span>Locations</span>
<span>Action</span>
</div>
<?php foreach ($services as $service): ?>
<?php
$sid = (int)$service['service_id'];
$enabled = (int)($service['enabled'] ?? 1) === 1;
$selectedIds = [];
foreach (preg_split('/\s+/', trim((string)($service['remote_server_id'] ?? ''))) ?: [] as $remoteServerId) {
$remoteServerId = trim((string)$remoteServerId);
if ($remoteServerId !== '') {
$selectedIds[$remoteServerId] = true;
}
}
$selected = $selectedIds;
$selectedCount = count($selected);
$serviceName = website_service_name($service);
$platformLabel = website_service_platform($service);
$minSlots = website_service_min_slots($service);
$maxSlots = website_service_max_slots($service);
$price = (float)($service['price_monthly'] ?? 0);
$imageValue = trim((string)($service['img_url'] ?? ''));
?>
<details class="staff-service-row">
<summary>
<span data-label="ID">#<?= website_escape((string)$sid) ?></span>
<span data-label="Status" class="status-badge <?= $enabled ? '' : 'status-badge-muted' ?>"><?= $enabled ? 'Enabled' : 'Disabled' ?></span>
<span data-label="Service" class="staff-service-name"><?= website_escape($serviceName) ?></span>
<span data-label="Platform"><?= website_escape($platformLabel) ?></span>
<span data-label="Price"><?= $price > 0 ? '$' . website_escape(number_format($price, 2)) : 'Contact' ?></span>
<span data-label="Slots"><?= website_escape((string)$minSlots) ?><?= $maxSlots > 0 ? '-' . website_escape((string)$maxSlots) : '+' ?></span>
<span data-label="Image"><?= $imageValue !== '' ? 'Set' : 'Default' ?></span>
<span data-label="Locations"><?= website_escape((string)$selectedCount) ?></span>
</summary>
<div class="staff-service-editor">
<label>
<span>Status</span>
<span><input type="checkbox" name="service[<?= $sid ?>][enabled]" value="1" <?= $enabled ? 'checked' : '' ?>> Enabled for public catalog</span>
</label>
<label>
<span>Display name</span>
<input name="service[<?= $sid ?>][service_name]" value="<?= website_escape((string)($service['service_name'] ?? '')) ?>">
</label>
<label class="staff-editor-wide">
<span>Description</span>
<textarea name="service[<?= $sid ?>][description]" rows="3"><?= website_escape((string)($service['description'] ?? '')) ?></textarea>
</label>
<label>
<span>Min slots</span>
<input type="number" name="service[<?= $sid ?>][slot_min_qty]" value="<?= website_escape((string)$minSlots) ?>">
</label>
<label>
<span>Max slots</span>
<input type="number" name="service[<?= $sid ?>][slot_max_qty]" value="<?= website_escape((string)$maxSlots) ?>">
</label>
<label>
<span>Monthly price</span>
<input type="number" step="0.01" name="service[<?= $sid ?>][price_monthly]" value="<?= website_escape((string)$price) ?>">
</label>
<label>
<span>Image URL</span>
<input name="service[<?= $sid ?>][img_url]" value="<?= website_escape($imageValue) ?>">
</label>
<fieldset class="staff-editor-wide">
<legend>Locations</legend>
<div class="checkbox-grid">
<?php foreach ($remoteServers as $rs): ?>
<?php $rid = (string)$rs['remote_server_id']; ?>
<label>
<input type="checkbox" name="service[<?= $sid ?>][locations][]" value="<?= website_escape($rid) ?>" <?= isset($selected[$rid]) ? 'checked' : '' ?>>
<span><?= website_escape((string)$rs['remote_server_name']) ?></span>
</label>
<?php endforeach; ?>
</div>
</fieldset>
</div>
</details>
<?php endforeach; ?>
</div>
<div class="card-actions">
<button class="button button-primary" type="submit">Save Services</button>
</div>
</form>
<?php endif; ?>
</div>
</section>

View file

@ -69,6 +69,28 @@ Checkout creates due invoices and pending-payment orders after login. PayPal ord
Paid orders appear in the website provisioning queue. The queue is the handoff point for Panel-side server creation; provisioning must remain idempotent and must not run before payment or approval.
## Service Catalog Display
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
action. Raw XML/config keys such as `*_linux64` or `*_win32` are internal
identifiers and should not be prominent public labels.
Platform labels are produced by `website_service_platform()` from service/config
data such as `cfg_game_key`, `cfg_file`, `home_cfg_file`, or related service
fields:
- Linux: keys/files containing `linux`, `linux32`, or `linux64`
- Windows: keys/files containing `win`, `win32`, `win64`, or `windows`
- Cross-platform: both platform families detected
- Unknown: no platform marker detected
Order/configuration pages may retain card-style layout, but should also show the
derived platform label instead of the raw XML/config key. Website staff service
management uses a compact expandable row list with service ID, status, display
name, platform, price, slot range, image status, and location count visible.
## Navigation
Website footer account links are state-aware: