diff --git a/containers/libreportal/frontend/html/topbar.html b/containers/libreportal/frontend/html/topbar.html
index b5b733a..a5ef5ae 100755
--- a/containers/libreportal/frontend/html/topbar.html
+++ b/containers/libreportal/frontend/html/topbar.html
@@ -51,17 +51,6 @@
Backups
-
diff --git a/containers/libreportal/frontend/js/components/config/config-manager.js b/containers/libreportal/frontend/js/components/config/config-manager.js
index 689ebc5..e1f2ca5 100755
--- a/containers/libreportal/frontend/js/components/config/config-manager.js
+++ b/containers/libreportal/frontend/js/components/config/config-manager.js
@@ -57,6 +57,28 @@ if (typeof window.ConfigManager === 'undefined') {
return;
}
+ // Peers is an admin tool page — named references to other LibrePortal
+ // instances (kind=backup-channel for shared-backup migrate, or
+ // kind=direct-ssh-direct for live SSH pulls). Same shape as SSH Access:
+ // we inject its content template, then init PeersPage.
+ if (category === 'peers') {
+ try { this.sidebar.populateSidebar(); } catch (e) {}
+ try {
+ const html = await fetch('/html/peers-content.html').then(r => r.text());
+ configSection.innerHTML = html;
+ } catch (e) {
+ configSection.innerHTML = '
Peers page template failed to load.
';
+ return;
+ }
+ if (typeof PeersPage !== 'undefined') {
+ window.peersPage = new PeersPage('config-section');
+ await window.peersPage.init();
+ } else {
+ configSection.innerHTML = '
PeersPage controller not loaded.
';
+ }
+ return;
+ }
+
// System is an admin tool page (live host + per-app statistics) with its
// own controller, like SSH Access above.
if (category === 'system') {
diff --git a/containers/libreportal/frontend/js/components/config/config-sidebar.js b/containers/libreportal/frontend/js/components/config/config-sidebar.js
index 7f2e5ae..8024a1c 100755
--- a/containers/libreportal/frontend/js/components/config/config-sidebar.js
+++ b/containers/libreportal/frontend/js/components/config/config-sidebar.js
@@ -36,6 +36,24 @@ class ConfigSidebar {
});
this.categoriesList.appendChild(overviewItem);
+ // System sits right under Overview — both are admin-landing surfaces
+ // (Overview = ops/health summary, System = live host + per-app stats),
+ // distinct from the config form pages or the Tools utilities below.
+ const systemItem = document.createElement('div');
+ systemItem.className = 'category';
+ systemItem.setAttribute('data-category', 'system');
+ systemItem.innerHTML = '
System';
+ systemItem.addEventListener('click', function () {
+ window.history.pushState({}, '', window.adminPath('system'));
+ document.querySelectorAll('.category').forEach(function (item) { item.classList.remove('active'); });
+ this.classList.add('active');
+ window.configCategory = 'system';
+ if (window.configManager && typeof window.configManager.renderConfig === 'function') {
+ window.configManager.renderConfig('system');
+ }
+ });
+ this.categoriesList.appendChild(systemItem);
+
// "Config" group heading above the configuration categories (mirrors the
// "Tools" heading below).
const configLabel = document.createElement('div');
@@ -118,20 +136,20 @@ class ConfigSidebar {
});
self.categoriesList.appendChild(sshItem);
- const systemItem = document.createElement('div');
- systemItem.className = 'category';
- systemItem.setAttribute('data-category', 'system');
- systemItem.innerHTML = '
System';
- systemItem.addEventListener('click', function () {
- window.history.pushState({}, '', window.adminPath('system'));
+ const peersItem = document.createElement('div');
+ peersItem.className = 'category';
+ peersItem.setAttribute('data-category', 'peers');
+ peersItem.innerHTML = '
Peers';
+ peersItem.addEventListener('click', function () {
+ window.history.pushState({}, '', window.adminPath('peers'));
document.querySelectorAll('.category').forEach(function (item) { item.classList.remove('active'); });
this.classList.add('active');
- window.configCategory = 'system';
+ window.configCategory = 'peers';
if (window.configManager && typeof window.configManager.renderConfig === 'function') {
- window.configManager.renderConfig('system');
+ window.configManager.renderConfig('peers');
}
});
- self.categoriesList.appendChild(systemItem);
+ self.categoriesList.appendChild(peersItem);
// Set initial active category
this.setActiveCategory(window.configCategory || 'overview');
diff --git a/containers/libreportal/frontend/js/components/peers/peers-page.js b/containers/libreportal/frontend/js/components/peers/peers-page.js
index f1d4cde..a2f5351 100644
--- a/containers/libreportal/frontend/js/components/peers/peers-page.js
+++ b/containers/libreportal/frontend/js/components/peers/peers-page.js
@@ -4,7 +4,12 @@
// kind=backup-channel; the other kinds light up in Phase 3.
class PeersPage {
- constructor() {
+ constructor(rootId = 'config-section') {
+ // rootId is the container the page renders into when embedded under
+ // /admin/tools/peers. The peers-* element IDs are unique enough that
+ // global document.getElementById queries still find them inside the
+ // injected template — but keeping the ref for future scoped queries.
+ this.rootId = rootId;
this.peers = [];
this.backupLocations = []; // populated for the loc_idx dropdown
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
diff --git a/containers/libreportal/frontend/js/spa.js b/containers/libreportal/frontend/js/spa.js
index 7e00169..b31935c 100755
--- a/containers/libreportal/frontend/js/spa.js
+++ b/containers/libreportal/frontend/js/spa.js
@@ -77,7 +77,7 @@ class LibrePortalSPAClean {
this.routes.set('/tasks*', () => this.handleTasks()); // Handle /tasks with query
this.routes.set('/backup', () => this.handleBackup());
this.routes.set('/backup*', () => this.handleBackup());
- this.routes.set('/peers', () => this.handlePeers());
+ this.routes.set('/peers', () => this.handlePeers()); // legacy → /admin/tools/peers
this.routes.set('/peers*', () => this.handlePeers());
this.routes.set('/ssh', () => this.handleSsh()); // legacy → /admin/tools/ssh-access
this.routes.set('/ssh*', () => this.handleSsh());
@@ -275,19 +275,8 @@ class LibrePortalSPAClean {
}
async handlePeers() {
- try {
- const html = await this.fetchContent('/html/peers-content.html');
- this.loadContent(html, 'Peers');
- if (typeof PeersPage !== 'undefined') {
- window.peersPage = new PeersPage();
- await window.peersPage.init();
- } else {
- console.error('PeersPage class not loaded');
- }
- } catch (error) {
- console.error('❌ Peers page load error:', error);
- this.showError('Failed to load peers page');
- }
+ // Legacy /peers → Peers under the Admin area.
+ this.navigate('/admin/tools/peers', true);
}
async handleSsh() {
@@ -533,6 +522,7 @@ class LibrePortalSPAClean {
window.adminPath = function (category) {
if (!category || category === 'overview') return '/admin';
if (category === 'ssh-access') return '/admin/tools/ssh-access';
+ if (category === 'peers') return '/admin/tools/peers';
return '/admin/config/' + category;
};
window.adminCategoryFromPath = function (pathname) {