fixed gameserver display
This commit is contained in:
parent
325feb7f25
commit
a28d3e1a4f
9 changed files with 482 additions and 36 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue