From fdc746763022c2d06b856bc7fcf88b0387cb0603 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Thu, 12 Feb 2026 07:36:36 +0100 Subject: [PATCH] feat(kiss): Add sub-tabs navigation and fix Streamlit reupload KISS Theme: - Add expandable sub-tabs under active sidebar items - Apps with multiple views show nested tabs when active - Support for CrowdSec, HAProxy, WireGuard, Ollama, Tor Shield, CDN Cache, InterceptoR, mitmproxy, Client Guardian Cloner: - Full KISS theme rewrite with stats grid, quick actions - TFTP boot commands with copy button - Progress tracking for image builds Streamlit: - Fix reupload not applying changes - auto-restart service after upload - Show "Restarting..." spinner during service reload Co-Authored-By: Claude Opus 4.5 --- .../resources/view/cloner/overview.js | 481 +++++++++++------- .../resources/secubox/kiss-theme.js | 169 ++++-- .../resources/view/streamlit/dashboard.js | 13 +- 3 files changed, 432 insertions(+), 231 deletions(-) diff --git a/package/secubox/luci-app-cloner/htdocs/luci-static/resources/view/cloner/overview.js b/package/secubox/luci-app-cloner/htdocs/luci-static/resources/view/cloner/overview.js index 0d97c12a..7605e3ce 100644 --- a/package/secubox/luci-app-cloner/htdocs/luci-static/resources/view/cloner/overview.js +++ b/package/secubox/luci-app-cloner/htdocs/luci-static/resources/view/cloner/overview.js @@ -64,187 +64,259 @@ var callDeleteToken = rpc.declare({ params: ['token'] }); +var callGetBuildProgress = rpc.declare({ + object: 'luci.cloner', + method: 'build_progress', + expect: { } +}); + +function fmtSize(bytes) { + if (!bytes) return '-'; + var u = ['B', 'KB', 'MB', 'GB']; + var i = 0; + while (bytes >= 1024 && i < u.length - 1) { bytes /= 1024; i++; } + return bytes.toFixed(1) + ' ' + u[i]; +} + +function fmtDate(iso) { + if (!iso) return '-'; + var d = new Date(iso); + return d.toLocaleDateString() + ' ' + d.toLocaleTimeString().slice(0, 5); +} + return view.extend({ + status: {}, + images: [], + tokens: [], + clones: [], + devices: [], + buildProgress: null, + load: function() { return Promise.all([ callGetStatus(), callListImages(), callListTokens(), callListClones(), - callListDevices() + callListDevices(), + callGetBuildProgress().catch(function() { return {}; }) ]); }, render: function(data) { - var status = data[0] || {}; - var images = data[1].images || []; - var tokens = data[2].tokens || []; - var clones = data[3].clones || []; - var devices = data[4].devices || []; + var self = this; + this.status = data[0] || {}; + this.images = data[1].images || []; + this.tokens = data[2].tokens || []; + this.clones = data[3].clones || []; + this.devices = data[4].devices || []; + this.buildProgress = data[5] || {}; - var view = E('div', { 'class': 'cbi-map' }, [ - E('h2', {}, 'Cloning Station'), - E('div', { 'class': 'cbi-map-descr' }, 'Build and deploy SecuBox clone images to new devices'), - - // Status Cards - E('div', { 'style': 'display:flex;gap:20px;margin:20px 0;flex-wrap:wrap;' }, [ - E('div', { 'style': 'padding:15px;background:#3b82f622;border-radius:8px;min-width:120px;' }, [ - E('div', { 'style': 'font-size:12px;color:#888;' }, 'Device'), - E('strong', { 'style': 'font-size:16px;color:#3b82f6;' }, status.device_type || 'unknown') + var content = [ + // Header + E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;' }, [ + E('div', {}, [ + E('h1', { 'style': 'font-size:28px;font-weight:700;margin:0;display:flex;align-items:center;gap:12px;' }, [ + '🔄 Cloning Station' + ]), + E('p', { 'style': 'color:var(--kiss-muted);margin:6px 0 0;' }, 'Build and deploy SecuBox clone images') ]), - E('div', { 'style': 'padding:15px;border-radius:8px;min-width:120px;', 'class': status.tftp_running ? 'tftp-on' : 'tftp-off' }, [ - E('div', { 'style': 'font-size:12px;color:#888;' }, 'TFTP'), - E('strong', { 'style': 'font-size:16px;', 'id': 'tftp-status' }, - status.tftp_running ? 'Running' : 'Stopped') - ]), - E('div', { 'style': 'padding:15px;background:#8b5cf622;border-radius:8px;min-width:120px;' }, [ - E('div', { 'style': 'font-size:12px;color:#888;' }, 'Tokens'), - E('strong', { 'style': 'font-size:24px;color:#8b5cf6;', 'id': 'token-count' }, - String(tokens.length)) - ]), - E('div', { 'style': 'padding:15px;background:#22c55e22;border-radius:8px;min-width:120px;' }, [ - E('div', { 'style': 'font-size:12px;color:#888;' }, 'Clones'), - E('strong', { 'style': 'font-size:24px;color:#22c55e;', 'id': 'clone-count' }, - String(status.clone_count || 0)) + E('div', { 'style': 'display:flex;gap:8px;' }, [ + KissTheme.badge(this.status.device_type || 'unknown', 'blue'), + KissTheme.badge(this.status.tftp_running ? 'TFTP ON' : 'TFTP OFF', + this.status.tftp_running ? 'green' : 'red') ]) ]), + // Stats Grid + E('div', { 'class': 'kiss-grid kiss-grid-4', 'id': 'stats-grid', 'style': 'margin-bottom:24px;' }, [ + KissTheme.stat(this.images.length, 'Images', 'var(--kiss-blue)'), + KissTheme.stat(this.tokens.length, 'Tokens', 'var(--kiss-purple)'), + KissTheme.stat(this.status.clone_count || 0, 'Clones', 'var(--kiss-green)'), + KissTheme.stat(this.status.tftp_running ? 'Active' : 'Idle', 'TFTP', this.status.tftp_running ? 'var(--kiss-green)' : 'var(--kiss-muted)') + ]), + // Quick Actions - E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, 'Quick Actions'), - E('div', { 'style': 'display:flex;gap:10px;flex-wrap:wrap;' }, [ - this.createActionButton('Build Image', 'cbi-button-action', L.bind(this.handleBuild, this)), - this.createActionButton(status.tftp_running ? 'Stop TFTP' : 'Start TFTP', - status.tftp_running ? 'cbi-button-negative' : 'cbi-button-positive', - L.bind(this.handleTftp, this, !status.tftp_running)), - this.createActionButton('New Token', 'cbi-button-action', L.bind(this.handleNewToken, this)), - this.createActionButton('Auto-Approve Token', 'cbi-button-save', L.bind(this.handleAutoToken, this)) - ]) + KissTheme.card([ + E('span', {}, '⚡ Quick Actions') + ], E('div', { 'style': 'display:flex;gap:12px;flex-wrap:wrap;' }, [ + E('button', { + 'class': 'kiss-btn kiss-btn-blue', + 'click': function() { self.handleBuild(); } + }, ['🔨 ', 'Build Image']), + E('button', { + 'class': 'kiss-btn ' + (this.status.tftp_running ? 'kiss-btn-red' : 'kiss-btn-green'), + 'click': function() { self.handleTftp(!self.status.tftp_running); } + }, [this.status.tftp_running ? 'âšī¸ Stop TFTP' : 'â–ļī¸ Start TFTP']), + E('button', { + 'class': 'kiss-btn', + 'click': function() { self.handleNewToken(); } + }, ['đŸŽŸī¸ ', 'New Token']), + E('button', { + 'class': 'kiss-btn kiss-btn-green', + 'click': function() { self.handleAutoToken(); } + }, ['✅ ', 'Auto-Approve Token']) + ])), + + // Build Progress (if building) + this.buildProgress.building ? this.renderBuildProgress() : null, + + // Two column layout + E('div', { 'class': 'kiss-grid kiss-grid-2', 'style': 'margin-top:16px;' }, [ + // Clone Images + KissTheme.card([ + E('span', {}, '💾 Clone Images'), + E('span', { 'style': 'margin-left:auto;font-size:12px;color:var(--kiss-muted);' }, this.images.length + ' available') + ], E('div', { 'id': 'images-container' }, this.renderImages())), + + // Tokens + KissTheme.card([ + E('span', {}, 'đŸŽŸī¸ Clone Tokens'), + E('span', { 'style': 'margin-left:auto;font-size:12px;color:var(--kiss-muted);' }, this.tokens.length + ' active') + ], E('div', { 'id': 'tokens-container' }, this.renderTokens())) ]), - // Clone Images - E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, 'Clone Images'), - E('table', { 'class': 'table', 'id': 'images-table' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th' }, 'Device'), - E('th', { 'class': 'th' }, 'Name'), - E('th', { 'class': 'th' }, 'Size'), - E('th', { 'class': 'th' }, 'TFTP Ready'), - E('th', { 'class': 'th' }, 'Actions') - ]) - ].concat(images.length > 0 ? images.map(L.bind(this.renderImageRow, this)) : - [E('tr', { 'class': 'tr' }, [E('td', { 'class': 'td', 'colspan': 5, 'style': 'text-align:center;' }, - 'No images available. Click "Build Image" to create one.')])] - )) - ]), - - // Tokens - E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, 'Clone Tokens'), - E('table', { 'class': 'table', 'id': 'tokens-table' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th' }, 'Token'), - E('th', { 'class': 'th' }, 'Created'), - E('th', { 'class': 'th' }, 'Type'), - E('th', { 'class': 'th' }, 'Actions') - ]) - ].concat(tokens.length > 0 ? tokens.map(L.bind(this.renderTokenRow, this)) : - [E('tr', { 'class': 'tr' }, [E('td', { 'class': 'td', 'colspan': 4, 'style': 'text-align:center;' }, - 'No tokens. Click "New Token" to generate one.')])] - )) - ]), - - // TFTP Instructions - status.tftp_running ? E('div', { 'class': 'cbi-section', 'style': 'background:#22c55e11;padding:15px;border-radius:8px;border-left:4px solid #22c55e;' }, [ - E('h3', { 'style': 'margin-top:0;' }, 'U-Boot Flash Commands'), - E('p', {}, 'Run these commands in U-Boot (Marvell>> prompt) on the target device:'), - E('pre', { 'style': 'background:#000;color:#0f0;padding:10px;border-radius:4px;overflow-x:auto;' }, - 'setenv serverip ' + status.lan_ip + '\n' + - 'setenv ipaddr 192.168.255.100\n' + - 'dhcp\n' + - 'tftpboot 0x6000000 secubox-clone.img\n' + - 'mmc dev 1\n' + - 'mmc write 0x6000000 0 ${filesize}\n' + - 'reset' - ) - ]) : null, + // TFTP Instructions (if running) + this.status.tftp_running ? this.renderTftpInstructions() : null, // Cloned Devices - E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, 'Cloned Devices'), - E('table', { 'class': 'table', 'id': 'clones-table' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th' }, 'Device'), - E('th', { 'class': 'th' }, 'Status') - ]) - ].concat(clones.length > 0 ? clones.map(L.bind(this.renderCloneRow, this)) : - [E('tr', { 'class': 'tr' }, [E('td', { 'class': 'td', 'colspan': 2, 'style': 'text-align:center;' }, - 'No clones yet.')])] - )) - ]) - ].filter(Boolean)); - - // Add dynamic styles - var style = E('style', {}, [ - '.tftp-on { background: #22c55e22; }', - '.tftp-on strong { color: #22c55e; }', - '.tftp-off { background: #64748b22; }', - '.tftp-off strong { color: #64748b; }' - ].join('\n')); - view.insertBefore(style, view.firstChild); + KissTheme.card([ + E('span', {}, '📡 Cloned Devices'), + E('span', { 'style': 'margin-left:auto;font-size:12px;color:var(--kiss-muted);' }, (this.status.clone_count || 0) + ' registered') + ], E('div', { 'id': 'clones-container' }, this.renderClones())) + ].filter(Boolean); poll.add(L.bind(this.refresh, this), 10); - return KissTheme.wrap([view], 'admin/secubox/system/cloner'); + return KissTheme.wrap(content, 'admin/secubox/system/cloner'); }, - createActionButton: function(label, cls, handler) { - var btn = E('button', { 'class': 'cbi-button ' + cls, 'style': 'padding:8px 16px;' }, label); - btn.addEventListener('click', handler); - return btn; + renderBuildProgress: function() { + var p = this.buildProgress; + var pct = p.percent || 0; + return KissTheme.card([ + E('span', {}, '🔨 Building Image...'), + KissTheme.badge(pct + '%', 'yellow') + ], E('div', {}, [ + E('div', { 'style': 'margin-bottom:12px;color:var(--kiss-muted);font-size:13px;' }, p.status || 'Processing...'), + E('div', { 'class': 'kiss-progress', 'style': 'height:12px;' }, [ + E('div', { 'class': 'kiss-progress-fill', 'style': 'width:' + pct + '%;' }) + ]), + p.device ? E('div', { 'style': 'margin-top:8px;font-size:12px;color:var(--kiss-muted);' }, 'Device: ' + p.device) : null + ].filter(Boolean))); }, - renderImageRow: function(img) { - var deviceBadge = E('span', { - 'style': 'padding:2px 8px;border-radius:4px;font-size:12px;background:#3b82f622;color:#3b82f6;' - }, img.device || 'unknown'); + renderImages: function() { + if (!this.images.length) { + return E('div', { 'style': 'text-align:center;padding:30px;color:var(--kiss-muted);' }, [ + E('div', { 'style': 'font-size:32px;margin-bottom:8px;' }, '💾'), + E('div', {}, 'No images yet'), + E('div', { 'style': 'font-size:12px;margin-top:4px;' }, 'Click "Build Image" to create one') + ]); + } - return E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, deviceBadge), - E('td', { 'class': 'td', 'style': 'font-family:monospace;font-size:12px;' }, img.name), - E('td', { 'class': 'td' }, img.size), - E('td', { 'class': 'td' }, img.tftp_ready ? - E('span', { 'style': 'color:#22c55e;' }, 'Ready') : - E('span', { 'style': 'color:#f59e0b;' }, 'Pending')), - E('td', { 'class': 'td' }, '-') + return E('div', { 'style': 'display:flex;flex-direction:column;gap:8px;' }, + this.images.map(function(img) { + return E('div', { 'style': 'display:flex;align-items:center;gap:12px;padding:12px;background:var(--kiss-bg2);border-radius:8px;border:1px solid var(--kiss-line);' }, [ + E('div', { 'style': 'font-size:24px;' }, 'đŸ“Ļ'), + E('div', { 'style': 'flex:1;' }, [ + E('div', { 'style': 'font-weight:600;font-size:13px;' }, img.name), + E('div', { 'style': 'font-size:11px;color:var(--kiss-muted);display:flex;gap:12px;margin-top:4px;' }, [ + E('span', {}, img.device || 'unknown'), + E('span', {}, fmtSize(img.size_bytes || 0)), + E('span', {}, fmtDate(img.created)) + ]) + ]), + img.tftp_ready ? + E('span', { 'style': 'color:var(--kiss-green);font-size:12px;' }, '✓ Ready') : + E('span', { 'style': 'color:var(--kiss-yellow);font-size:12px;' }, 'âŗ Pending') + ]); + }) + ); + }, + + renderTokens: function() { + var self = this; + if (!this.tokens.length) { + return E('div', { 'style': 'text-align:center;padding:30px;color:var(--kiss-muted);' }, [ + E('div', { 'style': 'font-size:32px;margin-bottom:8px;' }, 'đŸŽŸī¸'), + E('div', {}, 'No tokens'), + E('div', { 'style': 'font-size:12px;margin-top:4px;' }, 'Generate a token for new devices') + ]); + } + + return E('div', { 'style': 'display:flex;flex-direction:column;gap:6px;' }, + this.tokens.map(function(tok) { + var isAuto = tok.auto_approve; + var isUsed = tok.used; + return E('div', { + 'style': 'display:flex;align-items:center;gap:10px;padding:10px;background:var(--kiss-bg2);border-radius:6px;border:1px solid var(--kiss-line);' + (isUsed ? 'opacity:0.5;' : '') + }, [ + E('div', { 'style': 'font-family:monospace;font-size:12px;flex:1;color:var(--kiss-cyan);' }, tok.token_short || tok.token.slice(0, 12) + '...'), + isAuto ? E('span', { 'style': 'font-size:10px;padding:2px 6px;background:rgba(0,200,83,0.2);color:var(--kiss-green);border-radius:4px;' }, 'AUTO') : null, + isUsed ? E('span', { 'style': 'font-size:10px;color:var(--kiss-muted);' }, 'used') : null, + E('button', { + 'class': 'kiss-btn kiss-btn-red', + 'style': 'padding:4px 8px;font-size:11px;', + 'data-token': tok.token, + 'click': function(ev) { self.handleDeleteToken(ev); } + }, '✕') + ].filter(Boolean)); + }) + ); + }, + + renderTftpInstructions: function() { + var ip = this.status.lan_ip || '192.168.255.1'; + var cmds = [ + 'setenv serverip ' + ip, + 'setenv ipaddr 192.168.255.100', + 'dhcp', + 'tftpboot 0x6000000 secubox-clone.img', + 'mmc dev 1', + 'mmc write 0x6000000 0 ${filesize}', + 'reset' + ].join('\n'); + + return E('div', { 'class': 'kiss-card kiss-panel-green', 'style': 'margin-top:16px;' }, [ + E('div', { 'class': 'kiss-card-title' }, '📟 U-Boot Flash Commands'), + E('p', { 'style': 'color:var(--kiss-muted);font-size:13px;margin-bottom:12px;' }, + 'Run these commands in U-Boot (Marvell>> prompt) on the target device:'), + E('pre', { 'style': 'background:#000;color:#0f0;padding:16px;border-radius:8px;font-size:12px;overflow-x:auto;margin:0;' }, cmds), + E('button', { + 'class': 'kiss-btn', + 'style': 'margin-top:12px;', + 'click': function() { + navigator.clipboard.writeText(cmds); + ui.addNotification(null, E('p', 'Commands copied to clipboard'), 'info'); + } + }, ['📋 ', 'Copy Commands']) ]); }, - renderTokenRow: function(tok) { - var typeLabel = tok.auto_approve ? 'Auto-Approve' : 'Manual'; - var usedLabel = tok.used ? ' (used)' : ''; - var style = tok.used ? 'opacity:0.5;' : ''; + renderClones: function() { + if (!this.clones.length) { + return E('div', { 'style': 'text-align:center;padding:30px;color:var(--kiss-muted);' }, [ + E('div', { 'style': 'font-size:32px;margin-bottom:8px;' }, '📡'), + E('div', {}, 'No clones registered yet') + ]); + } - var deleteBtn = E('button', { - 'class': 'cbi-button cbi-button-negative', - 'style': 'padding:2px 8px;font-size:12px;', - 'data-token': tok.token - }, 'Delete'); - deleteBtn.addEventListener('click', L.bind(this.handleDeleteToken, this)); - - return E('tr', { 'class': 'tr', 'style': style }, [ - E('td', { 'class': 'td', 'style': 'font-family:monospace;' }, tok.token_short), - E('td', { 'class': 'td' }, tok.created ? tok.created.split('T')[0] : '-'), - E('td', { 'class': 'td' }, typeLabel + usedLabel), - E('td', { 'class': 'td' }, deleteBtn) - ]); - }, - - renderCloneRow: function(clone) { - var statusColor = clone.status === 'active' ? '#22c55e' : '#f59e0b'; - return E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, clone.info || '-'), - E('td', { 'class': 'td' }, E('span', { 'style': 'color:' + statusColor }, clone.status)) + return E('table', { 'class': 'kiss-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Device'), + E('th', {}, 'Token'), + E('th', {}, 'Registered'), + E('th', {}, 'Status') + ])), + E('tbody', {}, this.clones.map(function(c) { + var statusColor = c.status === 'active' ? 'var(--kiss-green)' : 'var(--kiss-yellow)'; + return E('tr', {}, [ + E('td', { 'style': 'font-family:monospace;' }, c.device_id || c.info || '-'), + E('td', { 'style': 'font-size:11px;color:var(--kiss-muted);' }, c.token_short || '-'), + E('td', {}, fmtDate(c.registered)), + E('td', {}, E('span', { 'style': 'color:' + statusColor + ';' }, c.status || 'pending')) + ]); + })) ]); }, @@ -252,109 +324,122 @@ return view.extend({ var self = this; callListDevices().then(function(data) { var devices = data.devices || []; - var select = E('select', { 'id': 'device-select', 'class': 'cbi-input-select', 'style': 'width:100%;' }); + var select = E('select', { 'id': 'device-select', 'style': 'width:100%;padding:10px;background:var(--kiss-bg2);border:1px solid var(--kiss-line);border-radius:6px;color:var(--kiss-text);font-size:14px;' }); devices.forEach(function(dev) { select.appendChild(E('option', { 'value': dev.id }, dev.name + ' (' + dev.cpu + ')')); }); ui.showModal('Build Clone Image', [ - E('p', {}, 'Select the target device type to build an image for:'), + E('p', { 'style': 'color:var(--kiss-muted);' }, 'Select target device type:'), E('div', { 'style': 'margin:15px 0;' }, select), - E('p', { 'style': 'color:#888;font-size:12px;' }, 'The image will be built via ASU API and may take several minutes.'), - E('div', { 'class': 'right' }, [ + E('p', { 'style': 'color:var(--kiss-yellow);font-size:12px;' }, 'âš ī¸ Building may take several minutes via ASU API'), + E('div', { 'class': 'right', 'style': 'margin-top:20px;' }, [ E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), ' ', E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { var deviceType = document.getElementById('device-select').value; ui.hideModal(); - ui.addNotification(null, E('p', 'Building image for ' + deviceType + '...'), 'info'); + ui.addNotification(null, E('p', '🔨 Building image for ' + deviceType + '...'), 'info'); callBuildImage(deviceType).then(function(res) { - ui.addNotification(null, E('p', res.message || 'Build started'), 'info'); + ui.addNotification(null, E('p', res.message || 'Build started'), res.success ? 'info' : 'warning'); self.refresh(); }); - } }, 'Build') + } }, '🔨 Build') ]) ]); }); }, handleTftp: function(start) { + var self = this; var fn = start ? callTftpStart : callTftpStop; - fn().then(L.bind(function(res) { + fn().then(function(res) { ui.addNotification(null, E('p', res.message || (start ? 'TFTP started' : 'TFTP stopped')), 'info'); - this.refresh(); - }, this)); + self.refresh(); + }); }, handleNewToken: function() { - callGenerateToken(false).then(L.bind(function(res) { + var self = this; + callGenerateToken(false).then(function(res) { if (res.success) { ui.showModal('Token Generated', [ - E('p', {}, 'New clone token created:'), - E('pre', { 'style': 'background:#f1f5f9;padding:10px;border-radius:4px;word-break:break-all;' }, res.token), - E('p', { 'style': 'color:#888;' }, 'This token requires manual approval when used.'), - E('div', { 'class': 'right' }, [ - E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { - ui.hideModal(); - } }, 'OK') + E('p', { 'style': 'color:var(--kiss-muted);' }, 'New clone token:'), + E('pre', { 'style': 'background:var(--kiss-bg2);color:var(--kiss-cyan);padding:12px;border-radius:6px;word-break:break-all;font-size:12px;' }, res.token), + E('p', { 'style': 'color:var(--kiss-yellow);font-size:12px;' }, 'âš ī¸ Requires manual approval when used'), + E('div', { 'class': 'right', 'style': 'margin-top:15px;' }, [ + E('button', { 'class': 'cbi-button cbi-button-positive', 'click': ui.hideModal }, 'OK') ]) ]); - this.refresh(); + self.refresh(); } - }, this)); + }); }, handleAutoToken: function() { - callGenerateToken(true).then(L.bind(function(res) { + var self = this; + callGenerateToken(true).then(function(res) { if (res.success) { - ui.showModal('Auto-Approve Token Generated', [ - E('p', {}, 'New auto-approve token created:'), - E('pre', { 'style': 'background:#22c55e22;padding:10px;border-radius:4px;word-break:break-all;' }, res.token), - E('p', { 'style': 'color:#22c55e;' }, 'Devices using this token will auto-join the mesh without manual approval.'), - E('div', { 'class': 'right' }, [ - E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { - ui.hideModal(); - } }, 'OK') + ui.showModal('Auto-Approve Token', [ + E('p', { 'style': 'color:var(--kiss-muted);' }, 'Auto-approve token created:'), + E('pre', { 'style': 'background:rgba(0,200,83,0.1);color:var(--kiss-green);padding:12px;border-radius:6px;word-break:break-all;font-size:12px;border:1px solid rgba(0,200,83,0.3);' }, res.token), + E('p', { 'style': 'color:var(--kiss-green);font-size:12px;' }, '✅ Devices using this token auto-join without approval'), + E('div', { 'class': 'right', 'style': 'margin-top:15px;' }, [ + E('button', { 'class': 'cbi-button cbi-button-positive', 'click': ui.hideModal }, 'OK') ]) ]); - this.refresh(); + self.refresh(); } - }, this)); + }); }, handleDeleteToken: function(ev) { var token = ev.currentTarget.dataset.token; + var self = this; if (confirm('Delete this token?')) { - callDeleteToken(token).then(L.bind(function() { - this.refresh(); - }, this)); + callDeleteToken(token).then(function() { + self.refresh(); + }); } }, refresh: function() { + var self = this; return Promise.all([ callGetStatus(), callListImages(), callListTokens(), - callListClones() - ]).then(L.bind(function(data) { - var status = data[0] || {}; - var tokens = data[2].tokens || []; + callListClones(), + callGetBuildProgress().catch(function() { return {}; }) + ]).then(function(data) { + self.status = data[0] || {}; + self.images = data[1].images || []; + self.tokens = data[2].tokens || []; + self.clones = data[3].clones || []; + self.buildProgress = data[4] || {}; - // Update counts - var tftpEl = document.getElementById('tftp-status'); - var tokenEl = document.getElementById('token-count'); - var cloneEl = document.getElementById('clone-count'); - - if (tftpEl) { - tftpEl.textContent = status.tftp_running ? 'Running' : 'Stopped'; - tftpEl.parentNode.parentNode.className = status.tftp_running ? 'tftp-on' : 'tftp-off'; + // Update stats + var statsEl = document.getElementById('stats-grid'); + if (statsEl) { + dom.content(statsEl, [ + KissTheme.stat(self.images.length, 'Images', 'var(--kiss-blue)'), + KissTheme.stat(self.tokens.length, 'Tokens', 'var(--kiss-purple)'), + KissTheme.stat(self.status.clone_count || 0, 'Clones', 'var(--kiss-green)'), + KissTheme.stat(self.status.tftp_running ? 'Active' : 'Idle', 'TFTP', self.status.tftp_running ? 'var(--kiss-green)' : 'var(--kiss-muted)') + ]); } - if (tokenEl) tokenEl.textContent = String(tokens.length); - if (cloneEl) cloneEl.textContent = String(status.clone_count || 0); - }, this)); + // Update containers + var imagesEl = document.getElementById('images-container'); + if (imagesEl) dom.content(imagesEl, self.renderImages()); + + var tokensEl = document.getElementById('tokens-container'); + if (tokensEl) dom.content(tokensEl, self.renderTokens()); + + var clonesEl = document.getElementById('clones-container'); + if (clonesEl) dom.content(clonesEl, self.renderClones()); + }); }, handleSaveApply: null, diff --git a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js index 3fbb1997..5b5ca4a8 100644 --- a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js +++ b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js @@ -8,6 +8,7 @@ var KissThemeClass = baseclass.extend({ // Navigation config - organized by category with collapsible sections + // Items with `tabs` array show sub-navigation when active nav: [ { cat: 'Dashboard', icon: '📊', collapsed: false, items: [ { icon: '🏠', name: 'Home', path: 'admin/secubox-home' }, @@ -15,27 +16,75 @@ var KissThemeClass = baseclass.extend({ { icon: 'đŸ–Ĩī¸', name: 'System Hub', path: 'admin/secubox/system/system-hub' } ]}, { cat: 'Security', icon: 'đŸ›Ąī¸', collapsed: false, items: [ - { icon: '🧙', name: 'InterceptoR', path: 'admin/secubox/interceptor' }, - { icon: 'đŸ›Ąī¸', name: 'CrowdSec', path: 'admin/secubox/security/crowdsec' }, - { icon: '🔍', name: 'mitmproxy', path: 'admin/secubox/security/mitmproxy' }, + { icon: '🧙', name: 'InterceptoR', path: 'admin/secubox/interceptor', tabs: [ + { name: 'Overview', path: 'admin/secubox/interceptor/overview' }, + { name: 'Services', path: 'admin/secubox/interceptor/services' } + ]}, + { icon: 'đŸ›Ąī¸', name: 'CrowdSec', path: 'admin/secubox/security/crowdsec', tabs: [ + { name: 'Overview', path: 'admin/secubox/security/crowdsec/overview' }, + { name: 'Decisions', path: 'admin/secubox/security/crowdsec/decisions' }, + { name: 'Alerts', path: 'admin/secubox/security/crowdsec/alerts' }, + { name: 'Bouncers', path: 'admin/secubox/security/crowdsec/bouncers' }, + { name: 'Setup', path: 'admin/secubox/security/crowdsec/setup' } + ]}, + { icon: '🔍', name: 'mitmproxy', path: 'admin/secubox/security/mitmproxy', tabs: [ + { name: 'Status', path: 'admin/secubox/security/mitmproxy/status' }, + { name: 'Settings', path: 'admin/secubox/security/mitmproxy/settings' } + ]}, { icon: 'đŸšĢ', name: 'Vortex FW', path: 'admin/secubox/security/vortex-firewall' }, - { icon: 'đŸ‘ī¸', name: 'Client Guard', path: 'admin/secubox/security/guardian' }, + { icon: 'đŸ‘ī¸', name: 'Client Guard', path: 'admin/secubox/security/guardian', tabs: [ + { name: 'Clients', path: 'admin/secubox/security/guardian/clients' }, + { name: 'Settings', path: 'admin/secubox/security/guardian/settings' } + ]}, { icon: 'đŸĒ', name: 'Cookie Track', path: 'admin/secubox/interceptor/cookies' } ]}, { cat: 'Network', icon: '🌐', collapsed: true, items: [ - { icon: 'âš–ī¸', name: 'HAProxy', path: 'admin/services/haproxy' }, - { icon: '🔒', name: 'WireGuard', path: 'admin/services/wireguard' }, - { icon: '🌍', name: 'Tor Shield', path: 'admin/services/tor-shield' }, - { icon: '💾', name: 'CDN Cache', path: 'admin/services/cdn-cache' }, + { icon: 'âš–ī¸', name: 'HAProxy', path: 'admin/services/haproxy', tabs: [ + { name: 'Overview', path: 'admin/services/haproxy/overview' }, + { name: 'Vhosts', path: 'admin/services/haproxy/vhosts' }, + { name: 'Backends', path: 'admin/services/haproxy/backends' }, + { name: 'Certs', path: 'admin/services/haproxy/certificates' }, + { name: 'ACLs', path: 'admin/services/haproxy/acls' }, + { name: 'Stats', path: 'admin/services/haproxy/stats' }, + { name: 'Settings', path: 'admin/services/haproxy/settings' } + ]}, + { icon: '🔒', name: 'WireGuard', path: 'admin/services/wireguard', tabs: [ + { name: 'Wizard', path: 'admin/services/wireguard/wizard' }, + { name: 'Overview', path: 'admin/services/wireguard/overview' }, + { name: 'Peers', path: 'admin/services/wireguard/peers' }, + { name: 'QR Codes', path: 'admin/services/wireguard/qrcodes' }, + { name: 'Traffic', path: 'admin/services/wireguard/traffic' }, + { name: 'Config', path: 'admin/services/wireguard/config' }, + { name: 'Settings', path: 'admin/services/wireguard/settings' } + ]}, + { icon: '🌍', name: 'Tor Shield', path: 'admin/services/tor-shield', tabs: [ + { name: 'Overview', path: 'admin/services/tor-shield/overview' }, + { name: 'Circuits', path: 'admin/services/tor-shield/circuits' }, + { name: 'Hidden Svc', path: 'admin/services/tor-shield/hidden-services' }, + { name: 'Bridges', path: 'admin/services/tor-shield/bridges' }, + { name: 'Settings', path: 'admin/services/tor-shield/settings' } + ]}, + { icon: '💾', name: 'CDN Cache', path: 'admin/services/cdn-cache', tabs: [ + { name: 'Overview', path: 'admin/services/cdn-cache/overview' }, + { name: 'Cache', path: 'admin/services/cdn-cache/cache' }, + { name: 'Policies', path: 'admin/services/cdn-cache/policies' }, + { name: 'Stats', path: 'admin/services/cdn-cache/statistics' }, + { name: 'Maint.', path: 'admin/services/cdn-cache/maintenance' }, + { name: 'Settings', path: 'admin/services/cdn-cache/settings' } + ]}, { icon: '📡', name: 'Bandwidth', path: 'admin/services/bandwidth-manager' }, { icon: 'đŸ“ļ', name: 'Traffic Shaper', path: 'admin/services/traffic-shaper' }, { icon: '🌐', name: 'Network Modes', path: 'admin/services/network-modes' }, { icon: '🔌', name: 'Interfaces', path: 'admin/network/network' } ]}, { cat: 'AI & LLM', icon: '🤖', collapsed: true, items: [ - { icon: 'đŸĻ™', name: 'Ollama', path: 'admin/services/ollama' }, - { icon: '🤖', name: 'LocalAI', path: 'admin/services/localai' }, - { icon: 'đŸ’Ŧ', name: 'Chat', path: 'admin/services/ollama/chat' } + { icon: 'đŸĻ™', name: 'Ollama', path: 'admin/services/ollama', tabs: [ + { name: 'Dashboard', path: 'admin/services/ollama/dashboard' }, + { name: 'Models', path: 'admin/services/ollama/models' }, + { name: 'Chat', path: 'admin/services/ollama/chat' }, + { name: 'Settings', path: 'admin/services/ollama/settings' } + ]}, + { icon: '🤖', name: 'LocalAI', path: 'admin/services/localai' } ]}, { cat: 'Apps', icon: 'đŸ“Ļ', collapsed: true, items: [ { icon: 'đŸŽŦ', name: 'Media Flow', path: 'admin/services/media-flow' }, @@ -173,8 +222,36 @@ var KissThemeClass = baseclass.extend({ color: ${c.green}; background: rgba(0,200,83,0.08); border-left-color: ${c.green}; } +.kiss-nav-item.has-tabs { padding-right: 8px; } +.kiss-nav-item .tab-arrow { margin-left: auto; font-size: 9px; opacity: 0.5; transition: transform 0.2s; } +.kiss-nav-item.expanded .tab-arrow { transform: rotate(90deg); } .kiss-nav-icon { font-size: 14px; width: 20px; text-align: center; flex-shrink: 0; } +/* === Sub-tabs (nested under active items) === */ +.kiss-nav-tabs { + overflow: hidden; max-height: 0; transition: max-height 0.3s ease; + background: rgba(0,0,0,0.15); +} +.kiss-nav-tabs.expanded { max-height: 500px; } +.kiss-nav-tab { + display: flex; align-items: center; gap: 6px; padding: 6px 16px 6px 48px; + text-decoration: none; font-size: 11px; color: ${c.muted}; + transition: all 0.15s; border-left: 2px solid transparent; + position: relative; +} +.kiss-nav-tab::before { + content: ''; position: absolute; left: 36px; top: 50%; + width: 4px; height: 4px; border-radius: 50%; + background: ${c.line}; transform: translateY(-50%); +} +.kiss-nav-tab:hover { background: rgba(255,255,255,0.03); color: ${c.text}; } +.kiss-nav-tab:hover::before { background: ${c.muted}; } +.kiss-nav-tab.active { + color: ${c.cyan}; background: rgba(34,211,238,0.05); + border-left-color: ${c.cyan}; +} +.kiss-nav-tab.active::before { background: ${c.cyan}; } + /* === Main Content === */ .kiss-main { margin-left: 220px; margin-top: 56px; padding: 20px; @@ -549,7 +626,13 @@ body.kiss-mode .cbi-section { max-width: 100% !important; width: 100% !important if (self.collapsedState[cat.cat] === undefined) { // Auto-expand if current path is in this category var hasActive = cat.items.some(function(item) { - return currentPath.indexOf(item.path) !== -1; + if (item.path && currentPath.indexOf(item.path) !== -1) return true; + if (item.tabs) { + return item.tabs.some(function(tab) { + return currentPath === tab.path || currentPath.indexOf(tab.path) !== -1; + }); + } + return false; }); self.collapsedState[cat.cat] = hasActive ? false : (cat.collapsed || false); } @@ -572,24 +655,50 @@ body.kiss-mode .cbi-section { max-width: 100% !important; width: 100% !important self.E('span', { 'class': 'kiss-nav-section-arrow' }, 'â–ŧ') ])); - // Items container - var itemsContainer = self.E('div', { 'class': 'kiss-nav-items' }, - cat.items.map(function(item) { - var isExternal = !!item.url; - var href = isExternal ? item.url : '/cgi-bin/luci/' + item.path; - var isActive = !isExternal && currentPath.indexOf(item.path) !== -1; - return self.E('a', { - 'href': href, - 'class': 'kiss-nav-item' + (isActive ? ' active' : '') + (isExternal ? ' external' : ''), - 'target': isExternal ? '_blank' : '', - 'onClick': function() { self.closeSidebar(); } - }, [ - self.E('span', { 'class': 'kiss-nav-icon' }, item.icon), - self.E('span', {}, item.name), - isExternal ? self.E('span', { 'style': 'margin-left:auto;font-size:10px;opacity:0.5;' }, '↗') : null - ]); - }) - ); + // Items container with sub-tabs + var itemElements = []; + cat.items.forEach(function(item) { + var isExternal = !!item.url; + var href = isExternal ? item.url : '/cgi-bin/luci/' + item.path; + var hasTabs = item.tabs && item.tabs.length > 0; + + // Check if this item or any of its tabs is active + var isItemActive = !isExternal && item.path && currentPath.indexOf(item.path) !== -1; + var isTabActive = hasTabs && item.tabs.some(function(tab) { + return currentPath === tab.path || currentPath.indexOf(tab.path) !== -1; + }); + var isActive = isItemActive || isTabActive; + + // Main nav item + itemElements.push(self.E('a', { + 'href': href, + 'class': 'kiss-nav-item' + (isActive ? ' active' : '') + (isExternal ? ' external' : '') + (hasTabs ? ' has-tabs' : '') + (isActive && hasTabs ? ' expanded' : ''), + 'target': isExternal ? '_blank' : '', + 'onClick': function() { self.closeSidebar(); } + }, [ + self.E('span', { 'class': 'kiss-nav-icon' }, item.icon), + self.E('span', {}, item.name), + isExternal ? self.E('span', { 'style': 'margin-left:auto;font-size:10px;opacity:0.5;' }, '↗') : null, + hasTabs ? self.E('span', { 'class': 'tab-arrow' }, 'â–ļ') : null + ])); + + // Sub-tabs (only rendered if item has tabs) + if (hasTabs) { + var tabsContainer = self.E('div', { + 'class': 'kiss-nav-tabs' + (isActive ? ' expanded' : '') + }, item.tabs.map(function(tab) { + var tabActive = currentPath === tab.path || currentPath.indexOf(tab.path) !== -1; + return self.E('a', { + 'href': '/cgi-bin/luci/' + tab.path, + 'class': 'kiss-nav-tab' + (tabActive ? ' active' : ''), + 'onClick': function() { self.closeSidebar(); } + }, tab.name); + })); + itemElements.push(tabsContainer); + } + }); + + var itemsContainer = self.E('div', { 'class': 'kiss-nav-items' }, itemElements); navItems.push(itemsContainer); }); diff --git a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/dashboard.js b/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/dashboard.js index 91581e86..eda88c3e 100644 --- a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/dashboard.js +++ b/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/dashboard.js @@ -773,11 +773,18 @@ return view.extend({ uploadPromise.then(function(r) { poll.start(); - ui.hideModal(); if (r && r.success) { - ui.addNotification(null, E('p', {}, _('App reuploaded: ') + id), 'success'); - self.refresh().then(function() { self.updateStatus(); }); + // Restart service to reload the updated file + ui.showModal(_('Restarting...'), [ + E('p', { 'class': 'spinning' }, _('Restarting Streamlit to apply changes...')) + ]); + return api.restart().then(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, _('App reuploaded and service restarted: ') + id), 'success'); + self.refresh().then(function() { self.updateStatus(); }); + }); } else { + ui.hideModal(); ui.addNotification(null, E('p', {}, (r && r.message) || _('Reupload failed')), 'error'); } }).catch(function(err) {