Updated admin page

This commit is contained in:
Frank Harris 2026-01-17 09:29:24 -06:00
parent fcc1b18e4c
commit cbd7995a31
6 changed files with 284 additions and 10 deletions

View file

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../lib/SteamWorkshopService.php';
class AdminWorkshopController
{
private SteamWorkshopService $service;
private array $lang;
public function __construct(OGPDatabase $db)
{
$this->service = new SteamWorkshopService($db);
$this->lang = $this->loadLang();
}
public function handle(): void
{
global $db;
$userId = (int)($_SESSION['user_id'] ?? 0);
if (!$db->isAdmin($userId)) {
print_failure($this->lang['error_admin_only'] ?? 'Admin access required.');
return;
}
echo '<link rel="stylesheet" type="text/css" href="modules/steam_workshop/steam_workshop.css" />';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->processSave();
}
$gameKeys = $this->service->listAvailableGameKeys();
$mappings = $this->service->getAdapterMappings();
$adapters = $this->service->loadAdapters();
$adapterOptions = $this->service->getAdapterOptions();
$this->render('admin/index', [
'lang' => $this->lang,
'gameKeys' => $gameKeys,
'mappings' => $mappings,
'adapters' => $adapters,
'adapterOptions' => $adapterOptions,
]);
}
private function processSave(): void
{
$payload = $_POST['mapping'] ?? [];
if (!is_array($payload)) {
$payload = [];
}
$this->service->saveAdapterMappings($payload);
print_success($this->lang['message_mappings_saved'] ?? 'Adapter mappings saved.');
}
private function render(string $view, array $data = []): void
{
extract($data);
$lang = $this->lang;
require __DIR__ . '/../views/' . $view . '.php';
}
private function loadLang(): array
{
$langFile = __DIR__ . '/../lang/en_US.php';
if (is_file($langFile)) {
$strings = require $langFile;
if (is_array($strings)) {
return $strings;
}
}
return [];
}
}

View file

@ -54,10 +54,11 @@ class SteamWorkshopController
}
$config = $this->service->buildConfigFromRequest($_POST);
$adapterLocked = $this->applyGameAdapterOverride($home, $config);
$this->service->saveConfig($homeId, $config);
print_success($this->lang['message_config_saved'] ?? 'Workshop configuration saved.');
$this->renderEdit($home, $config, $isAdmin);
$this->renderEdit($home, $config, $isAdmin, $adapterLocked);
}
private function handleEdit(int $userId, bool $isAdmin): void
@ -77,7 +78,8 @@ class SteamWorkshopController
}
$config = $this->service->loadConfig($homeId);
$this->renderEdit($home, $config, $isAdmin);
$adapterLocked = $this->applyGameAdapterOverride($home, $config);
$this->renderEdit($home, $config, $isAdmin, $adapterLocked);
}
private function renderIndex(int $userId, bool $isAdmin): void
@ -86,6 +88,7 @@ class SteamWorkshopController
$homes = $this->service->listHomesForUser($userId, $isAdmin);
foreach ($homes as $home) {
$config = $this->service->loadConfig((int)$home['home_id']);
$this->applyGameAdapterOverride($home, $config);
$adapter = $this->service->getAdapterByKey($config['adapter_key']);
$records[] = [
'home' => $home,
@ -102,7 +105,7 @@ class SteamWorkshopController
]);
}
private function renderEdit(array $home, array $config, bool $isAdmin): void
private function renderEdit(array $home, array $config, bool $isAdmin, bool $adapterLocked): void
{
$this->render('edit', [
'lang' => $this->lang,
@ -110,9 +113,22 @@ class SteamWorkshopController
'config' => $config,
'isAdmin' => $isAdmin,
'adapterOptions' => $this->service->getAdapterOptions(),
'adapterLocked' => $adapterLocked,
]);
}
private function applyGameAdapterOverride(array $home, array &$config): bool
{
$gameKey = isset($home['game_key']) ? (string)$home['game_key'] : '';
$mapped = $this->service->getAdapterKeyForGame($gameKey);
if ($mapped !== null && $mapped !== '') {
$config['adapter_key'] = $mapped;
return true;
}
return false;
}
private function render(string $view, array $data = []): void
{
extract($data);

View file

@ -16,6 +16,7 @@ return [
'label_post_install_script' => 'Post-install script (absolute path)',
'label_mod_import' => 'Workshop IDs list (one "id,@ModName" per line)',
'hint_mod_import' => 'Paste from Modlist.txt or import from a collection. IDs are sanitized automatically.',
'adapter_locked_note' => 'This adapter is enforced for the current game type by your administrator.',
'status_enabled' => 'Enabled',
'status_disabled' => 'Disabled',
'status_hot_reload' => 'Hot reload ready',

View file

@ -9,16 +9,20 @@ class SteamWorkshopService
private OGPDatabase $db;
private string $configDir;
private string $adapterDir;
private string $adapterMapFile;
public function __construct(OGPDatabase $db)
{
$this->db = $db;
$this->configDir = __DIR__ . '/../data/configs';
$this->adapterDir = __DIR__ . '/GameAdapters';
$this->adapterMapFile = __DIR__ . '/../data/game_adapter_map.json';
if (!is_dir($this->configDir)) {
mkdir($this->configDir, 0775, true);
}
$this->ensureDataFiles();
}
/**
@ -308,6 +312,89 @@ class SteamWorkshopService
return [];
}
/**
* Return adapter key chosen for the given game key, or null if unmapped.
*/
public function getAdapterKeyForGame(string $gameKey): ?string
{
$gameKey = trim($gameKey);
if ($gameKey === '') {
return null;
}
$map = $this->getAdapterMappings();
return $map[$gameKey] ?? null;
}
/**
* Persist adapter mappings (game_key => adapter_key).
*/
public function saveAdapterMappings(array $mappings): void
{
$sanitized = [];
$options = $this->getAdapterOptions();
foreach ($mappings as $gameKey => $adapterKey) {
$gameKey = trim((string)$gameKey);
$adapterKey = $this->sanitizeAdapterKey((string)$adapterKey);
if ($gameKey === '' || !isset($options[$adapterKey])) {
continue;
}
$sanitized[$gameKey] = $adapterKey;
}
file_put_contents($this->adapterMapFile, json_encode($sanitized, JSON_PRETTY_PRINT));
}
/**
* @return array<string,string>
*/
public function getAdapterMappings(): array
{
if (!is_file($this->adapterMapFile)) {
return [];
}
$raw = file_get_contents($this->adapterMapFile);
$decoded = json_decode((string)$raw, true);
if (!is_array($decoded)) {
return [];
}
$result = [];
foreach ($decoded as $gameKey => $adapterKey) {
if (!is_string($gameKey) || !is_string($adapterKey)) {
continue;
}
$result[$gameKey] = $adapterKey;
}
return $result;
}
/**
* Discover available game keys from server config XMLs.
*
* @return array<int,string>
*/
public function listAvailableGameKeys(): array
{
$keys = [];
$configDir = defined('SERVER_CONFIG_LOCATION') ? SERVER_CONFIG_LOCATION : __DIR__ . '/../../config_games/server_configs';
foreach (glob($configDir . '/*.xml') as $file) {
$xml = @simplexml_load_file($file);
if ($xml === false) {
continue;
}
if (isset($xml->game_key)) {
$keys[] = trim((string)$xml->game_key);
}
}
$keys = array_filter(array_unique($keys));
sort($keys);
return array_values($keys);
}
private function sanitizeInterval(?int $minutes): int
{
if ($minutes === null || $minutes <= 0) {
@ -387,4 +474,16 @@ class SteamWorkshopService
'last_saved_at' => null,
];
}
private function ensureDataFiles(): void
{
$dir = dirname($this->adapterMapFile);
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
if (!is_file($this->adapterMapFile)) {
file_put_contents($this->adapterMapFile, json_encode([]));
}
}
}

View file

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/** @var array $lang */
/** @var array $gameKeys */
/** @var array $mappings */
/** @var array $adapterOptions */
/** @var array $adapters */
?>
<div class="sw-admin">
<h3><?php echo htmlspecialchars($lang['admin_heading_game_mapping'] ?? 'Game type adapter mapping'); ?></h3>
<p><?php echo htmlspecialchars($lang['admin_subheading_game_mapping'] ?? 'Select which adapter will manage Steam Workshop installs for each supported game.'); ?></p>
<form method="post" class="sw-form">
<table class="table sw-mods__table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['admin_col_game_key'] ?? 'Game Key'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_adapter'] ?? 'Adapter'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($gameKeys)): ?>
<tr>
<td colspan="2"><?php echo htmlspecialchars($lang['admin_no_game_keys'] ?? 'No game definitions were found in modules/config_games/server_configs.'); ?></td>
</tr>
<?php else: ?>
<?php foreach ($gameKeys as $gameKey): ?>
<tr>
<td><?php echo htmlspecialchars($gameKey); ?></td>
<td>
<select name="mapping[<?php echo htmlspecialchars($gameKey); ?>]">
<option value="">--</option>
<?php foreach ($adapterOptions as $key => $label): ?>
<option value="<?php echo htmlspecialchars($key); ?>" <?php echo (isset($mappings[$gameKey]) && $mappings[$gameKey] === $key) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($label); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<div class="sw-form__actions">
<button class="btn primary" type="submit"><?php echo htmlspecialchars($lang['button_save']); ?></button>
</div>
</form>
<h3><?php echo htmlspecialchars($lang['admin_heading_adapters'] ?? 'Available adapters'); ?></h3>
<table class="table sw-mods__table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['admin_col_key'] ?? 'Key'); ?></th>
<th><?php echo htmlspecialchars($lang['summary_adapter']); ?></th>
<th>Steam App ID</th>
<th><?php echo htmlspecialchars($lang['admin_col_mods_dir'] ?? 'Mods Dir'); ?></th>
<th><?php echo htmlspecialchars($lang['summary_hot_reload']); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_notes'] ?? 'Notes'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($adapters as $adapter): ?>
<tr>
<td><?php echo htmlspecialchars($adapter['key']); ?></td>
<td><?php echo htmlspecialchars($adapter['name']); ?></td>
<td><?php echo htmlspecialchars($adapter['steam_app_id']); ?></td>
<td><?php echo htmlspecialchars($adapter['mods_dir']); ?></td>
<td><?php echo !empty($adapter['supports_hot_reload']) ? htmlspecialchars($lang['status_hot_reload']) : htmlspecialchars($lang['status_restart_required']); ?></td>
<td><?php echo htmlspecialchars($adapter['notes']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>

View file

@ -3,6 +3,7 @@ declare(strict_types=1);
/** @var array $formConfig */
/** @var array $adapterOptions */
/** @var array $lang */
/** @var bool $adapterLocked */
$enabled = !empty($formConfig['workshop_enabled']);
$interval = (int)$formConfig['update_interval_minutes'];
$stagingDir = htmlspecialchars($formConfig['staging_dir']);
@ -10,6 +11,7 @@ $postInstall = htmlspecialchars($formConfig['post_install_script']);
$rawDefinition = htmlspecialchars($formConfig['raw_definition']);
$installStrategy = $formConfig['install_strategy'];
$onUpdateAction = $formConfig['on_update_action'];
$currentAdapterName = $adapterOptions[$formConfig['adapter_key']] ?? strtoupper($formConfig['adapter_key']);
?>
<div class="sw-form__grid">
<label class="sw-toggle">
@ -19,13 +21,18 @@ $onUpdateAction = $formConfig['on_update_action'];
<label>
<span><?php echo htmlspecialchars($lang['label_adapter']); ?></span>
<select name="workshop[adapter_key]">
<?php foreach ($adapterOptions as $key => $label): ?>
<option value="<?php echo htmlspecialchars($key); ?>" <?php echo $formConfig['adapter_key'] === $key ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($label); ?>
</option>
<?php endforeach; ?>
</select>
<?php if ($adapterLocked): ?>
<input type="text" value="<?php echo htmlspecialchars($currentAdapterName); ?>" disabled />
<small><?php echo htmlspecialchars($lang['adapter_locked_note'] ?? 'This adapter is managed by the administrator.'); ?></small>
<?php else: ?>
<select name="workshop[adapter_key]">
<?php foreach ($adapterOptions as $key => $label): ?>
<option value="<?php echo htmlspecialchars($key); ?>" <?php echo $formConfig['adapter_key'] === $key ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($label); ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
</label>
<label>