Merge claude/2
This commit is contained in:
commit
0a25bd5a28
@ -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