feat(admin): move Peers into Admin/Tools; lift System next to Overview
Two related UI tidies — both removing surface area from the topbar / Tools
group rather than adding new pages.
Peers → /admin/tools/peers
Was a top-level /peers route with its own topbar nav item, which doubled
the navigation surface for what's really an admin tool (same shape as
SSH Access). Now lives under the Admin sidebar's Tools group alongside
SSH Access. /peers is kept as a legacy redirect → /admin/tools/peers.
Plumbing:
- config-sidebar.js gains a Peers entry under the Tools label.
- config-manager.js gains a 'peers' branch that fetches
peers-content.html into config-section, then inits PeersPage.
- window.adminPath() learns 'peers' → /admin/tools/peers.
- spa.js handlePeers() is now a redirect (mirrors handleSsh).
- topbar.html drops the Peers nav item.
- peers-content.html slimmed to a config-section template (no
standalone page wrapper) so it embeds cleanly under the admin shell.
- PeersPage gains a rootId constructor arg for symmetry with SshPage
(queries still work globally — IDs are unique).
System lifted out of the Tools group
User feedback: 'overview/system are kinda like, the same thing'. Moved
System to sit right under Overview at the top of the sidebar, before
the 'Config' label. Both surfaces are admin-landing pages (Overview =
ops/health summary, System = live host + per-app stats) — distinct from
config form pages or the Tools utilities.
config-sidebar.js: System block moved to the top section (right after
Overview's click handler). Original Tools-group instance removed.
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
64a0509ea9
commit
cfdd39386c
@ -1,66 +1,51 @@
|
|||||||
<div class="container peers-layout">
|
<div class="peers-page">
|
||||||
<div class="main">
|
<div class="page-header config-page-header">
|
||||||
<div class="peers-page" id="peers-page">
|
<div class="page-header-title">
|
||||||
<div class="config-section">
|
<div class="admin-breadcrumb">Tools</div>
|
||||||
<div class="page-header">
|
<h1>Peers</h1>
|
||||||
<div class="page-header-icon-slot">
|
<p>Named references to other LibrePortal instances. Use them in the Migrate tab to pull apps across without typing hostnames.</p>
|
||||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
</div>
|
||||||
<circle cx="6" cy="7" r="3"></circle>
|
<div class="page-header-actions">
|
||||||
<circle cx="18" cy="7" r="3"></circle>
|
<button class="backup-refresh-btn" id="peers-refresh-btn" title="Refresh">
|
||||||
<path d="M9 17l3-3 3 3"></path>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M12 14v7"></path>
|
<polyline points="23 4 23 10 17 10"></polyline>
|
||||||
<path d="M6 10v3a3 3 0 003 3"></path>
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||||
<path d="M18 10v3a3 3 0 01-3 3"></path>
|
</svg>
|
||||||
</svg>
|
Refresh
|
||||||
</div>
|
</button>
|
||||||
<div class="page-header-title">
|
<button class="backup-secondary-btn" id="peers-token-btn" title="Show this host's pairing token">
|
||||||
<h1>Peers</h1>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<p>Named references to other LibrePortal instances. Use them in the Migrate tab to pull apps across without typing hostnames.</p>
|
<rect x="3" y="11" width="18" height="11" rx="2"></rect>
|
||||||
</div>
|
<path d="M7 11V7a5 5 0 0110 0v4"></path>
|
||||||
<div class="page-header-actions">
|
</svg>
|
||||||
<button class="backup-refresh-btn" id="peers-refresh-btn" title="Refresh">
|
Show my token
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
</button>
|
||||||
<polyline points="23 4 23 10 17 10"></polyline>
|
<button class="backup-secondary-btn" id="peers-pair-btn" title="Paste a token from another LibrePortal">
|
||||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
</svg>
|
<path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07L11.5 5.45"></path>
|
||||||
Refresh
|
<path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07L12.5 18.55"></path>
|
||||||
</button>
|
</svg>
|
||||||
<button class="backup-secondary-btn" id="peers-token-btn" title="Show this host's pairing token">
|
Pair with token
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
</button>
|
||||||
<rect x="3" y="11" width="18" height="11" rx="2"></rect>
|
<button class="backup-primary-btn" id="peers-add-btn">
|
||||||
<path d="M7 11V7a5 5 0 0110 0v4"></path>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
</svg>
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
Show my token
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
</button>
|
</svg>
|
||||||
<button class="backup-secondary-btn" id="peers-pair-btn" title="Paste a token from another LibrePortal">
|
Add backup-channel peer
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
</button>
|
||||||
<path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07L11.5 5.45"></path>
|
|
||||||
<path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07L12.5 18.55"></path>
|
|
||||||
</svg>
|
|
||||||
Pair with token
|
|
||||||
</button>
|
|
||||||
<button class="backup-primary-btn" id="peers-add-btn">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
||||||
</svg>
|
|
||||||
Add backup-channel peer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="peers-empty" id="peers-empty" hidden>
|
|
||||||
<p>No peers yet.</p>
|
|
||||||
<p class="backup-card-hint">
|
|
||||||
Add one to give a memorable name to another LibrePortal you share a backup location with.
|
|
||||||
Direct-SSH peers (no shared backup repo needed) ship with Phase 3.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="peers-list" id="peers-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="peers-empty" id="peers-empty" hidden style="padding:24px; text-align:center; opacity:.75">
|
||||||
|
<p>No peers yet.</p>
|
||||||
|
<p class="backup-card-hint">
|
||||||
|
Add one to give a memorable name to another LibrePortal you share a backup location with.
|
||||||
|
Or use <strong>Show my token</strong> / <strong>Pair with token</strong> for direct-SSH peers.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="peers-list" id="peers-list" style="padding-top:8px"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="backup-modal" id="peers-add-modal">
|
<div class="backup-modal" id="peers-add-modal">
|
||||||
|
|||||||
@ -51,17 +51,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Backups
|
Backups
|
||||||
</a>
|
</a>
|
||||||
<a href="/peers" class="nav-item" id="nav-peers">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<circle cx="6" cy="7" r="3"></circle>
|
|
||||||
<circle cx="18" cy="7" r="3"></circle>
|
|
||||||
<path d="M9 17l3-3 3 3"></path>
|
|
||||||
<path d="M12 14v7"></path>
|
|
||||||
<path d="M6 10v3a3 3 0 003 3"></path>
|
|
||||||
<path d="M18 10v3a3 3 0 01-3 3"></path>
|
|
||||||
</svg>
|
|
||||||
Peers
|
|
||||||
</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
<div class="mobile-drawer-page-section" id="mobile-drawer-page-section"></div>
|
<div class="mobile-drawer-page-section" id="mobile-drawer-page-section"></div>
|
||||||
<div class="topbar-controls">
|
<div class="topbar-controls">
|
||||||
|
|||||||
@ -57,6 +57,28 @@ if (typeof window.ConfigManager === 'undefined') {
|
|||||||
return;
|
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 = '<div class="error">Peers page template failed to load.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof PeersPage !== 'undefined') {
|
||||||
|
window.peersPage = new PeersPage('config-section');
|
||||||
|
await window.peersPage.init();
|
||||||
|
} else {
|
||||||
|
configSection.innerHTML = '<div class="error">PeersPage controller not loaded.</div>';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// System is an admin tool page (live host + per-app statistics) with its
|
// System is an admin tool page (live host + per-app statistics) with its
|
||||||
// own controller, like SSH Access above.
|
// own controller, like SSH Access above.
|
||||||
if (category === 'system') {
|
if (category === 'system') {
|
||||||
|
|||||||
@ -36,6 +36,24 @@ class ConfigSidebar {
|
|||||||
});
|
});
|
||||||
this.categoriesList.appendChild(overviewItem);
|
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 = '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:8px;vertical-align:middle"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg> 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
|
// "Config" group heading above the configuration categories (mirrors the
|
||||||
// "Tools" heading below).
|
// "Tools" heading below).
|
||||||
const configLabel = document.createElement('div');
|
const configLabel = document.createElement('div');
|
||||||
@ -118,20 +136,20 @@ class ConfigSidebar {
|
|||||||
});
|
});
|
||||||
self.categoriesList.appendChild(sshItem);
|
self.categoriesList.appendChild(sshItem);
|
||||||
|
|
||||||
const systemItem = document.createElement('div');
|
const peersItem = document.createElement('div');
|
||||||
systemItem.className = 'category';
|
peersItem.className = 'category';
|
||||||
systemItem.setAttribute('data-category', 'system');
|
peersItem.setAttribute('data-category', 'peers');
|
||||||
systemItem.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:8px;vertical-align:middle"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg> System';
|
peersItem.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:8px;vertical-align:middle"><circle cx="6" cy="7" r="3"></circle><circle cx="18" cy="7" r="3"></circle><path d="M9 17l3-3 3 3"></path><path d="M12 14v7"></path><path d="M6 10v3a3 3 0 003 3"></path><path d="M18 10v3a3 3 0 01-3 3"></path></svg> Peers';
|
||||||
systemItem.addEventListener('click', function () {
|
peersItem.addEventListener('click', function () {
|
||||||
window.history.pushState({}, '', window.adminPath('system'));
|
window.history.pushState({}, '', window.adminPath('peers'));
|
||||||
document.querySelectorAll('.category').forEach(function (item) { item.classList.remove('active'); });
|
document.querySelectorAll('.category').forEach(function (item) { item.classList.remove('active'); });
|
||||||
this.classList.add('active');
|
this.classList.add('active');
|
||||||
window.configCategory = 'system';
|
window.configCategory = 'peers';
|
||||||
if (window.configManager && typeof window.configManager.renderConfig === 'function') {
|
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
|
// Set initial active category
|
||||||
this.setActiveCategory(window.configCategory || 'overview');
|
this.setActiveCategory(window.configCategory || 'overview');
|
||||||
|
|||||||
@ -4,7 +4,12 @@
|
|||||||
// kind=backup-channel; the other kinds light up in Phase 3.
|
// kind=backup-channel; the other kinds light up in Phase 3.
|
||||||
|
|
||||||
class PeersPage {
|
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.peers = [];
|
||||||
this.backupLocations = []; // populated for the loc_idx dropdown
|
this.backupLocations = []; // populated for the loc_idx dropdown
|
||||||
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
|
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
|
||||||
|
|||||||
@ -77,7 +77,7 @@ class LibrePortalSPAClean {
|
|||||||
this.routes.set('/tasks*', () => this.handleTasks()); // Handle /tasks with query
|
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('/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('/peers*', () => this.handlePeers());
|
||||||
this.routes.set('/ssh', () => this.handleSsh()); // legacy → /admin/tools/ssh-access
|
this.routes.set('/ssh', () => this.handleSsh()); // legacy → /admin/tools/ssh-access
|
||||||
this.routes.set('/ssh*', () => this.handleSsh());
|
this.routes.set('/ssh*', () => this.handleSsh());
|
||||||
@ -275,19 +275,8 @@ class LibrePortalSPAClean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handlePeers() {
|
async handlePeers() {
|
||||||
try {
|
// Legacy /peers → Peers under the Admin area.
|
||||||
const html = await this.fetchContent('/html/peers-content.html');
|
this.navigate('/admin/tools/peers', true);
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSsh() {
|
async handleSsh() {
|
||||||
@ -533,6 +522,7 @@ class LibrePortalSPAClean {
|
|||||||
window.adminPath = function (category) {
|
window.adminPath = function (category) {
|
||||||
if (!category || category === 'overview') return '/admin';
|
if (!category || category === 'overview') return '/admin';
|
||||||
if (category === 'ssh-access') return '/admin/tools/ssh-access';
|
if (category === 'ssh-access') return '/admin/tools/ssh-access';
|
||||||
|
if (category === 'peers') return '/admin/tools/peers';
|
||||||
return '/admin/config/' + category;
|
return '/admin/config/' + category;
|
||||||
};
|
};
|
||||||
window.adminCategoryFromPath = function (pathname) {
|
window.adminCategoryFromPath = function (pathname) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user