feat(luci): Add provisioning and ttyd deployment UI

Config Vault Dashboard (overview.js):
- "Device Provisioning" card with 3 action buttons
- Provision Remote: Modal to push clone to remote node
- Serve via HTTP: Generate clone for HTTP download
- Restore All: Confirmation modal to restore all modules
- RPC calls: provision, serve_clone, restore_all

RTTY Remote Dashboard (dashboard.js):
- "Deploy ttyd to All" global button in controls
- Per-node "ttyd" button in Connected Nodes table
- Confirmation modal for bulk deployment
- Progress spinner and result display
- RPC calls: deploy_ttyd, install_remote

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-03-15 15:30:32 +01:00
parent e86545bd3a
commit c80b10f18d
4 changed files with 293 additions and 6 deletions

View File

@ -5253,3 +5253,9 @@ git checkout HEAD -- index.html
- CLI commands: restore-all, provision, pull-config, serve-clone - 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 - 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 - 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

View File

@ -10,6 +10,20 @@ _Last updated: 2026-03-16 (DPI LAN Passive Analysis)_
### 2026-03-16 ### 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)** - **Device Provisioning System (Complete)**
- **Auto-Restore**: `configvaultctl import-clone <file> --apply` auto-restores all modules - **Auto-Restore**: `configvaultctl import-clone <file> --apply` auto-restores all modules
- **Remote Provisioning**: `configvaultctl provision <node|all>` pushes clone to remote nodes - **Remote Provisioning**: `configvaultctl provision <node|all>` pushes clone to remote nodes
@ -645,8 +659,7 @@ _Last updated: 2026-03-16 (DPI LAN Passive Analysis)_
### v1.0 Release Prep ### v1.0 Release Prep
1. **LuCI Provisioning Dashboard** - Add provisioning UI to Config Vault dashboard (optional) All core features complete. Optional polish tasks remain.
2. **LuCI Remote Install Button** - Add "Deploy ttyd" action to Remote Control dashboard (optional)
### v1.1+ Extended Mesh ### v1.1+ Extended Mesh

View File

@ -56,6 +56,26 @@ var callExportClone = rpc.declare({
expect: {} 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 // KissTheme helper
var KissTheme = { var KissTheme = {
colors: { 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) { renderContent: function(data) {
var status = data[0] || {}; var status = data[0] || {};
var modulesData = data[1] || {}; var modulesData = data[1] || {};
@ -287,7 +463,7 @@ return view.extend({
KissTheme.statCard('⚠️', status.uncommitted || 0, 'Uncommitted', status.uncommitted > 0 ? 'yellow' : 'green') KissTheme.statCard('⚠️', status.uncommitted || 0, 'Uncommitted', status.uncommitted > 0 ? 'yellow' : 'green')
]); ]);
// Quick Actions // Quick Actions - Backup & Sync
var actionsContent = E('div', { var actionsContent = E('div', {
'style': 'display:flex;gap:1rem;flex-wrap:wrap;' '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(); }) : '' !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 // Modules table
var modulesTable = E('table', { var modulesTable = E('table', {
'style': 'width:100%;border-collapse:collapse;font-size:0.9rem;' 'style': 'width:100%;border-collapse:collapse;font-size:0.9rem;'
@ -369,6 +554,8 @@ return view.extend({
KissTheme.card('Quick Actions', '⚡', actionsContent), KissTheme.card('Quick Actions', '⚡', actionsContent),
KissTheme.card('Device Provisioning', '🚀', provisionContent),
KissTheme.card('Repository Info', '📊', gitInfo), KissTheme.card('Repository Info', '📊', gitInfo),
KissTheme.card('Modules', '📦', modulesTable, { text: modules.length + ' configured', color: 'purple' }), KissTheme.card('Modules', '📦', modulesTable, { text: modules.length + ' configured', color: 'purple' }),

View File

@ -51,6 +51,20 @@ var callRpcCall = rpc.declare({
expect: {} 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({ return view.extend({
handleSaveApply: null, handleSaveApply: null,
handleSave: null, handleSave: null,
@ -124,7 +138,13 @@ return view.extend({
'class': 'kiss-btn', 'class': 'kiss-btn',
'style': 'padding: 4px 10px; font-size: 11px;', 'style': 'padding: 4px 10px; font-size: 11px;',
'click': function() { self.handleConnect(node); } '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 self = this;
var isRunning = status.running; 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', { isRunning ? E('button', {
'class': 'kiss-btn kiss-btn-red', 'class': 'kiss-btn kiss-btn-red',
'click': function() { self.handleServerStop(); } 'click': function() { self.handleServerStop(); }
}, 'Stop Server') : E('button', { }, 'Stop Server') : E('button', {
'class': 'kiss-btn kiss-btn-green', 'class': 'kiss-btn kiss-btn-green',
'click': function() { self.handleServerStart(); } '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) { render: function(data) {
var self = this; var self = this;
var status = data[0] || {}; var status = data[0] || {};