small fixes

This commit is contained in:
Frank Harris 2026-06-17 09:15:42 -05:00
parent bd3875743e
commit 28533be24d
9 changed files with 310 additions and 524 deletions

View file

@ -222,14 +222,15 @@ function gsp_update_settings()
{
global $settings;
$repo_root = !empty($settings['gsp_update_repo_root']) ? (string)$settings['gsp_update_repo_root'] : GSP_DEFAULT_REPO_ROOT;
$panel_path = !empty($settings['gsp_update_panel_path']) ? (string)$settings['gsp_update_panel_path'] : GSP_DEFAULT_PANEL_PATH;
$repo_root = rtrim($repo_root, '/');
$panel_path = $repo_root . '/Panel';
return [
'repo_url' => !empty($settings['gsp_update_repo_url']) ? (string)$settings['gsp_update_repo_url'] : GSP_DEFAULT_REPO_URL,
'branch' => !empty($settings['gsp_update_branch']) ? (string)$settings['gsp_update_branch'] : GSP_DEFAULT_BRANCH,
'repo_root' => rtrim($repo_root, '/'),
'repo_root' => $repo_root,
'panel_path' => rtrim($panel_path, '/'),
'panel_source_path' => !empty($settings['gsp_update_panel_source_path']) ? trim((string)$settings['gsp_update_panel_source_path'], '/') : GSP_DEFAULT_PANEL_SOURCE,
'git_path' => !empty($settings['gsp_update_git_path']) ? trim((string)$settings['gsp_update_git_path']) : 'git',
'panel_source_path' => GSP_DEFAULT_PANEL_SOURCE,
'git_path' => 'git',
'backup_path' => !empty($settings['gsp_update_backup_path']) ? rtrim((string)$settings['gsp_update_backup_path'], '/') : GSP_BACKUP_BASE,
'backup_retention' => !empty($settings['gsp_update_backup_retention']) ? (string)$settings['gsp_update_backup_retention'] : (string)GSP_DEFAULT_BACKUP_RETENTION,
'panel_post_update_command' => !empty($settings['gsp_update_panel_post_update_command']) ? (string)$settings['gsp_update_panel_post_update_command'] : '',
@ -268,15 +269,6 @@ $errors[] = ucfirst(str_replace('_', ' ', $key)) . ' must be a safe absolute pat
if (isset($cfg['backup_retention']) && (!preg_match('/^\d+$/', (string)$cfg['backup_retention']) || (int)$cfg['backup_retention'] < 1 || (int)$cfg['backup_retention'] > 200)) {
$errors[] = 'Backup retention must be a whole number between 1 and 200.';
}
foreach (['panel_source_path'] as $key) {
$value = isset($cfg[$key]) ? trim((string)$cfg[$key], '/') : '';
if ($value === '' || strpos($value, "\0") !== false || strpos($value, '..') !== false || !preg_match('/^[A-Za-z0-9._\/-]+$/', $value)) {
$errors[] = ucfirst(str_replace('_', ' ', $key)) . ' is invalid.';
}
}
if (isset($cfg['git_path']) && trim((string)$cfg['git_path']) !== '' && (strpos((string)$cfg['git_path'], "\0") !== false || strpos((string)$cfg['git_path'], "\n") !== false || strpos((string)$cfg['git_path'], "\r") !== false)) {
$errors[] = 'Git executable path is invalid.';
}
foreach (['panel_post_update_command'] as $key) {
if (isset($cfg[$key]) && (strpos((string)$cfg[$key], "\0") !== false || strpos((string)$cfg[$key], "\n") !== false || strpos((string)$cfg[$key], "\r") !== false)) {
$errors[] = ucfirst(str_replace('_', ' ', $key)) . ' must be a single-line admin command.';
@ -1588,9 +1580,9 @@ $new_cfg = [
'repo_url' => isset($_POST['gsp_update_repo_url']) ? trim((string)$_POST['gsp_update_repo_url']) : '',
'branch' => isset($_POST['gsp_update_branch']) ? trim((string)$_POST['gsp_update_branch']) : '',
'repo_root' => isset($_POST['gsp_update_repo_root']) ? rtrim(trim((string)$_POST['gsp_update_repo_root']), '/') : '',
'panel_path' => isset($_POST['gsp_update_panel_path']) ? rtrim(trim((string)$_POST['gsp_update_panel_path']), '/') : '',
'panel_source_path' => isset($_POST['gsp_update_panel_source_path']) ? trim((string)$_POST['gsp_update_panel_source_path'], '/') : '',
'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : '',
'panel_path' => isset($_POST['gsp_update_repo_root']) ? rtrim(trim((string)$_POST['gsp_update_repo_root']), '/') . '/Panel' : GSP_DEFAULT_PANEL_PATH,
'panel_source_path' => GSP_DEFAULT_PANEL_SOURCE,
'git_path' => 'git',
'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : '',
'backup_retention' => isset($_POST['gsp_update_backup_retention']) ? trim((string)$_POST['gsp_update_backup_retention']) : '',
'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : '',
@ -1606,7 +1598,7 @@ $db->setSettings([
'gsp_update_repo_root' => $new_cfg['repo_root'],
'gsp_update_panel_path' => $new_cfg['panel_path'],
'gsp_update_panel_source_path' => $new_cfg['panel_source_path'],
'gsp_update_git_path' => $new_cfg['git_path'],
'gsp_update_git_path' => 'git',
'gsp_update_backup_path' => $new_cfg['backup_path'],
'gsp_update_backup_retention' => $new_cfg['backup_retention'],
'gsp_update_panel_post_update_command' => $new_cfg['panel_post_update_command'],
@ -1617,7 +1609,7 @@ $settings['gsp_update_branch'] = $new_cfg['branch'];
$settings['gsp_update_repo_root'] = $new_cfg['repo_root'];
$settings['gsp_update_panel_path'] = $new_cfg['panel_path'];
$settings['gsp_update_panel_source_path'] = $new_cfg['panel_source_path'];
$settings['gsp_update_git_path'] = $new_cfg['git_path'];
$settings['gsp_update_git_path'] = 'git';
$settings['gsp_update_backup_path'] = $new_cfg['backup_path'];
$settings['gsp_update_backup_retention'] = $new_cfg['backup_retention'];
$settings['gsp_update_panel_post_update_command'] = $new_cfg['panel_post_update_command'];
@ -1630,9 +1622,9 @@ $update_cfg = [
'repo_url' => isset($_POST['gsp_update_repo_url']) ? trim((string)$_POST['gsp_update_repo_url']) : $update_cfg['repo_url'],
'branch' => isset($_POST['gsp_update_branch']) ? trim((string)$_POST['gsp_update_branch']) : $update_cfg['branch'],
'repo_root' => isset($_POST['gsp_update_repo_root']) ? rtrim(trim((string)$_POST['gsp_update_repo_root']), '/') : $update_cfg['repo_root'],
'panel_path' => isset($_POST['gsp_update_panel_path']) ? rtrim(trim((string)$_POST['gsp_update_panel_path']), '/') : $update_cfg['panel_path'],
'panel_source_path' => isset($_POST['gsp_update_panel_source_path']) ? trim((string)$_POST['gsp_update_panel_source_path'], '/') : $update_cfg['panel_source_path'],
'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : $update_cfg['git_path'],
'panel_path' => (isset($_POST['gsp_update_repo_root']) ? rtrim(trim((string)$_POST['gsp_update_repo_root']), '/') : $update_cfg['repo_root']) . '/Panel',
'panel_source_path' => GSP_DEFAULT_PANEL_SOURCE,
'git_path' => 'git',
'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : $update_cfg['backup_path'],
'backup_retention' => isset($_POST['gsp_update_backup_retention']) ? trim((string)$_POST['gsp_update_backup_retention']) : $update_cfg['backup_retention'],
'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : $update_cfg['panel_post_update_command'],
@ -1644,7 +1636,7 @@ $db->setSettings([
'gsp_update_repo_root' => $update_cfg['repo_root'],
'gsp_update_panel_path' => $update_cfg['panel_path'],
'gsp_update_panel_source_path' => $update_cfg['panel_source_path'],
'gsp_update_git_path' => $update_cfg['git_path'],
'gsp_update_git_path' => 'git',
'gsp_update_backup_path' => $update_cfg['backup_path'],
'gsp_update_backup_retention' => $update_cfg['backup_retention'],
'gsp_update_panel_post_update_command' => $update_cfg['panel_post_update_command'],
@ -1711,19 +1703,40 @@ $preflight_result = gsp_preflight_check($update_cfg);
$ssl_issue_count = !empty($apache_scan_result['ssl_issues']) ? count($apache_scan_result['ssl_issues']) : 0;
echo "<h2>Panel Updates</h2>\n";
echo "<table class='administration-table'><tr><td>\n";
echo "<style>
.gsp-update-shell{display:grid;gap:18px;margin-top:18px}
.gsp-update-card{padding:18px 20px;background:rgba(11,20,36,.92);border:1px solid rgba(77,160,255,.18);border-radius:8px;box-shadow:0 14px 32px rgba(0,0,0,.22)}
.gsp-update-card h3{margin-top:0}
.gsp-update-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 18px}
.gsp-update-grid dt{font-weight:700;color:#e6f0ff}
.gsp-update-grid dd{margin:0;color:#d2def4;word-break:break-word}
.gsp-update-copy{margin:0 0 12px;color:#9fb5d8}
.gsp-update-form-grid{display:grid;grid-template-columns:minmax(180px,240px) minmax(0,1fr);gap:12px 16px;align-items:start}
.gsp-update-form-grid label{font-weight:700;color:#e6f0ff}
.gsp-update-form-grid input[type=text],.gsp-update-form-grid input[type=number]{width:100%;max-width:100%;box-sizing:border-box}
.gsp-update-actions,.gsp-update-inline-actions{display:flex;flex-wrap:wrap;gap:10px}
.gsp-update-note{display:block;margin-top:6px;color:#9fb5d8}
.gsp-update-path{word-break:break-all}
.gsp-update-details{margin-top:12px}
.gsp-update-details summary{cursor:pointer}
.gsp-update-select{max-width:100%;width:100%}
@media (max-width: 720px){.gsp-update-grid,.gsp-update-form-grid{grid-template-columns:1fr}.gsp-update-shell{gap:14px}}
</style>\n";
echo "<div class='gsp-update-shell'>\n";
echo "<section class='gsp-update-card'>\n";
echo "<h3>Current Installation</h3>\n";
echo "<table class='center'>\n";
echo "<tr><td><strong>Installed Version:</strong></td><td>" . htmlspecialchars($current_version) . "</td></tr>\n";
echo "<tr><td><strong>Current Branch:</strong></td><td>" . htmlspecialchars($current_branch) . "</td></tr>\n";
echo "<p class='gsp-update-copy'>Managed backups are stored update snapshots. Rollback backups are full snapshots currently available for restoration.</p>\n";
echo "<dl class='gsp-update-grid'>\n";
echo "<dt>Installed Version</dt><dd>" . htmlspecialchars($current_version) . "</dd>\n";
echo "<dt>Current Branch</dt><dd>" . htmlspecialchars($current_branch) . "</dd>\n";
if ($git_commit) {
echo "<tr><td><strong>Git Commit:</strong></td><td>" . htmlspecialchars(substr($git_commit, 0, 12)) . "</td></tr>\n";
echo "<dt>Git Commit</dt><dd>" . htmlspecialchars(substr($git_commit, 0, 12)) . "</dd>\n";
}
echo "<tr><td><strong>Update Trace Log:</strong></td><td><code>" . htmlspecialchars(GSP_UPDATE_LOG) . "</code></td></tr>\n";
echo "<tr><td><strong>Backup Path:</strong></td><td><code>" . htmlspecialchars($backup_base) . "</code></td></tr>\n";
echo "<tr><td><strong>Managed Backups Stored:</strong></td><td>" . intval(count($managed_backups)) . " (retention: " . intval($backup_retention) . ")</td></tr>\n";
echo "<tr><td><strong>Rollback Backups Available:</strong></td><td>" . intval(count($backups)) . "</td></tr>\n";
echo "</table>\n";
echo "<dt>Update Trace Log</dt><dd class='gsp-update-path'><code>" . htmlspecialchars(GSP_UPDATE_LOG) . "</code></dd>\n";
echo "<dt>Backup Path</dt><dd class='gsp-update-path'><code>" . htmlspecialchars($backup_base) . "</code></dd>\n";
echo "<dt>Managed Backups Stored</dt><dd>" . intval(count($managed_backups)) . " (retention: " . intval($backup_retention) . ")</dd>\n";
echo "<dt>Rollback Backups Available</dt><dd>" . intval(count($backups)) . "</dd>\n";
echo "</dl>\n";
if ($ssl_issue_count > 0) {
echo "<p style='color:#8a6d3b;'><strong>SSL warning:</strong> " . intval($ssl_issue_count) . " Apache SSL certificate issue(s) detected. Updates are not blocked by this. See Advanced Diagnostics for details.</p>\n";
}
@ -1733,41 +1746,51 @@ echo "<p style='color:#a94442;'><strong>Update preflight errors:</strong><br>" .
if (!empty($preflight_result['warnings'])) {
echo "<p style='color:#8a6d3b;'><strong>Update preflight warnings:</strong><br>" . implode('<br>', array_map('htmlspecialchars', $preflight_result['warnings'])) . "</p>\n";
}
echo "</section>\n";
echo "<h3>Repository Settings</h3>\n";
echo "<section class='gsp-update-card'>\n";
echo "<h3>Update Settings</h3>\n";
echo "<form method='POST'>\n";
echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n";
echo "<table class='center'>\n";
echo "<tr><td><strong>Repository URL / Path</strong></td><td><input type='text' name='gsp_update_repo_url' value='" . htmlspecialchars($update_cfg['repo_url'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "<tr><td><strong>Branch / Channel</strong></td><td><input type='text' name='gsp_update_branch' value='" . htmlspecialchars($update_cfg['branch'], ENT_QUOTES, 'UTF-8') . "' size='40'></td></tr>\n";
echo "<tr><td><strong>Repository Root</strong></td><td><input type='text' name='gsp_update_repo_root' value='" . htmlspecialchars($update_cfg['repo_root'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "<tr><td><strong>Panel Path</strong></td><td><input type='text' name='gsp_update_panel_path' value='" . htmlspecialchars($update_cfg['panel_path'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "<tr><td><strong>Panel Source Folder</strong></td><td><input type='text' name='gsp_update_panel_source_path' value='" . htmlspecialchars($update_cfg['panel_source_path'], ENT_QUOTES, 'UTF-8') . "' size='45'></td></tr>\n";
echo "<tr><td><strong>Git Executable</strong></td><td><input type='text' name='gsp_update_git_path' value='" . htmlspecialchars($update_cfg['git_path'], ENT_QUOTES, 'UTF-8') . "' size='45'> <small>Usually <code>git</code>.</small></td></tr>\n";
echo "<tr><td><strong>Backup Path</strong></td><td><input type='text' name='gsp_update_backup_path' value='" . htmlspecialchars($update_cfg['backup_path'], ENT_QUOTES, 'UTF-8') . "' size='85'></td></tr>\n";
echo "<tr><td><strong>Backup Retention</strong></td><td><input type='number' min='1' max='200' name='gsp_update_backup_retention' value='" . htmlspecialchars($update_cfg['backup_retention'], ENT_QUOTES, 'UTF-8') . "' style='width:90px;'> <small>Keep newest backups only.</small></td></tr>\n";
echo "<tr><td><strong>Backup Before Update</strong></td><td><label><input type='checkbox' name='gsp_update_backup_before' value='1' " . (!empty($update_cfg['backup_before_update']) ? "checked" : "") . "> create backup before updating</label></td></tr>\n";
echo "<tr><td><strong>Panel Post-update Command</strong></td><td><input type='text' name='gsp_update_panel_post_update_command' value='" . htmlspecialchars($update_cfg['panel_post_update_command'], ENT_QUOTES, 'UTF-8') . "' size='85'> <small>Admin-only dangerous command.</small></td></tr>\n";
echo "</table>\n";
echo "<p>\n";
echo "<div class='gsp-update-form-grid'>\n";
echo "<label for='gsp_update_repo_url'>Repository URL / Path</label><div><input id='gsp_update_repo_url' type='text' name='gsp_update_repo_url' value='" . htmlspecialchars($update_cfg['repo_url'], ENT_QUOTES, 'UTF-8') . "'><small class='gsp-update-note'>Git URL or safe absolute local repository path.</small></div>\n";
echo "<label for='gsp_update_branch'>Branch / Channel</label><div><input id='gsp_update_branch' type='text' name='gsp_update_branch' value='" . htmlspecialchars($update_cfg['branch'], ENT_QUOTES, 'UTF-8') . "'><small class='gsp-update-note'>The updater checks out this branch before copying the Panel folder.</small></div>\n";
echo "<label for='gsp_update_repo_root'>Repository Root</label><div><input id='gsp_update_repo_root' type='text' name='gsp_update_repo_root' value='" . htmlspecialchars($update_cfg['repo_root'], ENT_QUOTES, 'UTF-8') . "'><small class='gsp-update-note'>Panel Path is derived automatically as <code>" . htmlspecialchars($update_cfg['panel_path'], ENT_QUOTES, 'UTF-8') . "</code>.</small></div>\n";
echo "<label for='gsp_update_backup_path'>Backup Path</label><div><input id='gsp_update_backup_path' type='text' name='gsp_update_backup_path' value='" . htmlspecialchars($update_cfg['backup_path'], ENT_QUOTES, 'UTF-8') . "'></div>\n";
echo "<label for='gsp_update_backup_retention'>Backup Retention</label><div><input id='gsp_update_backup_retention' type='number' min='1' max='200' name='gsp_update_backup_retention' value='" . htmlspecialchars($update_cfg['backup_retention'], ENT_QUOTES, 'UTF-8') . "'><small class='gsp-update-note'>Keep only the newest managed backups.</small></div>\n";
echo "<label>Backup Before Update</label><div><label><input type='checkbox' name='gsp_update_backup_before' value='1' " . (!empty($update_cfg['backup_before_update']) ? "checked" : "") . "> create a full backup before updating</label></div>\n";
echo "</div>\n";
echo "<details class='gsp-update-details'>\n";
echo "<summary><strong>Advanced Settings</strong></summary>\n";
echo "<div class='gsp-update-form-grid' style='margin-top:12px;'>\n";
echo "<label>Panel Source Folder</label><div><code>" . htmlspecialchars($update_cfg['panel_source_path'], ENT_QUOTES, 'UTF-8') . "</code><small class='gsp-update-note'>The updater currently copies the fixed <code>Panel</code> folder from the checked-out repository.</small></div>\n";
echo "<label>Panel Path</label><div><code class='gsp-update-path'>" . htmlspecialchars($update_cfg['panel_path'], ENT_QUOTES, 'UTF-8') . "</code><small class='gsp-update-note'>Derived from Repository Root plus <code>/Panel</code>.</small></div>\n";
echo "<label for='gsp_update_panel_post_update_command'>Post-update Command</label><div><input id='gsp_update_panel_post_update_command' type='text' name='gsp_update_panel_post_update_command' value='" . htmlspecialchars($update_cfg['panel_post_update_command'], ENT_QUOTES, 'UTF-8') . "'><small class='gsp-update-note'>Optional command to run after a successful update. Leave blank unless required.</small></div>\n";
echo "</div>\n";
echo "</details>\n";
echo "<p class='gsp-update-actions'>\n";
echo "<button type='submit' name='gsp_update_action' value='save_settings'>Save Settings</button> ";
echo "<button type='submit' name='gsp_update_action' value='update_configured' onclick='return confirm(\"Update Panel from the configured repository and branch?\");'>Update Panel</button>\n";
echo "</p>\n";
echo "</form>\n";
echo "</section>\n";
echo "<section class='gsp-update-card'>\n";
echo "<h3>Backup</h3>\n";
echo "<form method='POST'>\n";
echo "<input type='hidden' name='gsp_update_action' value='backup_only'>\n";
echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n";
echo "<button type='submit'>Create Backup</button>\n";
echo "</form>\n";
echo "</section>\n";
if (!empty($backups)) {
echo "<section class='gsp-update-card'>\n";
echo "<h3>Rollback</h3>\n";
echo "<form method='POST'>\n";
echo "<input type='hidden' name='gsp_update_action' value='revert'>\n";
echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($csrf_token) . "'>\n";
echo "<select name='gsp_revert_backup'>\n";
echo "<select class='gsp-update-select' name='gsp_revert_backup'>\n";
foreach ($backups as $bk) {
$label = htmlspecialchars($bk['ts']);
if (!empty($bk['meta']['update_target_type']) && !empty($bk['meta']['update_target_version'])) {
@ -1776,26 +1799,29 @@ $label .= ' (before ' . htmlspecialchars($bk['meta']['update_target_type']) . ':
echo "<option value='" . htmlspecialchars($bk['ts']) . "'>{$label}</option>\n";
}
echo "</select> ";
echo "<span class='gsp-update-note'>Rollback restores only full timestamped snapshots.</span>";
echo "<label><input type='checkbox' name='gsp_restore_apache' value='1'> restore Apache configs if backup contains them</label> ";
echo "<button type='submit' onclick='return confirm(\"Restore Panel, version.json, and database from selected backup?\");'>Rollback</button>\n";
echo "</form>\n";
echo "</section>\n";
}
echo "<details style='margin-top:18px;'>\n";
echo "<section class='gsp-update-card'>\n";
echo "<details>\n";
echo "<summary><strong>Advanced Diagnostics</strong></summary>\n";
echo "<h4>Preflight</h4>\n";
echo "<table class='center'>\n";
echo "<tr><td><strong>Status:</strong></td><td>" . ($preflight_result['success'] ? 'PASS' : 'FAIL') . "</td></tr>\n";
echo "<tr><td><strong>Pending Patches:</strong></td><td>" . intval(count($patch_overview['pending'])) . "</td></tr>\n";
echo "<tr><td><strong>Patch Directory:</strong></td><td><code>" . htmlspecialchars(GSP_PATCH_DIR) . "</code></td></tr>\n";
echo "</table>\n";
echo "<dl class='gsp-update-grid'>\n";
echo "<dt>Status</dt><dd>" . ($preflight_result['success'] ? 'PASS' : 'FAIL') . "</dd>\n";
echo "<dt>Pending Patches</dt><dd>" . intval(count($patch_overview['pending'])) . "</dd>\n";
echo "<dt>Patch Directory</dt><dd class='gsp-update-path'><code>" . htmlspecialchars(GSP_PATCH_DIR) . "</code></dd>\n";
echo "</dl>\n";
echo "<h4>Apache</h4>\n";
echo "<table class='center'>\n";
echo "<tr><td><strong>Config Directory:</strong></td><td><code>" . htmlspecialchars($apache_scan_result['base']) . "</code></td></tr>\n";
echo "<tr><td><strong>Configs Found:</strong></td><td>" . intval(count($apache_scan_result['files'])) . "</td></tr>\n";
echo "<tr><td><strong>Stale Path Hits:</strong></td><td>" . intval(count($apache_scan_result['stale_issues'])) . "</td></tr>\n";
echo "<tr><td><strong>SSL Certificate Issues:</strong></td><td>" . intval($ssl_issue_count) . "</td></tr>\n";
echo "</table>\n";
echo "<dl class='gsp-update-grid'>\n";
echo "<dt>Config Directory</dt><dd class='gsp-update-path'><code>" . htmlspecialchars($apache_scan_result['base']) . "</code></dd>\n";
echo "<dt>Configs Found</dt><dd>" . intval(count($apache_scan_result['files'])) . "</dd>\n";
echo "<dt>Stale Path Hits</dt><dd>" . intval(count($apache_scan_result['stale_issues'])) . "</dd>\n";
echo "<dt>SSL Certificate Issues</dt><dd>" . intval($ssl_issue_count) . "</dd>\n";
echo "</dl>\n";
if (!empty($apache_scan_result['stale_issues'])) {
echo "<p style='color:#a94442;'><strong>Apache stale path issues:</strong><br>" . implode('<br>', array_map('htmlspecialchars', array_unique($apache_scan_result['stale_issues']))) . "</p>\n";
}
@ -1822,7 +1848,8 @@ echo "<input type='hidden' name='gsp_update_csrf' value='" . htmlspecialchars($c
echo "<button type='submit' onclick='return confirm(\"Backup Apache configs, run configtest, and apply path fixes?\");'>Fix Apache Paths</button>\n";
echo "</form>\n";
echo "</details>\n";
echo "</td></tr></table>\n";
echo "</section>\n";
echo "</div>\n";
if ($auto_restart_payload) {
gsp_render_restart_form($auto_restart_payload['action'], $csrf_token, $auto_restart_payload['nonce'], isset($auto_restart_payload['version']) ? $auto_restart_payload['version'] : '');

View file

@ -6,79 +6,53 @@
border-radius:0px;
-moz-border-radius:0px;
}
#column2 #ref.online_servers div{
float:left;
text-align:left;
width:35%;
height:15px;
overflow: hidden;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
-moz-binding: url('assets/xml/ellipsis.xml#ellipsis');
}
#column2 #ref.online_servers div#gamelink{
width:36%;
float:right;
text-align:right;
}
#column2 #ref.online_servers div.name{
width:29%;
height:18px;
white-space:nowrap;
}
#column2 #noref.online_servers div#gamelink{
float:right;
text-align:right;
width:40%;
height:18px;
overflow: hidden;
}
#column2 #noref.online_servers div.name{
float:left;
text-align:left;
width:57%;
height:18px;
overflow: hidden;
.dashboard-welcome {
margin: 0 0 18px;
}
.dragbox.bloc.rounded {
.dashboard-card-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
margin-top: 20px;
}
.dashboard-action-card {
padding: 18px 20px;
background: linear-gradient(180deg, rgba(10, 18, 32, 0.92) 0%, rgba(14, 25, 45, 0.94) 100%);
border: 1px solid rgba(77, 160, 255, 0.18);
border-radius: 8px;
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.22);
color: #d2def4;
min-height: 100%;
}
.dragbox.bloc.rounded > h4,
.dashboard-promo-card > h4,
.dashboard-secondary-card > h4 {
margin: 0 0 12px;
.dashboard-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.dashboard-card-header > h3 {
margin: 0;
padding-bottom: 12px;
border-bottom: 1px solid rgba(77, 160, 255, 0.18);
color: #e6f0ff;
letter-spacing: 0.04em;
flex: 1 1 auto;
}
.dragbox-content {
padding: 4px 2px 2px;
color: #d2def4;
}
.dashboard-widget-copy {
position: relative;
padding-right: 56px;
}
.dashboard-widget-copy p {
.dashboard-action-card p {
margin: 0 0 12px;
}
.dashboard-widget-icon {
position: absolute;
top: 4px;
right: 0;
width: 40px;
height: 40px;
opacity: 0.9;
flex: 0 0 auto;
}
.dashboard-link-group {
@ -132,34 +106,13 @@
box-shadow: 0 0 0 3px rgba(90, 199, 247, 0.12);
}
.dashboard-status-panel {
margin-top: 20px;
}
.dashboard-status-content {
padding: 18px 20px;
text-align: center;
color: #d2def4;
}
.dashboard-status-content p {
margin: 0 0 14px;
}
.dashboard-status-button {
margin-bottom: 10px;
}
.dashboard-status-note {
display: block;
color: #9fb5d8;
@media (max-width: 991px) {
.dashboard-card-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 430px) {
.dashboard-widget-copy {
padding-right: 0;
}
.dashboard-cta-button,
.dashboard-support-link {
width: 100%;
@ -167,8 +120,7 @@
}
.dashboard-widget-icon {
position: static;
display: block;
margin: 0 0 12px auto;
width: 36px;
height: 36px;
}
}

View file

@ -1,5 +1,3 @@
<script type="text/javascript" src="js/jquery/plugins/jquery.json-2.3.min.js"></script>
<script type="text/javascript" src="js/modules/dashboard.js"></script>
<?php
/*
*
@ -24,248 +22,92 @@
*
*/
require_once('includes/lib_remote.php');
function exec_ogp_module()
{
global $db, $settings, $loggedInUserInfo;
global $settings;
$projectRequestUrl = htmlspecialchars(gsp_project_request_url(), ENT_QUOTES, 'UTF-8');
$discordInviteUrl = htmlspecialchars(gsp_discord_invite_url(), ENT_QUOTES, 'UTF-8');
$serverStatusUrl = htmlspecialchars(gsp_server_status_url(), ENT_QUOTES, 'UTF-8');
$theme = isset($settings['theme']) ? $settings['theme'] : 'SimpleBootstrap';
$themeBase = 'themes/' . htmlspecialchars($theme, ENT_QUOTES, 'UTF-8') . '/images/icons/';
$cards = array(
array(
'title' => 'Account Overview',
'copy' => 'Review your assigned game servers and open the Game Monitor.',
'button_label' => 'View My Servers',
'button_class' => 'dashboard-cta-button',
'url' => 'home.php?m=gamemanager&p=game_monitor',
'icon' => $themeBase . 'game_monitor.png',
),
array(
'title' => 'Custom Server Code',
'copy' => 'Need something beyond the standard server options? Our developers can customize, repair, automate, or extend your game server with mods, scripts, integrations, migrations, and server-specific tools.',
'secondary' => 'Tell us what you want to build or improve, and we will review the request with you.',
'button_label' => 'Request Custom Development',
'button_class' => 'dashboard-cta-button',
'url' => $projectRequestUrl,
'icon' => $themeBase . 'folder.png',
'external' => true,
),
array(
'title' => 'Support',
'copy' => 'Need help with an existing service? Create a support ticket or join our Discord support server.',
'secondary' => 'Support covers service problems, broken functionality, routine troubleshooting, and help using the features already included with your server.',
'button_label' => 'Create Support Ticket',
'button_class' => 'dashboard-support-link',
'url' => 'home.php?m=tickets&p=submitticket',
'icon' => $themeBase . 'support.png',
'secondary_button_label' => 'Join Discord',
'secondary_button_class' => 'dashboard-support-link dashboard-support-link-secondary',
'secondary_url' => $discordInviteUrl,
'secondary_external' => true,
),
array(
'title' => 'Server Status',
'copy' => 'View the current availability of all configured game-server locations.',
'secondary' => 'Status checks run only when you open the status page, so normal dashboard loading stays fast.',
'button_label' => 'View Server Status',
'button_class' => 'dashboard-cta-button',
'url' => $serverStatusUrl,
'icon' => $themeBase . 'game_monitor.png',
'external' => true,
),
);
$isAdmin = $db->isAdmin($_SESSION['user_id']);
$user_id = $_SESSION['user_id'];
$page_user = (isset($_GET['page']) && (int)$_GET['page'] > 0) ? (int)$_GET['page'] : 1; // thanks for Adjokip
$limit_user = isset($_GET['limit']) ? $_GET['limit'] : 10;
if(hasValue($loggedInUserInfo) && is_array($loggedInUserInfo) && $loggedInUserInfo["users_page_limit"] && !(isset($_GET['limit']) and !empty($_GET['limit']))){
$limit_user = $loggedInUserInfo["users_page_limit"];
if (isset($settings['welcome_title']) && $settings['welcome_title'] == "1" && isset($settings['welcome_title_message']) && !empty($settings['welcome_title_message'])) {
echo "<div class='dashboard-welcome'>" . $settings['welcome_title_message'] . "</div>";
}
if( isset($settings['welcome_title']) && $settings['welcome_title'] == "1" )
{
if( isset($settings['welcome_title_message']) && !empty($settings['welcome_title_message'] ))
{
echo "<div>" . $settings['welcome_title_message'] . "</div>";
echo "<div class='dashboard-card-grid'>";
foreach ($cards as $card) {
echo "<section class='dashboard-action-card'>";
echo "<div class='dashboard-card-header'>";
echo "<h3>" . htmlspecialchars($card['title'], ENT_QUOTES, 'UTF-8') . "</h3>";
if (!empty($card['icon'])) {
echo "<img src='" . $card['icon'] . "' class='dashboard-widget-icon' alt=''>";
}
echo "</div>";
echo "<p>" . htmlspecialchars($card['copy'], ENT_QUOTES, 'UTF-8') . "</p>";
if (!empty($card['secondary'])) {
echo "<p class='dashboard-card-note'>" . htmlspecialchars($card['secondary'], ENT_QUOTES, 'UTF-8') . "</p>";
}
require_once("includes/refreshed.php");
$refresh = new refreshed();
$OnlineServers = "<p>Recent updates and changes</p>";
?>
<div style="margin-top:20px;">
<?php
//$title[$id] = "The Title";
//$content[$id] = "Content of the Widget";
$title = array();
$content = array();
$href = array();
// Account Overview
$title[1] = "Account Overview";
$content[1] = 'Review your assigned servers and jump straight into the Game Monitor.';
$href[1] = 'home.php?m=gamemanager&p=game_monitor';
// Recent News
//$xml=simplexml_load_file("modules/news/data/listings.xml");
//$lastnews = count((array)$xml)-1;
//$title[2] = "Recent News";
//$content[2] = $xml->listing[$lastnews]->title;
//$href[2] = 'home.php?m=news&p=news';
// Notifications
$title[3] = "Notifications"; // get_lang('orders');
$content[3] = 'View all your notifications. ';
$href[3] = 'home.php?m=circular&p=show_circular&list=true';
// Custom Server Code
$title[4] = 'Custom Server Code';
$content[4] = '<div class="dashboard-widget-copy">'
. '<p>Need something beyond the standard server options? Our developers can customize, repair, automate, or extend your game server with mods, scripts, integrations, migrations, and server-specific tools.</p>'
. '<p class="dashboard-card-note">Tell us what you want to build or improve, and we will review the request with you.</p>'
. '<a class="dashboard-cta-button" href="' . $projectRequestUrl . '" target="_blank" rel="noopener noreferrer">Request Custom Development</a>'
. '</div>';
$href[4] = null;
// Support
$title[5] = 'Support';
$content[5] = '<div class="dashboard-widget-copy">'
. '<img src="themes/' . $settings['theme'] . '/images/icons/support.png" class="dashboard-widget-icon" alt="" />'
. '<p>Need help with an existing service? Submit a support ticket or join our Discord support server.</p>'
. '<p class="dashboard-card-note">Support is for service problems, broken functionality, routine troubleshooting, and help using the features already included with your server.</p>'
. '<div class="dashboard-link-group">'
. '<a class="dashboard-support-link" href="home.php?m=tickets">Open Support Tickets</a>'
. '<a class="dashboard-support-link dashboard-support-link-secondary" href="' . $discordInviteUrl . '" target="_blank" rel="noopener noreferrer">Join Discord Support</a>'
. '</div>'
. '</div>';
$href[5] = null;
$widgets = $db->resultQuery("SELECT * FROM OGP_DB_PREFIXwidgets_users WHERE user_id='".$_SESSION['user_id']."' ORDER BY sort_no");
if(!$widgets)
{
if($db->createUserWidgets($_SESSION['user_id']))
$widgets = $db->resultQuery("SELECT * FROM OGP_DB_PREFIXwidgets_users WHERE user_id='".$_SESSION['user_id']."' ORDER BY sort_no");
echo "<div class='dashboard-link-group'>";
echo "<a class='" . htmlspecialchars($card['button_class'], ENT_QUOTES, 'UTF-8') . "' href='" . htmlspecialchars($card['url'], ENT_QUOTES, 'UTF-8') . "'";
if (!empty($card['external'])) {
echo " target='_blank' rel='noopener noreferrer'";
}
if($widgets)
{
$colhtml[1] = '<div class="column one_fourth" id="column1" >';
$colhtml[2] = '<div class="column one_two" id="column2" >';
$colhtml[3] = '<div class="column one_fourth" id="column3" >';
foreach ((array)$widgets as $widget)
{
if(array_key_exists($widget["widget_id"], (array)$title)){
if( (!isset($settings['old_dashboard_behavior']) or $settings['old_dashboard_behavior'] == 0) AND $widget['widget_id'] == "3" )
continue;
$colhtml[$widget['column_id']] .= '<div class="dragbox bloc rounded" id="item'.$widget['widget_id'].'">'.
'<h4><span class="configure"></span>';
if(!is_null($title[$widget['widget_id']]))
$colhtml[$widget['column_id']] .= $title[$widget['widget_id']];
$colhtml[$widget['column_id']] .= '</h4><div class="dragbox-content" ';
if(!is_null($href[$widget['widget_id']]))
{
$colhtml[$widget['column_id']] .= "onclick=\"location.href='". $href[$widget['widget_id']] . "'\" style=\"cursor:pointer;";
if($widget['collapsed']==1)
$colhtml[$widget['column_id']] .= 'display:none;';
$colhtml[$widget['column_id']] .= '"';
echo ">" . htmlspecialchars($card['button_label'], ENT_QUOTES, 'UTF-8') . "</a>";
if (!empty($card['secondary_button_label']) && !empty($card['secondary_url'])) {
echo "<a class='" . htmlspecialchars($card['secondary_button_class'], ENT_QUOTES, 'UTF-8') . "' href='" . htmlspecialchars($card['secondary_url'], ENT_QUOTES, 'UTF-8') . "'";
if (!empty($card['secondary_external'])) {
echo " target='_blank' rel='noopener noreferrer'";
}
elseif($widget['collapsed']==1)
$colhtml[$widget['column_id']] .= 'style="display:none;"';
$colhtml[$widget['column_id']] .= '>';
if(!is_null($content[$widget['widget_id']]))
$colhtml[$widget['column_id']] .= $content[$widget['widget_id']];
$colhtml[$widget['column_id']] .= '</div></div>';
echo ">" . htmlspecialchars($card['secondary_button_label'], ENT_QUOTES, 'UTF-8') . "</a>";
}
echo "</div>";
echo "</section>";
}
foreach ((array)$colhtml as $html)
echo $html.'</div>';
}
// Server Status Link - Available to all users
echo "<div class='dashboard-status-panel'>
<div class='bloc rounded'>
<h4>Server Status</h4>
<div class='dashboard-status-content'>
<p>Check the current state of configured remote servers without delaying the main dashboard.</p>
<a class='dashboard-cta-button dashboard-status-button' href='{$serverStatusUrl}' target='_blank' rel='noopener noreferrer'>View Server Status</a>
<small class='dashboard-status-note'>Opens in a new tab and runs remote checks only when requested.</small>
</div>
</div>
</div>";
if( $isAdmin AND $db->isModuleInstalled('status') )
{
echo "<h0>".get_lang('server_status')."</h0><br>";
$servers = $db->getRemoteServers();
echo "<div id='column4' style='float:left;width:40%;' >
<div class='bloc rounded' >
<h4>".get_lang('select_remote_server')."</h4>
<div>
<br>
<center>
<form action='' method='GET'>
<input type='hidden' name='m' value='".$_GET['m']."'/>
<input type='hidden' name='p' value='".$_GET['p']."'/>
<select name='remote_server_id' onchange=".'"this.form.submit()"'.">\n";
$agents_ips = array();
foreach ((array)$servers as $server_row)
{
$agents_ips[$server_row['remote_server_id']] = gethostbyname($server_row['agent_ip']);
if( !empty( $server_row['remote_server_id'] ) and !isset( $_GET['remote_server_id'] ) OR !empty( $server_row['remote_server_id'] ) and empty( $_GET['remote_server_id'] ) )
{
$_GET['remote_server_id'] = $server_row['remote_server_id'];
}
if( isset($_GET['remote_server_id']) AND $_GET['remote_server_id'] == $server_row['remote_server_id'] )
{
$remote = new OGPRemoteLibrary( $server_row['agent_ip'], $server_row['agent_port'],
$server_row['encryption_key'], $server_row['timeout'] );
$host_stat = $remote->status_chk();
if( $host_stat === 1 )
{
$checked = "selected='selected'";
}
else
{
$checked = '';
$_GET['remote_server_id'] = 'webhost';
}
}
else
{
$checked = '';
}
echo "<option value='".$server_row['remote_server_id']."' $checked >".$server_row['remote_server_name']."</option>\n";
}
if ( function_exists('exec') )
{
$host_ip = isset($_SERVER['LOCAL_ADDR']) ? $_SERVER['LOCAL_ADDR'] : $_SERVER['SERVER_ADDR'];
$remote_server_id = array_search($host_ip,$agents_ips);
$show_webhost = true;
if($remote_server_id)
{
$remote_server = $db->getRemoteServer($remote_server_id);
$remote = new OGPRemoteLibrary( $remote_server['agent_ip'], $remote_server['agent_port'],
$remote_server['encryption_key'], $remote_server['timeout'] );
$host_stat = $remote->status_chk();
if( $host_stat === 1 )
$show_webhost = false;
}
if($show_webhost)
{
$checked = ( isset($_GET['remote_server_id']) AND $_GET['remote_server_id'] == 'webhost' ) ? "selected='selected'" : "";
echo "<option value='webhost' $checked >Webhost Status</option>";
}
}
echo " </select>
</form>
</center>
<br><br>
</div>
</div>
</div>\n";
if( isset($_GET['remote_server_id']) AND ( $_GET['remote_server_id'] == "webhost" or $_GET['remote_server_id'] == "" ) )
unset($_GET['remote_server_id']);
if( isset($_GET['remote_server_id']) )
$remote_server = "&remote_server_id=".$_GET['remote_server_id'];
else
$remote_server = "";
if( isset($_GET['remote_server_id']) OR function_exists('exec') )
echo $refresh->getdiv($refresh->add("home.php?m=status&type=cleared".$remote_server));
}
?>
</div>
<script type="text/javascript">
$(document).ready(function(){
<?php echo $refresh->build(isset($settings['query_cache_life']) ? $settings['query_cache_life'] * 2000 : 60000); ?>
});
</script>
<?php
echo "</div>";
}
?>

View file

@ -53,33 +53,6 @@ $settings = $db->getSettings();
@$GLOBALS['panel_language'] = $settings['panel_language'];
ogpLang();
function panel_ping_host($host, $timeout = 3) {
if (!function_exists('exec')) {
return false;
}
$output = array();
$result = 0;
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
exec("ping -n 1 -w " . ((int)$timeout * 1000) . " " . escapeshellarg($host), $output, $result);
} else {
exec("ping -c 1 -W " . (int)$timeout . " " . escapeshellarg($host), $output, $result);
}
if ($result !== 0) {
return false;
}
foreach ((array)$output as $line) {
if (preg_match('/time[=<]?\s*([0-9.]+)\s*ms/i', $line, $matches)) {
return (float)$matches[1];
}
}
return 0.0;
}
function agent_socket_reachable($host, $port, $timeout = 2.0) {
$errno = 0;
$errstr = '';
@ -203,27 +176,13 @@ $servers = $db->getRemoteServers();
color: var(--text);
}
.status-meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
margin: 0 0 20px;
}
.status-meta-item {
padding: 14px 16px;
.status-summary {
margin: 0 0 18px;
padding: 12px 14px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.status-meta-item strong {
display: block;
margin-bottom: 6px;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text);
color: var(--muted);
}
.status-table-wrap {
@ -235,7 +194,7 @@ $servers = $db->getRemoteServers();
table {
width: 100%;
min-width: 760px;
min-width: 540px;
border-collapse: collapse;
}
@ -297,25 +256,6 @@ $servers = $db->getRemoteServers();
color: var(--danger);
}
.latency-good {
color: var(--success);
font-weight: 700;
}
.latency-medium {
color: var(--warning);
font-weight: 700;
}
.latency-bad {
color: var(--danger);
font-weight: 700;
}
.latency-muted {
color: var(--muted);
}
.status-footer {
display: flex;
flex-wrap: wrap;
@ -364,20 +304,7 @@ $servers = $db->getRemoteServers();
</div>
</div>
<div class="status-meta">
<div class="status-meta-item">
<strong>Latency label</strong>
<span>Panel-to-server latency. This is a server-side connectivity check from the Panel host, not latency from the customer browser.</span>
</div>
<div class="status-meta-item">
<strong>Status source</strong>
<span>Remote servers from the active Panel configuration with live agent status checks.</span>
</div>
<div class="status-meta-item">
<strong>Checked</strong>
<span><?php echo htmlspecialchars($checkedAt, ENT_QUOTES, 'UTF-8'); ?></span>
</div>
</div>
<p class="status-summary"><strong>Checked:</strong> <?php echo htmlspecialchars($checkedAt, ENT_QUOTES, 'UTF-8'); ?></p>
<?php if (empty($servers)): ?>
<p class="status-empty">No remote servers are currently configured.</p>
@ -389,9 +316,6 @@ $servers = $db->getRemoteServers();
<th>Server Name</th>
<th>Location / IP</th>
<th>Status</th>
<th>Agent Status</th>
<th>Panel-to-server latency</th>
<th>Last Checked</th>
</tr>
</thead>
<tbody>
@ -404,28 +328,15 @@ $servers = $db->getRemoteServers();
$agentReachable = ($agentIp !== '' && $agentPort > 0) ? agent_socket_reachable($agentIp, $agentPort, 2.0) : false;
$remote = new OGPRemoteLibrary($agentIp, $agentPort, $server['encryption_key'], $server['timeout']);
$agentStatus = $remote->status_chk();
$latency = $displayIp !== '' ? panel_ping_host($displayIp, 3) : false;
if ($agentStatus === 1) {
$overallStatus = status_badge('Online', 'status-online');
$agentBadge = status_badge('Available', 'status-online');
} elseif ($agentReachable) {
$overallStatus = status_badge('Timed out', 'status-warning');
} elseif ($agentIp !== '') {
$overallStatus = status_badge('Agent unavailable', 'status-offline');
} else {
$overallStatus = status_badge('Unknown', 'status-warning');
$agentBadge = status_badge('Timed out', 'status-warning');
} else {
$overallStatus = status_badge('Offline', 'status-offline');
$agentBadge = status_badge('Unavailable', 'status-offline');
}
$latencyClass = 'latency-muted';
if ($latency !== false) {
if ($latency <= 50) {
$latencyClass = 'latency-good';
} elseif ($latency <= 150) {
$latencyClass = 'latency-medium';
} else {
$latencyClass = 'latency-bad';
}
}
?>
<tr>
@ -440,15 +351,6 @@ $servers = $db->getRemoteServers();
<?php endif; ?>
</td>
<td><?php echo $overallStatus; ?></td>
<td><?php echo $agentBadge; ?></td>
<td>
<?php if ($latency !== false): ?>
<span class="<?php echo htmlspecialchars($latencyClass, ENT_QUOTES, 'UTF-8'); ?>"><?php echo htmlspecialchars(number_format($latency, 1), ENT_QUOTES, 'UTF-8'); ?> ms</span>
<?php else: ?>
<span class="latency-muted">Unavailable</span>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($checkedAt, ENT_QUOTES, 'UTF-8'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>

View file

@ -23,6 +23,22 @@ The update page uses these settings:
- `gsp_update_panel_post_update_command`
- `gsp_update_backup_before`
The current admin UI shows these directly:
- Repository URL / Path
- Branch / Channel
- Repository Root
- Backup Path
- Backup Retention
- Backup Before Update
The current admin UI keeps these derived or advanced:
- Panel Path is derived from Repository Root plus `/Panel`
- Panel Source Folder is fixed to `Panel`
- Git Executable is hidden and defaults to `git`
- Post-update Command remains under Advanced Settings
## Defaults
- Repository URL / Path: `http://forge.runlevelsystems.com/dev/GSP.git`
@ -58,9 +74,15 @@ The backup directory is created recursively if missing.
Retention is enforced after each successful backup.
Current count labels:
- `Managed Backups Stored` means all managed updater snapshots discovered in the backup base
- `Rollback Backups Available` means only full timestamped backups that the restore flow can actually restore
## Rollback Workflow
- The rollback selector lists managed backups.
- The restore action only accepts full timestamped backups from `gsp_get_available_backups()`.
- Restoring a backup restores the Panel tree and metadata.
- Missing website archives are ignored because the active backup path is Panel-focused.
- Apache config restore is optional.

View file

@ -20,7 +20,7 @@ The active update page is intentionally narrow:
- shows the installed Panel version
- shows the current git branch and commit when available
- exposes editable repository settings for the Panel update only
- exposes a reduced set of commonly changed repository settings for the Panel update only
- can create a Panel backup
- can update the Panel from the configured repository and branch
- can roll back to an existing Panel backup
@ -43,6 +43,22 @@ The admin page stores these settings in the Panel settings table:
- `gsp_update_panel_post_update_command`
- `gsp_update_backup_before`
The active UI now exposes these commonly changed settings directly:
- Repository URL / Path
- Branch / Channel
- Repository Root
- Backup Path
- Backup Retention
- Backup Before Update
The active UI now treats these as derived or advanced:
- Panel Path is derived as `Repository Root + /Panel`
- Panel Source Folder is fixed to `Panel`
- Git Executable is hidden and defaults to `git`
- Post-update Command remains available under Advanced Settings
Defaults:
- Repository URL / Path: `http://forge.runlevelsystems.com/dev/GSP.git`
@ -66,20 +82,25 @@ Retention is enforced after each successful backup.
If retention is `5`, the newest five managed backups are kept and older managed backups are pruned automatically.
Current backup counters on the admin page:
- `Managed Backups Stored` counts all managed updater snapshots returned by `gsp_get_managed_backup_entries()`, including full and component-style entries when present
- `Rollback Backups Available` counts only full timestamped backups returned by `gsp_get_available_backups()` that the restore flow can actually use
## Implementation Notes
- `gsp_update_settings()` and `gsp_validate_update_settings()` remain top-level in `Panel/modules/administration/panel_update.php`.
- `gsp_checkout_update_source()` remains a top-level compatibility helper for the configured repository checkout.
- `gsp_do_configured_git_update()` now applies only the Panel source folder into the configured Panel path.
- `gsp_do_configured_git_update()` now applies only the fixed `Panel` source folder into the derived Panel path.
- The page no longer depends on website or agent update settings.
## Update Flow
1. Save or submit repository settings.
2. Validate repository URL, branch, repo root, Panel path, and backup settings.
2. Validate repository URL, branch, repo root, and backup settings.
3. Create a backup when enabled.
4. Clone the configured repository source and branch into a temporary checkout.
5. Copy only the configured Panel source folder into the configured Panel path.
5. Copy only the fixed `Panel` source folder into the derived Panel path.
6. Preserve `Panel/includes/config.inc.php`.
7. Run module updates/post-update hooks.
8. Write version metadata and `LAST_UPDATE.txt`.

View file

@ -9,17 +9,16 @@ Main landing dashboard with widgets and quick server overview.
## Current Status
- Functional
- Uses the existing collapsible widget layout for customer-facing quick actions
- Uses a fixed four-card customer dashboard layout
## Dependencies
- Panel dashboard widgets
- user permissions
- shared Panel URL helpers
## Database Tables
- `widgets`
- `widgets_users`
- none required for the active customer dashboard card layout
## Agent Interaction
@ -40,12 +39,12 @@ Main landing dashboard with widgets and quick server overview.
## Known Issues
- some default widget IDs are legacy and limit how far the collapsible layout can be expanded without a schema change
- legacy widget infrastructure still exists in the module for historical compatibility, but the active customer dashboard no longer depends on it
## Missing Functionality
- richer status and alert surfaces
- deeper async server-status embedding without adding remote checks to normal dashboard loads
- deeper account summaries if a future design needs per-user metrics beyond the Game Monitor link
## Suggested Future Improvements
@ -55,31 +54,41 @@ Main landing dashboard with widgets and quick server overview.
- Keep / Improve
## Runlevel Systems Project Request Integration
## Current Dashboard Cards
- Dashboard render file: `Panel/modules/dashboard/dashboard.php`
- Dashboard styles: `Panel/modules/dashboard/dashboard.css`
- Shared Panel project URL helper: `Panel/includes/functions.php`
- Shared Discord invite helper: `Panel/includes/functions.php`
- Shared server-status route helper: `Panel/includes/functions.php`
- Theme base: active SimpleBootstrap icon set
Current project request URL:
Rendered cards:
- `https://runlevelsystems.com/start-project.php`
- `Account Overview`
- button: `View My Servers`
- route: `home.php?m=gamemanager&p=game_monitor`
- `Custom Server Code`
- button: `Request Custom Development`
- URL: `https://runlevelsystems.com/start-project.php`
- opens in a new tab
- `Support`
- buttons:
- `Create Support Ticket`
- `Join Discord`
- support route: `home.php?m=tickets&p=submitticket`
- Discord URL: `https://discord.gg/qt9Hnkj6cv`
- `Server Status`
- button: `View Server Status`
- route: `server_status.php`
- opens separately so dashboard rendering does not wait on remote checks
Dashboard behavior:
Support versus custom development:
- retained collapsible sections:
- `Account Overview`
- `Custom Server Code`
- `Support`
- removed duplicate standalone sections:
- `Custom Server Development`
- `Support and Troubleshooting`
- support remains the path for routine troubleshooting, service issues, and existing features
- the custom-development CTA points users at Runlevel Systems for project work such as custom scripts, mods, integrations, automation, migrations, dashboards, and advanced server tooling
- the Discord support invite should use:
- `https://discord.gg/qt9Hnkj6cv`
- `Custom Server Code` should open the Runlevel project request in a new tab
- `Support` should keep ticketing and Discord support separate from paid project work
- `Server Status` stays available from the dashboard, but remote checks are intentionally deferred to the separate status page so normal dashboard loads do not block on agent/network timeouts
- support is for routine troubleshooting, service issues, and help using included features
- custom development is for scripts, integrations, automation, migrations, mods, and new tooling
Behavior change:
- collapsible behavior was removed from the active customer dashboard blocks
- the old blank-space-prone widget columns are no longer used for the four main customer actions

View file

@ -56,26 +56,34 @@ Admin status page for server/node state.
## Display Rules
- Removed the old `Hostname` column
- Removed the repeated per-row `Last Checked` column
- Keep one overall `Checked:` timestamp above the table
- Removed Panel-to-server latency from the customer-facing page
- Keep:
- `Server Name`
- `Location / IP`
- `Status`
- `Agent Status`
- `Panel-to-server latency`
- `Last Checked`
- Latency wording must remain truthful:
- current label: `Panel-to-server latency`
- this is a server-side connectivity check from the Panel host
- it is not customer-browser latency and must not be presented as the player's ping
- Status values should remain compact and truthful:
- `Online`
- `Timed out`
- `Agent unavailable`
- `Unknown`
Latency note:
- Browser ICMP ping is not available from the Panel UI
- The previous Panel-to-server latency values were removed because they do not represent the customer's connection to the game-server location
- Do not attempt to ping the customer's IP from the Panel or from a game server
## Theme Notes
- The status page should visually match the active dark Panel theme
- Use responsive table wrapping for mobile widths instead of forcing a wide desktop table into the viewport
- Keep the page compact and avoid repeated explanatory cards
## Suggested Future Improvements
- optional browser-side latency testing only if each location has a safe public health endpoint and the implementation can remain honest about what is being measured
- optional browser-side location latency testing only if each location has a safe public HTTP or HTTPS health endpoint and the implementation remains honest about what is being measured
## Recommendation

View file

@ -46,12 +46,12 @@ Panel update and patch management.
## Missing Functionality
- richer update history and rollback guidance
- live progress polling for asynchronous remote agent updates
- clearer operator documentation around backup counts and derived settings
## Suggested Future Improvements
- keep admin-only and document carefully
- add status polling for `component_update` logs after the agent has restarted
- add richer update history summaries if the page later needs deeper audit visibility
## Recommendation
@ -63,9 +63,12 @@ The primary update implementation lives in `Panel/modules/administration/panel_u
Current update targets:
- Panel files
- Website files
- Linux agents
- Windows/Cygwin agents
- Panel files only in the active admin UI
The updater uses a single configured Git repository with component source folders such as `Panel`, `Website`, `Agent_Linux`, and `Agent-Windows`. Remote agents are updated through the encrypted `component_update` XML-RPC method and preserve hosted game data and agent configuration folders.
Active UI behavior:
- Repository Root is the primary deployment path setting
- Panel Path is derived internally as `Repository Root + /Panel`
- Panel Source Folder is fixed to `Panel`
- Git Executable is hidden and defaults to `git`
- Post-update Command remains available only as an advanced field