librelad cfdd39386c 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>
2026-05-26 20:16:45 +01:00

174 lines
8.0 KiB
JavaScript
Executable File

// Config Sidebar - Handles sidebar population and navigation
class ConfigSidebar {
constructor() {
this.categoriesList = null;
}
populateSidebar() {
//console.log('ConfigSidebar: Populating sidebar with categories...');
this.categoriesList = document.getElementById('config-categories-list');
if (!this.categoriesList) {
console.error('ConfigSidebar: config-categories-list element not found');
return;
}
if (!window.configData || !window.configData.categories) {
console.error('ConfigSidebar: No config data available for sidebar');
return;
}
this.categoriesList.innerHTML = '';
// Overview — the Admin landing (an ops/health board, not a config form).
const overviewItem = document.createElement('div');
overviewItem.className = 'category';
overviewItem.setAttribute('data-category', 'overview');
overviewItem.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"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg> Overview';
overviewItem.addEventListener('click', function () {
window.history.pushState({}, '', window.adminPath('overview'));
document.querySelectorAll('.category').forEach(function (item) { item.classList.remove('active'); });
this.classList.add('active');
window.configCategory = 'overview';
if (window.configManager && typeof window.configManager.renderConfig === 'function') {
window.configManager.renderConfig('overview');
}
});
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
// "Tools" heading below).
const configLabel = document.createElement('div');
configLabel.className = 'sidebar-group-label';
configLabel.textContent = 'Config';
this.categoriesList.appendChild(configLabel);
// Convert categories object to array and sort by ORDER
const categoriesArray = Object.entries(window.configData.categories).map(([key, value]) => ({
id: key,
...value
}));
// Sort by ORDER if available, otherwise by title
categoriesArray.sort(function(a, b) {
const orderA = parseInt(a.order) || 999;
const orderB = parseInt(b.order) || 999;
return orderA - orderB;
});
var self = this; // Preserve 'this' context
categoriesArray.forEach(function(category) {
// Backup category has its own top-level page (/backup) which renders
// these same fields dynamically — hide it from the /config sidebar to
// avoid two surfaces for the same data.
if (category.id === 'backup') return;
const categoryItem = document.createElement('div');
categoryItem.className = 'category';
categoryItem.setAttribute('data-category', category.id);
// Use correct icon from our new structure
const iconName = category.icon || category.id;
const iconPath = '/icons/config/' + iconName + '.svg';
categoryItem.innerHTML = '<img src="' + iconPath + '" alt="' + category.title + '" style="width: 20px; height: 20px; margin-right: 8px;"/> ' + category.title;
categoryItem.addEventListener('click', function() {
// Update URL without full page reload
window.history.pushState({}, '', window.adminPath(category.id));
// Update active state
document.querySelectorAll('.category').forEach(function(item) {
item.classList.remove('active');
});
this.classList.add('active');
// Update global category and load dynamically
window.configCategory = category.id;
// Load config dynamically without page refresh
if (window.configManager && typeof window.configManager.renderConfig === 'function') {
window.configManager.renderConfig(category.id);
}
});
self.categoriesList.appendChild(categoryItem);
});
// Tools group — admin pages that live in this area but aren't config
// categories (rendered by their own controller, not the config form).
const toolsLabel = document.createElement('div');
toolsLabel.className = 'sidebar-group-label';
toolsLabel.textContent = 'Tools';
self.categoriesList.appendChild(toolsLabel);
const sshItem = document.createElement('div');
sshItem.className = 'category';
sshItem.setAttribute('data-category', 'ssh-access');
sshItem.innerHTML = '<img src="/icons/config/security.svg" alt="SSH Access" style="width: 20px; height: 20px; margin-right: 8px;"/> SSH Access';
sshItem.addEventListener('click', function () {
window.history.pushState({}, '', window.adminPath('ssh-access'));
document.querySelectorAll('.category').forEach(function (item) { item.classList.remove('active'); });
this.classList.add('active');
window.configCategory = 'ssh-access';
if (window.configManager && typeof window.configManager.renderConfig === 'function') {
window.configManager.renderConfig('ssh-access');
}
});
self.categoriesList.appendChild(sshItem);
const peersItem = document.createElement('div');
peersItem.className = 'category';
peersItem.setAttribute('data-category', 'peers');
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';
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 = 'peers';
if (window.configManager && typeof window.configManager.renderConfig === 'function') {
window.configManager.renderConfig('peers');
}
});
self.categoriesList.appendChild(peersItem);
// Set initial active category
this.setActiveCategory(window.configCategory || 'overview');
//console.log('ConfigSidebar: Sidebar populated with ' + categoriesArray.length + ' categories');
}
setActiveCategory(categoryId) {
// Update active state
document.querySelectorAll('.category').forEach(function(item) {
item.classList.remove('active');
});
var activeItem = document.querySelector('[data-category="' + categoryId + '"]');
if (activeItem) {
activeItem.classList.add('active');
}
}
}
// Export to global scope
window.ConfigSidebar = ConfigSidebar;