diff --git a/containers/libreportal/frontend/css/ssh.css b/containers/libreportal/frontend/css/ssh.css
index 078f268..0b55c7d 100644
--- a/containers/libreportal/frontend/css/ssh.css
+++ b/containers/libreportal/frontend/css/ssh.css
@@ -4,7 +4,19 @@
.ssh-page {
max-width: 860px;
margin: 0 auto;
- padding: 24px 20px 40px;
+ padding: 8px 4px 40px;
+}
+
+/* "Tools" group heading in the Admin (config) sidebar, above SSH Access. */
+.sidebar-group-label {
+ margin: 14px 8px 4px;
+ padding-top: 10px;
+ border-top: 1px solid rgba(var(--text-rgb), 0.08);
+ font-size: 0.7rem;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: rgba(var(--text-rgb), 0.45);
}
.ssh-page-header {
diff --git a/containers/libreportal/frontend/html/ssh-content.html b/containers/libreportal/frontend/html/ssh-content.html
deleted file mode 100644
index 1ba6ee1..0000000
--- a/containers/libreportal/frontend/html/ssh-content.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
diff --git a/containers/libreportal/frontend/js/components/config/config-manager.js b/containers/libreportal/frontend/js/components/config/config-manager.js
index 260042d..ae56e75 100755
--- a/containers/libreportal/frontend/js/components/config/config-manager.js
+++ b/containers/libreportal/frontend/js/components/config/config-manager.js
@@ -25,6 +25,19 @@ if (typeof window.ConfigManager === 'undefined') {
return;
}
+ // SSH Access is an admin tool page that lives in this sidebar rather than
+ // a config category — render its own controller into the main pane.
+ if (category === 'ssh-access') {
+ try { this.sidebar.populateSidebar(); } catch (e) {}
+ if (typeof SshPage !== 'undefined') {
+ window.sshPage = new SshPage('config-section');
+ await window.sshPage.init();
+ } else {
+ configSection.innerHTML = '
SSH Access page failed to load.
';
+ }
+ return;
+ }
+
try {
// Show loading state with enhanced box styling
configSection.innerHTML = `
diff --git a/containers/libreportal/frontend/js/components/config/config-sidebar.js b/containers/libreportal/frontend/js/components/config/config-sidebar.js
index 9da7d3c..86ce36a 100755
--- a/containers/libreportal/frontend/js/components/config/config-sidebar.js
+++ b/containers/libreportal/frontend/js/components/config/config-sidebar.js
@@ -73,7 +73,29 @@ class ConfigSidebar {
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 = '

SSH Access';
+ sshItem.addEventListener('click', function () {
+ window.history.pushState({}, '', '/config?=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);
+
// Set initial active category
this.setActiveCategory(window.configCategory || 'general');
diff --git a/containers/libreportal/frontend/js/components/ssh/ssh-page.js b/containers/libreportal/frontend/js/components/ssh/ssh-page.js
index 3f0afe4..735ff32 100644
--- a/containers/libreportal/frontend/js/components/ssh/ssh-page.js
+++ b/containers/libreportal/frontend/js/components/ssh/ssh-page.js
@@ -1,15 +1,22 @@
-// SSH Access page — manage inbound admin SSH to this host: authorize public
-// keys (paste to grant access), remove them, and toggle password login behind
-// the backend's lockout guard. Reads /data/ssh/access.json; all mutations run
-// as `libreportal ssh ...` tasks. LibrePortal never handles a private key here.
+// SSH Access — inbound admin SSH to this host. Lives in the Admin area (a
+// sidebar item on the Config/Admin page) and renders into whatever container
+// it's given (defaults to #config-section). Authorize public keys (paste to
+// grant access), remove them, and toggle password login behind the backend's
+// lockout guard. Reads /data/ssh/access.json; mutations run as
+// `libreportal ssh ...` tasks. LibrePortal never handles a private key here.
class SshPage {
- constructor() {
+ constructor(rootId = 'config-section') {
+ this.rootId = rootId;
this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
this.data = null;
this._bound = false;
}
+ root() { return document.getElementById(this.rootId); }
+
async init() {
+ const r = this.root();
+ if (r) r.innerHTML = '
';
this.bindEvents();
await this.refresh();
this.render();
@@ -48,10 +55,10 @@ class SshPage {
}
render() {
- const root = document.getElementById('ssh-page-root');
+ const root = this.root();
if (!root) return;
if (!this.data) {
- root.innerHTML = `
Couldn't load SSH access data.
`;
+ root.innerHTML = `
Couldn't load SSH access data.
`;
return;
}
const d = this.data;
@@ -69,32 +76,39 @@ class SshPage {
`).join('') : `No keys authorized yet — add one below to allow key-based login.
`;
root.innerHTML = `
-