secubox-openwrt/package/secubox/luci-app-config-vault/htdocs/luci-static/resources/view/config-vault/overview.js
CyberMind-FR c80b10f18d 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>
2026-03-15 15:30:32 +01:00

583 lines
25 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require dom';
'require poll';
'require rpc';
'require ui';
var callStatus = rpc.declare({
object: 'luci.config-vault',
method: 'status',
expect: {}
});
var callModules = rpc.declare({
object: 'luci.config-vault',
method: 'modules',
expect: {}
});
var callHistory = rpc.declare({
object: 'luci.config-vault',
method: 'history',
params: ['count'],
expect: {}
});
var callBackup = rpc.declare({
object: 'luci.config-vault',
method: 'backup',
params: ['module'],
expect: {}
});
var callPush = rpc.declare({
object: 'luci.config-vault',
method: 'push',
expect: {}
});
var callPull = rpc.declare({
object: 'luci.config-vault',
method: 'pull',
expect: {}
});
var callInit = rpc.declare({
object: 'luci.config-vault',
method: 'init',
expect: {}
});
var callExportClone = rpc.declare({
object: 'luci.config-vault',
method: 'export_clone',
params: ['path'],
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: {
purple: '#6366f1',
cyan: '#06b6d4',
green: '#22c55e',
orange: '#f97316',
red: '#ef4444',
yellow: '#f59e0b'
},
badge: function(text, color) {
return E('span', {
'style': 'display:inline-block;padding:0.25rem 0.75rem;background:' +
(this.colors[color] || color) + '20;color:' +
(this.colors[color] || color) + ';border-radius:20px;font-size:0.75rem;font-weight:600;'
}, text);
},
statCard: function(icon, value, label, color) {
return E('div', {
'class': 'cbi-section',
'style': 'background:linear-gradient(135deg,rgba(99,102,241,0.05),rgba(6,182,212,0.02));border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:1.25rem;text-align:center;min-width:140px;'
}, [
E('div', { 'style': 'font-size:1.75rem;margin-bottom:0.5rem;filter:drop-shadow(0 0 6px ' + (this.colors[color] || '#6366f1') + ');' }, icon),
E('div', { 'style': 'font-size:1.75rem;font-weight:700;color:#f0f2ff;' }, String(value)),
E('div', { 'style': 'font-size:0.7rem;color:#666;text-transform:uppercase;letter-spacing:0.05em;margin-top:0.25rem;' }, label)
]);
},
card: function(title, icon, content, badge) {
return E('div', {
'class': 'cbi-section',
'style': 'background:#151525;border:1px solid rgba(255,255,255,0.06);border-radius:16px;margin-bottom:1.5rem;overflow:hidden;'
}, [
E('div', {
'style': 'padding:1rem 1.5rem;border-bottom:1px solid rgba(255,255,255,0.06);display:flex;align-items:center;gap:0.75rem;'
}, [
E('span', { 'style': 'font-size:1.25rem;' }, icon || ''),
E('span', { 'style': 'font-size:1rem;font-weight:600;flex:1;color:#f0f2ff;' }, title),
badge ? this.badge(badge.text, badge.color) : ''
]),
E('div', { 'style': 'padding:1.5rem;' }, content)
]);
},
actionBtn: function(text, icon, color, onclick) {
return E('button', {
'class': 'cbi-button',
'style': 'background:' + (this.colors[color] || '#6366f1') + ';color:white;border:none;padding:0.5rem 1rem;border-radius:8px;cursor:pointer;display:inline-flex;align-items:center;gap:0.5rem;font-weight:500;transition:all 0.2s;',
'click': onclick
}, [icon ? E('span', {}, icon) : '', text]);
},
moduleRow: function(mod, onBackup) {
var statusColor = mod.enabled ? 'green' : 'orange';
var statusText = mod.enabled ? 'Active' : 'Disabled';
return E('tr', { 'style': 'border-bottom:1px solid rgba(255,255,255,0.06);' }, [
E('td', { 'style': 'padding:0.75rem;' }, [
E('div', { 'style': 'font-weight:600;color:#f0f2ff;' }, mod.name),
E('div', { 'style': 'font-size:0.75rem;color:#666;' }, mod.description || '')
]),
E('td', { 'style': 'padding:0.75rem;text-align:center;' }, this.badge(statusText, statusColor)),
E('td', { 'style': 'padding:0.75rem;text-align:center;color:#888;' }, String(mod.files || 0)),
E('td', { 'style': 'padding:0.75rem;text-align:center;font-size:0.8rem;color:#666;' },
mod.last_backup ? mod.last_backup.split('T')[0] : '-'),
E('td', { 'style': 'padding:0.75rem;text-align:right;' },
E('button', {
'class': 'cbi-button cbi-button-action',
'style': 'padding:0.25rem 0.75rem;font-size:0.75rem;',
'click': function() { onBackup(mod.name); }
}, 'Backup')
)
]);
},
commitRow: function(commit) {
return E('tr', { 'style': 'border-bottom:1px solid rgba(255,255,255,0.06);' }, [
E('td', { 'style': 'padding:0.5rem;' }, [
E('code', { 'style': 'font-size:0.75rem;background:rgba(99,102,241,0.1);padding:0.15rem 0.4rem;border-radius:4px;color:#6366f1;' }, commit.short)
]),
E('td', { 'style': 'padding:0.5rem;color:#f0f2ff;font-size:0.85rem;' }, commit.message),
E('td', { 'style': 'padding:0.5rem;color:#666;font-size:0.75rem;white-space:nowrap;' },
commit.date ? commit.date.split(' ')[0] : '')
]);
}
};
return view.extend({
load: function() {
return Promise.all([
callStatus(),
callModules(),
callHistory(10)
]);
},
handleBackup: function(module) {
var self = this;
ui.showModal('Backing up...', [
E('p', { 'class': 'spinning' }, 'Backing up ' + (module || 'all modules') + '...')
]);
callBackup(module || '').then(function(res) {
ui.hideModal();
if (res.success) {
ui.addNotification(null, E('p', {}, 'Backup completed successfully'), 'success');
self.load().then(function(data) {
dom.content(document.querySelector('.cbi-map'), self.renderContent(data));
});
} else {
ui.addNotification(null, E('p', {}, 'Backup failed: ' + (res.output || 'Unknown error')), 'error');
}
});
},
handlePush: function() {
var self = this;
ui.showModal('Pushing to Gitea...', [
E('p', { 'class': 'spinning' }, 'Syncing with remote repository...')
]);
callPush().then(function(res) {
ui.hideModal();
if (res.success) {
ui.addNotification(null, E('p', {}, 'Successfully pushed to Gitea'), 'success');
self.load().then(function(data) {
dom.content(document.querySelector('.cbi-map'), self.renderContent(data));
});
} else {
ui.addNotification(null, E('p', {}, 'Push failed: ' + (res.output || 'Check Gitea configuration')), 'error');
}
});
},
handlePull: function() {
var self = this;
ui.showModal('Pulling from Gitea...', [
E('p', { 'class': 'spinning' }, 'Fetching latest from repository...')
]);
callPull().then(function(res) {
ui.hideModal();
if (res.success) {
ui.addNotification(null, E('p', {}, 'Successfully pulled from Gitea'), 'success');
self.load().then(function(data) {
dom.content(document.querySelector('.cbi-map'), self.renderContent(data));
});
} else {
ui.addNotification(null, E('p', {}, 'Pull failed: ' + (res.output || 'Check network')), 'error');
}
});
},
handleInit: function() {
var self = this;
ui.showModal('Initializing Vault...', [
E('p', { 'class': 'spinning' }, 'Setting up configuration vault...')
]);
callInit().then(function(res) {
ui.hideModal();
if (res.success) {
ui.addNotification(null, E('p', {}, 'Vault initialized successfully'), 'success');
self.load().then(function(data) {
dom.content(document.querySelector('.cbi-map'), self.renderContent(data));
});
} else {
ui.addNotification(null, E('p', {}, 'Init failed: ' + (res.output || 'Unknown error')), 'error');
}
});
},
handleExportClone: function() {
var self = this;
var path = '/tmp/secubox-clone-' + new Date().toISOString().split('T')[0] + '.tar.gz';
ui.showModal('Creating Clone Package...', [
E('p', { 'class': 'spinning' }, 'Exporting configuration for deployment...')
]);
callExportClone(path).then(function(res) {
ui.hideModal();
if (res.success) {
ui.showModal('Clone Package Ready', [
E('div', { 'style': 'text-align:center;' }, [
E('div', { 'style': 'font-size:3rem;margin-bottom:1rem;' }, '📦'),
E('p', {}, 'Clone package created successfully!'),
E('p', { 'style': 'font-family:monospace;background:#0a0a0f;padding:0.5rem;border-radius:4px;margin:1rem 0;' }, res.path),
E('p', { 'style': 'color:#666;font-size:0.85rem;' }, 'Size: ' + Math.round((res.size || 0) / 1024) + ' KB'),
E('p', { 'style': 'margin-top:1rem;' }, [
E('a', {
'href': '/cgi-bin/luci/admin/system/flashops/backup?download=' + encodeURIComponent(res.path),
'class': 'cbi-button cbi-button-positive'
}, 'Download Clone')
])
]),
E('div', { 'class': 'right', 'style': 'margin-top:1rem;' }, [
E('button', {
'class': 'cbi-button',
'click': ui.hideModal
}, 'Close')
])
]);
} else {
ui.addNotification(null, E('p', {}, 'Export failed: ' + (res.output || 'Unknown error')), 'error');
}
});
},
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] || {};
var historyData = data[2] || {};
var modules = modulesData.modules || [];
var commits = historyData.commits || [];
var self = this;
// Stats cards
var statsRow = E('div', {
'style': 'display:flex;gap:1.5rem;flex-wrap:wrap;margin-bottom:2rem;justify-content:center;'
}, [
KissTheme.statCard('🔐', status.initialized ? 'Active' : 'Not Init', 'Vault Status', status.initialized ? 'green' : 'orange'),
KissTheme.statCard('📦', modules.length, 'Modules', 'purple'),
KissTheme.statCard('📝', status.total_commits || 0, 'Commits', 'cyan'),
KissTheme.statCard('⚠️', status.uncommitted || 0, 'Uncommitted', status.uncommitted > 0 ? 'yellow' : 'green')
]);
// Quick Actions - Backup & Sync
var actionsContent = E('div', {
'style': 'display:flex;gap:1rem;flex-wrap:wrap;'
}, [
KissTheme.actionBtn('Backup All', '💾', 'purple', function() { self.handleBackup(); }),
KissTheme.actionBtn('Push to Gitea', '', 'cyan', function() { self.handlePush(); }),
KissTheme.actionBtn('Pull from Gitea', '', 'green', function() { self.handlePull(); }),
KissTheme.actionBtn('Export Clone', '📦', 'orange', function() { self.handleExportClone(); }),
!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;'
}, [
E('thead', {}, [
E('tr', { 'style': 'border-bottom:2px solid rgba(255,255,255,0.1);' }, [
E('th', { 'style': 'padding:0.75rem;text-align:left;color:#888;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.05em;' }, 'Module'),
E('th', { 'style': 'padding:0.75rem;text-align:center;color:#888;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.05em;' }, 'Status'),
E('th', { 'style': 'padding:0.75rem;text-align:center;color:#888;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.05em;' }, 'Files'),
E('th', { 'style': 'padding:0.75rem;text-align:center;color:#888;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.05em;' }, 'Last Backup'),
E('th', { 'style': 'padding:0.75rem;text-align:right;color:#888;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.05em;' }, 'Actions')
])
]),
E('tbody', {}, modules.map(function(m) {
return KissTheme.moduleRow(m, function(name) { self.handleBackup(name); });
}))
]);
// History table
var historyTable = E('table', {
'style': 'width:100%;border-collapse:collapse;font-size:0.85rem;'
}, [
E('thead', {}, [
E('tr', { 'style': 'border-bottom:2px solid rgba(255,255,255,0.1);' }, [
E('th', { 'style': 'padding:0.5rem;text-align:left;color:#888;font-size:0.7rem;text-transform:uppercase;' }, 'Commit'),
E('th', { 'style': 'padding:0.5rem;text-align:left;color:#888;font-size:0.7rem;text-transform:uppercase;' }, 'Message'),
E('th', { 'style': 'padding:0.5rem;text-align:right;color:#888;font-size:0.7rem;text-transform:uppercase;' }, 'Date')
])
]),
E('tbody', {}, commits.length > 0 ? commits.map(function(c) {
return KissTheme.commitRow(c);
}) : E('tr', {}, E('td', { 'colspan': '3', 'style': 'padding:1rem;text-align:center;color:#666;' }, 'No commits yet')))
]);
// Git info
var gitInfo = status.initialized ? E('div', {
'style': 'display:grid;grid-template-columns:repeat(2,1fr);gap:1rem;font-size:0.85rem;'
}, [
E('div', {}, [
E('span', { 'style': 'color:#666;' }, 'Branch: '),
E('span', { 'style': 'color:#f0f2ff;' }, status.branch || 'main')
]),
E('div', {}, [
E('span', { 'style': 'color:#666;' }, 'Repository: '),
E('span', { 'style': 'color:#f0f2ff;' }, status.gitea_repo || 'Not configured')
]),
E('div', {}, [
E('span', { 'style': 'color:#666;' }, 'Last Commit: '),
E('code', { 'style': 'color:#6366f1;' }, status.last_commit || '-')
]),
E('div', {}, [
E('span', { 'style': 'color:#666;' }, 'Vault Path: '),
E('span', { 'style': 'color:#f0f2ff;font-family:monospace;' }, status.vault_path || '/srv/config-vault')
])
]) : E('p', { 'style': 'color:#f97316;' }, 'Vault not initialized. Click "Initialize Vault" to start.');
return E('div', {}, [
// Header
E('div', {
'style': 'text-align:center;padding:2rem;margin-bottom:2rem;background:linear-gradient(135deg,rgba(99,102,241,0.1),rgba(6,182,212,0.05));border-radius:20px;border:1px solid rgba(255,255,255,0.06);'
}, [
E('h1', {
'style': 'font-size:2rem;font-weight:800;background:linear-gradient(135deg,#6366f1,#06b6d4);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:0.5rem;'
}, '🔐 Configuration Vault'),
E('p', { 'style': 'color:rgba(240,242,255,0.6);' }, 'Versioned configuration backup with audit trail')
]),
statsRow,
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' }),
KissTheme.card('Change History', '📜', historyTable, { text: commits.length + ' recent', color: 'cyan' })
]);
},
render: function(data) {
var content = this.renderContent(data);
return E('div', { 'class': 'cbi-map' }, [
E('style', {}, [
'.cbi-section { margin: 0; background: transparent; }',
'.cbi-button:hover { opacity: 0.9; transform: translateY(-1px); }'
].join('\n')),
content
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});