diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 57cc515e..2931e61d 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -5253,3 +5253,9 @@ git checkout HEAD -- index.html - CLI commands: restore-all, provision, pull-config, serve-clone - 6 new RPCD methods: restore_all, import_apply, provision, pull_config, export_clone_b64, serve_clone - Use case: Zero-touch provisioning of new SecuBox devices from master configuration + +- **LuCI Provisioning & ttyd Deployment UI (Complete)** + - Config Vault Dashboard: "Device Provisioning" card with Provision Remote, Serve via HTTP, Restore All buttons + - RTTY Remote Dashboard: "Deploy ttyd to All" button and per-node ttyd button in actions column + - Modal dialogs for confirmation, progress, and result display + - Full mesh provisioning workflow now accessible via web UI diff --git a/.claude/WIP.md b/.claude/WIP.md index a4b0755a..40369e66 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -10,6 +10,20 @@ _Last updated: 2026-03-16 (DPI LAN Passive Analysis)_ ### 2026-03-16 +- **LuCI Provisioning Dashboard (Complete)** + - Config Vault dashboard: "Device Provisioning" card with 3 action buttons + - "Provision Remote" - Modal dialog to push clone to remote node + - "Serve via HTTP" - Generate clone for HTTP download, shows URL + - "Restore All" - Confirmation modal to restore all modules from vault + - Full provisioning workflow accessible from web UI + +- **LuCI Deploy ttyd Button (Complete)** + - RTTY Remote Control dashboard: "Deploy ttyd to All" global button + - Per-node "ttyd" button in Connected Nodes table + - Confirmation modal for bulk deployment + - Progress spinner and result display + - Enables web terminal deployment to mesh nodes via UI + - **Device Provisioning System (Complete)** - **Auto-Restore**: `configvaultctl import-clone --apply` auto-restores all modules - **Remote Provisioning**: `configvaultctl provision ` pushes clone to remote nodes @@ -645,8 +659,7 @@ _Last updated: 2026-03-16 (DPI LAN Passive Analysis)_ ### v1.0 Release Prep -1. **LuCI Provisioning Dashboard** - Add provisioning UI to Config Vault dashboard (optional) -2. **LuCI Remote Install Button** - Add "Deploy ttyd" action to Remote Control dashboard (optional) +All core features complete. Optional polish tasks remain. ### v1.1+ Extended Mesh diff --git a/package/secubox/luci-app-config-vault/htdocs/luci-static/resources/view/config-vault/overview.js b/package/secubox/luci-app-config-vault/htdocs/luci-static/resources/view/config-vault/overview.js index 9a6ea6d4..db9d26ee 100644 --- a/package/secubox/luci-app-config-vault/htdocs/luci-static/resources/view/config-vault/overview.js +++ b/package/secubox/luci-app-config-vault/htdocs/luci-static/resources/view/config-vault/overview.js @@ -56,6 +56,26 @@ var callExportClone = rpc.declare({ expect: {} }); +var callProvision = rpc.declare({ + object: 'luci.config-vault', + method: 'provision', + params: ['target', 'clone_file'], + expect: {} +}); + +var callServeClone = rpc.declare({ + object: 'luci.config-vault', + method: 'serve_clone', + params: ['output_dir'], + expect: {} +}); + +var callRestoreAll = rpc.declare({ + object: 'luci.config-vault', + method: 'restore_all', + expect: {} +}); + // KissTheme helper var KissTheme = { colors: { @@ -267,6 +287,162 @@ return view.extend({ }); }, + handleProvision: function() { + var self = this; + var targetInput; + + ui.showModal('Provision Remote Node', [ + E('div', { 'style': 'margin-bottom:1rem;' }, [ + E('p', { 'style': 'margin-bottom:0.5rem;color:#888;' }, 'Push configuration clone to a remote SecuBox node:'), + E('div', { 'style': 'display:flex;gap:0.5rem;align-items:center;' }, [ + targetInput = E('input', { + 'type': 'text', + 'placeholder': 'IP address or hostname (e.g., 192.168.255.2)', + 'style': 'flex:1;padding:0.5rem;border-radius:6px;border:1px solid rgba(255,255,255,0.1);background:#0a0a0f;color:#f0f2ff;' + }) + ]), + E('p', { 'style': 'margin-top:0.5rem;font-size:0.75rem;color:#666;' }, + 'Enter "all" to provision all mesh nodes.') + ]), + E('div', { 'class': 'right', 'style': 'margin-top:1rem;display:flex;gap:0.5rem;justify-content:flex-end;' }, [ + E('button', { + 'class': 'cbi-button', + 'click': ui.hideModal + }, 'Cancel'), + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': function() { + var target = targetInput.value.trim(); + if (!target) { + ui.addNotification(null, E('p', {}, 'Please enter a target node'), 'error'); + return; + } + ui.hideModal(); + self.doProvision(target); + } + }, 'Provision') + ]) + ]); + }, + + doProvision: function(target) { + var self = this; + + ui.showModal('Provisioning ' + target + '...', [ + E('p', { 'class': 'spinning' }, 'Creating clone and pushing to remote node...') + ]); + + callProvision(target, '').then(function(res) { + ui.hideModal(); + if (res.success) { + ui.showModal('Provisioning Complete', [ + E('div', { 'style': 'text-align:center;' }, [ + E('div', { 'style': 'font-size:3rem;margin-bottom:1rem;' }, '🚀'), + E('p', {}, 'Configuration pushed to ' + target), + E('pre', { + 'style': 'text-align:left;background:#0a0a0f;padding:1rem;border-radius:8px;max-height:200px;overflow-y:auto;font-size:0.75rem;color:#888;' + }, res.output || 'Provisioning successful') + ]), + E('div', { 'class': 'right', 'style': 'margin-top:1rem;' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Close') + ]) + ]); + } else { + ui.addNotification(null, E('p', {}, 'Provisioning failed: ' + (res.error || res.output || 'Unknown error')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Provisioning error: ' + err.message), 'error'); + }); + }, + + handleServeClone: function() { + var self = this; + + ui.showModal('Serving Clone via HTTP...', [ + E('p', { 'class': 'spinning' }, 'Generating clone for HTTP download...') + ]); + + callServeClone('/www/config-vault').then(function(res) { + ui.hideModal(); + if (res.success) { + var hostname = window.location.hostname; + var cloneUrl = 'http://' + hostname + '/config-vault/clone.tar.gz'; + + ui.showModal('Clone Available via HTTP', [ + E('div', { 'style': 'text-align:center;' }, [ + E('div', { 'style': 'font-size:3rem;margin-bottom:1rem;' }, '🌐'), + E('p', {}, 'Clone is now available for HTTP download:'), + E('p', { + 'style': 'font-family:monospace;background:#0a0a0f;padding:0.75rem;border-radius:6px;margin:1rem 0;word-break:break-all;color:#06b6d4;' + }, cloneUrl), + E('p', { 'style': 'font-size:0.8rem;color:#888;margin-top:1rem;' }, [ + 'New devices can pull this config with:', + E('br'), + E('code', { 'style': 'color:#6366f1;' }, 'configvaultctl pull-config ' + hostname) + ]) + ]), + E('div', { 'class': 'right', 'style': 'margin-top:1rem;' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Close') + ]) + ]); + } else { + ui.addNotification(null, E('p', {}, 'Serve failed: ' + (res.output || 'Unknown error')), 'error'); + } + }); + }, + + handleRestoreAll: function() { + var self = this; + + ui.showModal('Confirm Restore All', [ + E('div', { 'style': 'text-align:center;' }, [ + E('div', { 'style': 'font-size:3rem;margin-bottom:1rem;' }, '⚠️'), + E('p', { 'style': 'color:#f97316;font-weight:600;' }, 'This will restore ALL modules from the vault!'), + E('p', { 'style': 'color:#888;' }, 'Current configurations will be overwritten.') + ]), + E('div', { 'class': 'right', 'style': 'margin-top:1rem;display:flex;gap:0.5rem;justify-content:flex-end;' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'click': function() { + ui.hideModal(); + self.doRestoreAll(); + } + }, 'Restore All') + ]) + ]); + }, + + doRestoreAll: function() { + var self = this; + + ui.showModal('Restoring All Modules...', [ + E('p', { 'class': 'spinning' }, 'Applying configurations from vault...') + ]); + + callRestoreAll().then(function(res) { + ui.hideModal(); + if (res.success) { + ui.showModal('Restore Complete', [ + E('div', { 'style': 'text-align:center;' }, [ + E('div', { 'style': 'font-size:3rem;margin-bottom:1rem;' }, '✅'), + E('p', {}, 'All modules restored successfully!'), + E('p', { 'style': 'color:#888;font-size:0.85rem;' }, 'A reboot may be required to apply all changes.'), + E('pre', { + 'style': 'text-align:left;background:#0a0a0f;padding:1rem;border-radius:8px;max-height:150px;overflow-y:auto;font-size:0.75rem;color:#888;' + }, res.output || '') + ]), + E('div', { 'class': 'right', 'style': 'margin-top:1rem;' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Close') + ]) + ]); + } else { + ui.addNotification(null, E('p', {}, 'Restore failed: ' + (res.output || 'Unknown error')), 'error'); + } + }); + }, + renderContent: function(data) { var status = data[0] || {}; var modulesData = data[1] || {}; @@ -287,7 +463,7 @@ return view.extend({ KissTheme.statCard('⚠️', status.uncommitted || 0, 'Uncommitted', status.uncommitted > 0 ? 'yellow' : 'green') ]); - // Quick Actions + // Quick Actions - Backup & Sync var actionsContent = E('div', { 'style': 'display:flex;gap:1rem;flex-wrap:wrap;' }, [ @@ -298,6 +474,15 @@ return view.extend({ !status.initialized ? KissTheme.actionBtn('Initialize Vault', '🚀', 'red', function() { self.handleInit(); }) : '' ]); + // Provisioning Actions + var provisionContent = E('div', { + 'style': 'display:flex;gap:1rem;flex-wrap:wrap;' + }, [ + KissTheme.actionBtn('Provision Remote', '🚀', 'cyan', function() { self.handleProvision(); }), + KissTheme.actionBtn('Serve via HTTP', '🌐', 'green', function() { self.handleServeClone(); }), + KissTheme.actionBtn('Restore All', '🔄', 'orange', function() { self.handleRestoreAll(); }) + ]); + // Modules table var modulesTable = E('table', { 'style': 'width:100%;border-collapse:collapse;font-size:0.9rem;' @@ -369,6 +554,8 @@ return view.extend({ KissTheme.card('Quick Actions', '⚡', actionsContent), + KissTheme.card('Device Provisioning', '🚀', provisionContent), + KissTheme.card('Repository Info', '📊', gitInfo), KissTheme.card('Modules', '📦', modulesTable, { text: modules.length + ' configured', color: 'purple' }), diff --git a/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/dashboard.js b/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/dashboard.js index 73f53f10..091b4811 100644 --- a/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/dashboard.js +++ b/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/dashboard.js @@ -51,6 +51,20 @@ var callRpcCall = rpc.declare({ expect: {} }); +var callDeployTtyd = rpc.declare({ + object: 'luci.rtty-remote', + method: 'deploy_ttyd', + params: ['target'], + expect: {} +}); + +var callInstallRemote = rpc.declare({ + object: 'luci.rtty-remote', + method: 'install_remote', + params: ['node_id', 'app_id'], + expect: {} +}); + return view.extend({ handleSaveApply: null, handleSave: null, @@ -124,7 +138,13 @@ return view.extend({ 'class': 'kiss-btn', 'style': 'padding: 4px 10px; font-size: 11px;', 'click': function() { self.handleConnect(node); } - }, 'Term') + }, 'Term'), + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'style': 'padding: 4px 10px; font-size: 11px;', + 'title': 'Deploy ttyd web terminal', + 'click': function() { self.handleDeployTtydToNode(node); } + }, 'ttyd') ])) ]); })) @@ -203,14 +223,18 @@ return view.extend({ var self = this; var isRunning = status.running; - return E('div', { 'style': 'display: flex; gap: 12px;' }, [ + return E('div', { 'style': 'display: flex; gap: 12px; flex-wrap: wrap;' }, [ isRunning ? E('button', { 'class': 'kiss-btn kiss-btn-red', 'click': function() { self.handleServerStop(); } }, 'Stop Server') : E('button', { 'class': 'kiss-btn kiss-btn-green', 'click': function() { self.handleServerStart(); } - }, 'Start Server') + }, 'Start Server'), + E('button', { + 'class': 'kiss-btn kiss-btn-blue', + 'click': function() { self.handleDeployTtydAll(); } + }, '🖥️ Deploy ttyd to All') ]); }, @@ -284,6 +308,63 @@ return view.extend({ }); }, + handleDeployTtyd: function(target) { + var self = this; + var targetName = target || 'all nodes'; + + ui.showModal('Deploying ttyd...', [ + E('p', { 'class': 'spinning' }, 'Installing ttyd on ' + targetName + '...') + ]); + + callDeployTtyd(target || 'all').then(function(response) { + ui.hideModal(); + if (response.success) { + ui.showModal('ttyd Deployed', [ + E('div', { 'style': 'text-align: center;' }, [ + E('div', { 'style': 'font-size: 48px; margin-bottom: 16px;' }, '🖥️'), + E('p', { 'style': 'margin-bottom: 12px;' }, 'ttyd web terminal deployed successfully!'), + E('pre', { + 'style': 'text-align: left; background: var(--kiss-bg); padding: 12px; border-radius: 6px; font-size: 12px; max-height: 200px; overflow-y: auto; color: var(--kiss-muted);' + }, response.output || 'Deployment complete') + ]), + E('div', { 'style': 'text-align: right; margin-top: 16px;' }, [ + E('button', { 'class': 'kiss-btn', 'click': ui.hideModal }, 'Close') + ]) + ]); + } else { + ui.addNotification(null, E('p', 'Deployment failed: ' + (response.error || response.output || 'Unknown error')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', 'Deployment error: ' + err.message), 'error'); + }); + }, + + handleDeployTtydToNode: function(node) { + this.handleDeployTtyd(node.address || node.id); + }, + + handleDeployTtydAll: function() { + var self = this; + ui.showModal('Confirm Deploy All', [ + E('div', { 'style': 'text-align: center;' }, [ + E('div', { 'style': 'font-size: 48px; margin-bottom: 16px;' }, '🖥️'), + E('p', {}, 'Deploy ttyd web terminal to ALL mesh nodes?'), + E('p', { 'style': 'color: var(--kiss-muted); font-size: 12px;' }, 'This will install ttyd and start the service on each node.') + ]), + E('div', { 'style': 'display: flex; gap: 12px; justify-content: center; margin-top: 16px;' }, [ + E('button', { 'class': 'kiss-btn', 'click': ui.hideModal }, 'Cancel'), + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'click': function() { + ui.hideModal(); + self.handleDeployTtyd('all'); + } + }, 'Deploy All') + ]) + ]); + }, + render: function(data) { var self = this; var status = data[0] || {};