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 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-12 07:36:36 +01:00
parent 9a6aaf8caf
commit fdc7467630
3 changed files with 432 additions and 231 deletions

View File

@ -64,187 +64,259 @@ var callDeleteToken = rpc.declare({
params: ['token'] 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({ return view.extend({
status: {},
images: [],
tokens: [],
clones: [],
devices: [],
buildProgress: null,
load: function() { load: function() {
return Promise.all([ return Promise.all([
callGetStatus(), callGetStatus(),
callListImages(), callListImages(),
callListTokens(), callListTokens(),
callListClones(), callListClones(),
callListDevices() callListDevices(),
callGetBuildProgress().catch(function() { return {}; })
]); ]);
}, },
render: function(data) { render: function(data) {
var status = data[0] || {}; var self = this;
var images = data[1].images || []; this.status = data[0] || {};
var tokens = data[2].tokens || []; this.images = data[1].images || [];
var clones = data[3].clones || []; this.tokens = data[2].tokens || [];
var devices = data[4].devices || []; this.clones = data[3].clones || [];
this.devices = data[4].devices || [];
this.buildProgress = data[5] || {};
var view = E('div', { 'class': 'cbi-map' }, [ var content = [
E('h2', {}, 'Cloning Station'), // Header
E('div', { 'class': 'cbi-map-descr' }, 'Build and deploy SecuBox clone images to new devices'), E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;' }, [
E('div', {}, [
// Status Cards E('h1', { 'style': 'font-size:28px;font-weight:700;margin:0;display:flex;align-items:center;gap:12px;' }, [
E('div', { 'style': 'display:flex;gap:20px;margin:20px 0;flex-wrap:wrap;' }, [ '🔄 Cloning Station'
E('div', { 'style': 'padding:15px;background:#3b82f622;border-radius:8px;min-width:120px;' }, [ ]),
E('div', { 'style': 'font-size:12px;color:#888;' }, 'Device'), E('p', { 'style': 'color:var(--kiss-muted);margin:6px 0 0;' }, 'Build and deploy SecuBox clone images')
E('strong', { 'style': 'font-size:16px;color:#3b82f6;' }, status.device_type || 'unknown')
]), ]),
E('div', { 'style': 'padding:15px;border-radius:8px;min-width:120px;', 'class': status.tftp_running ? 'tftp-on' : 'tftp-off' }, [ E('div', { 'style': 'display:flex;gap:8px;' }, [
E('div', { 'style': 'font-size:12px;color:#888;' }, 'TFTP'), KissTheme.badge(this.status.device_type || 'unknown', 'blue'),
E('strong', { 'style': 'font-size:16px;', 'id': 'tftp-status' }, KissTheme.badge(this.status.tftp_running ? 'TFTP ON' : 'TFTP OFF',
status.tftp_running ? 'Running' : 'Stopped') this.status.tftp_running ? 'green' : 'red')
]),
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))
]) ])
]), ]),
// 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 // Quick Actions
E('div', { 'class': 'cbi-section' }, [ KissTheme.card([
E('h3', {}, 'Quick Actions'), E('span', {}, '⚡ Quick Actions')
E('div', { 'style': 'display:flex;gap:10px;flex-wrap:wrap;' }, [ ], E('div', { 'style': 'display:flex;gap:12px;flex-wrap:wrap;' }, [
this.createActionButton('Build Image', 'cbi-button-action', L.bind(this.handleBuild, this)), E('button', {
this.createActionButton(status.tftp_running ? 'Stop TFTP' : 'Start TFTP', 'class': 'kiss-btn kiss-btn-blue',
status.tftp_running ? 'cbi-button-negative' : 'cbi-button-positive', 'click': function() { self.handleBuild(); }
L.bind(this.handleTftp, this, !status.tftp_running)), }, ['🔨 ', 'Build Image']),
this.createActionButton('New Token', 'cbi-button-action', L.bind(this.handleNewToken, this)), E('button', {
this.createActionButton('Auto-Approve Token', 'cbi-button-save', L.bind(this.handleAutoToken, this)) '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 // TFTP Instructions (if running)
E('div', { 'class': 'cbi-section' }, [ this.status.tftp_running ? this.renderTftpInstructions() : null,
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,
// Cloned Devices // Cloned Devices
E('div', { 'class': 'cbi-section' }, [ KissTheme.card([
E('h3', {}, 'Cloned Devices'), E('span', {}, '📡 Cloned Devices'),
E('table', { 'class': 'table', 'id': 'clones-table' }, [ E('span', { 'style': 'margin-left:auto;font-size:12px;color:var(--kiss-muted);' }, (this.status.clone_count || 0) + ' registered')
E('tr', { 'class': 'tr table-titles' }, [ ], E('div', { 'id': 'clones-container' }, this.renderClones()))
E('th', { 'class': 'th' }, 'Device'), ].filter(Boolean);
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);
poll.add(L.bind(this.refresh, this), 10); 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) { renderBuildProgress: function() {
var btn = E('button', { 'class': 'cbi-button ' + cls, 'style': 'padding:8px 16px;' }, label); var p = this.buildProgress;
btn.addEventListener('click', handler); var pct = p.percent || 0;
return btn; 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) { renderImages: function() {
var deviceBadge = E('span', { if (!this.images.length) {
'style': 'padding:2px 8px;border-radius:4px;font-size:12px;background:#3b82f622;color:#3b82f6;' return E('div', { 'style': 'text-align:center;padding:30px;color:var(--kiss-muted);' }, [
}, img.device || 'unknown'); 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' }, [ return E('div', { 'style': 'display:flex;flex-direction:column;gap:8px;' },
E('td', { 'class': 'td' }, deviceBadge), this.images.map(function(img) {
E('td', { 'class': 'td', 'style': 'font-family:monospace;font-size:12px;' }, img.name), 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('td', { 'class': 'td' }, img.size), E('div', { 'style': 'font-size:24px;' }, '📦'),
E('td', { 'class': 'td' }, img.tftp_ready ? E('div', { 'style': 'flex:1;' }, [
E('span', { 'style': 'color:#22c55e;' }, 'Ready') : E('div', { 'style': 'font-weight:600;font-size:13px;' }, img.name),
E('span', { 'style': 'color:#f59e0b;' }, 'Pending')), E('div', { 'style': 'font-size:11px;color:var(--kiss-muted);display:flex;gap:12px;margin-top:4px;' }, [
E('td', { 'class': 'td' }, '-') 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) { renderClones: function() {
var typeLabel = tok.auto_approve ? 'Auto-Approve' : 'Manual'; if (!this.clones.length) {
var usedLabel = tok.used ? ' (used)' : ''; return E('div', { 'style': 'text-align:center;padding:30px;color:var(--kiss-muted);' }, [
var style = tok.used ? 'opacity:0.5;' : ''; E('div', { 'style': 'font-size:32px;margin-bottom:8px;' }, '📡'),
E('div', {}, 'No clones registered yet')
]);
}
var deleteBtn = E('button', { return E('table', { 'class': 'kiss-table' }, [
'class': 'cbi-button cbi-button-negative', E('thead', {}, E('tr', {}, [
'style': 'padding:2px 8px;font-size:12px;', E('th', {}, 'Device'),
'data-token': tok.token E('th', {}, 'Token'),
}, 'Delete'); E('th', {}, 'Registered'),
deleteBtn.addEventListener('click', L.bind(this.handleDeleteToken, this)); E('th', {}, 'Status')
])),
return E('tr', { 'class': 'tr', 'style': style }, [ E('tbody', {}, this.clones.map(function(c) {
E('td', { 'class': 'td', 'style': 'font-family:monospace;' }, tok.token_short), var statusColor = c.status === 'active' ? 'var(--kiss-green)' : 'var(--kiss-yellow)';
E('td', { 'class': 'td' }, tok.created ? tok.created.split('T')[0] : '-'), return E('tr', {}, [
E('td', { 'class': 'td' }, typeLabel + usedLabel), E('td', { 'style': 'font-family:monospace;' }, c.device_id || c.info || '-'),
E('td', { 'class': 'td' }, deleteBtn) 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'))
]);
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))
]); ]);
}, },
@ -252,109 +324,122 @@ return view.extend({
var self = this; var self = this;
callListDevices().then(function(data) { callListDevices().then(function(data) {
var devices = data.devices || []; 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) { devices.forEach(function(dev) {
select.appendChild(E('option', { 'value': dev.id }, dev.name + ' (' + dev.cpu + ')')); select.appendChild(E('option', { 'value': dev.id }, dev.name + ' (' + dev.cpu + ')'));
}); });
ui.showModal('Build Clone Image', [ 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('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('p', { 'style': 'color:var(--kiss-yellow);font-size:12px;' }, '⚠️ Building may take several minutes via ASU API'),
E('div', { 'class': 'right' }, [ E('div', { 'class': 'right', 'style': 'margin-top:20px;' }, [
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'),
' ', ' ',
E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() {
var deviceType = document.getElementById('device-select').value; var deviceType = document.getElementById('device-select').value;
ui.hideModal(); 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) { 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(); self.refresh();
}); });
} }, 'Build') } }, '🔨 Build')
]) ])
]); ]);
}); });
}, },
handleTftp: function(start) { handleTftp: function(start) {
var self = this;
var fn = start ? callTftpStart : callTftpStop; 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'); ui.addNotification(null, E('p', res.message || (start ? 'TFTP started' : 'TFTP stopped')), 'info');
this.refresh(); self.refresh();
}, this)); });
}, },
handleNewToken: function() { handleNewToken: function() {
callGenerateToken(false).then(L.bind(function(res) { var self = this;
callGenerateToken(false).then(function(res) {
if (res.success) { if (res.success) {
ui.showModal('Token Generated', [ ui.showModal('Token Generated', [
E('p', {}, 'New clone token created:'), E('p', { 'style': 'color:var(--kiss-muted);' }, 'New clone token:'),
E('pre', { 'style': 'background:#f1f5f9;padding:10px;border-radius:4px;word-break:break-all;' }, res.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:#888;' }, 'This token requires manual approval when used.'), E('p', { 'style': 'color:var(--kiss-yellow);font-size:12px;' }, '⚠️ Requires manual approval when used'),
E('div', { 'class': 'right' }, [ E('div', { 'class': 'right', 'style': 'margin-top:15px;' }, [
E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { E('button', { 'class': 'cbi-button cbi-button-positive', 'click': ui.hideModal }, 'OK')
ui.hideModal();
} }, 'OK')
]) ])
]); ]);
this.refresh(); self.refresh();
} }
}, this)); });
}, },
handleAutoToken: function() { handleAutoToken: function() {
callGenerateToken(true).then(L.bind(function(res) { var self = this;
callGenerateToken(true).then(function(res) {
if (res.success) { if (res.success) {
ui.showModal('Auto-Approve Token Generated', [ ui.showModal('Auto-Approve Token', [
E('p', {}, 'New auto-approve token created:'), E('p', { 'style': 'color:var(--kiss-muted);' }, 'Auto-approve token created:'),
E('pre', { 'style': 'background:#22c55e22;padding:10px;border-radius:4px;word-break:break-all;' }, res.token), 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:#22c55e;' }, 'Devices using this token will auto-join the mesh without manual approval.'), E('p', { 'style': 'color:var(--kiss-green);font-size:12px;' }, '✅ Devices using this token auto-join without approval'),
E('div', { 'class': 'right' }, [ E('div', { 'class': 'right', 'style': 'margin-top:15px;' }, [
E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { E('button', { 'class': 'cbi-button cbi-button-positive', 'click': ui.hideModal }, 'OK')
ui.hideModal();
} }, 'OK')
]) ])
]); ]);
this.refresh(); self.refresh();
} }
}, this)); });
}, },
handleDeleteToken: function(ev) { handleDeleteToken: function(ev) {
var token = ev.currentTarget.dataset.token; var token = ev.currentTarget.dataset.token;
var self = this;
if (confirm('Delete this token?')) { if (confirm('Delete this token?')) {
callDeleteToken(token).then(L.bind(function() { callDeleteToken(token).then(function() {
this.refresh(); self.refresh();
}, this)); });
} }
}, },
refresh: function() { refresh: function() {
var self = this;
return Promise.all([ return Promise.all([
callGetStatus(), callGetStatus(),
callListImages(), callListImages(),
callListTokens(), callListTokens(),
callListClones() callListClones(),
]).then(L.bind(function(data) { callGetBuildProgress().catch(function() { return {}; })
var status = data[0] || {}; ]).then(function(data) {
var tokens = data[2].tokens || []; 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 // Update stats
var tftpEl = document.getElementById('tftp-status'); var statsEl = document.getElementById('stats-grid');
var tokenEl = document.getElementById('token-count'); if (statsEl) {
var cloneEl = document.getElementById('clone-count'); dom.content(statsEl, [
KissTheme.stat(self.images.length, 'Images', 'var(--kiss-blue)'),
if (tftpEl) { KissTheme.stat(self.tokens.length, 'Tokens', 'var(--kiss-purple)'),
tftpEl.textContent = status.tftp_running ? 'Running' : 'Stopped'; KissTheme.stat(self.status.clone_count || 0, 'Clones', 'var(--kiss-green)'),
tftpEl.parentNode.parentNode.className = status.tftp_running ? 'tftp-on' : 'tftp-off'; 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, handleSaveApply: null,

View File

@ -8,6 +8,7 @@
var KissThemeClass = baseclass.extend({ var KissThemeClass = baseclass.extend({
// Navigation config - organized by category with collapsible sections // Navigation config - organized by category with collapsible sections
// Items with `tabs` array show sub-navigation when active
nav: [ nav: [
{ cat: 'Dashboard', icon: '📊', collapsed: false, items: [ { cat: 'Dashboard', icon: '📊', collapsed: false, items: [
{ icon: '🏠', name: 'Home', path: 'admin/secubox-home' }, { 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' } { icon: '🖥️', name: 'System Hub', path: 'admin/secubox/system/system-hub' }
]}, ]},
{ cat: 'Security', icon: '🛡️', collapsed: false, items: [ { cat: 'Security', icon: '🛡️', collapsed: false, items: [
{ icon: '🧙', name: 'InterceptoR', path: 'admin/secubox/interceptor' }, { icon: '🧙', name: 'InterceptoR', path: 'admin/secubox/interceptor', tabs: [
{ icon: '🛡️', name: 'CrowdSec', path: 'admin/secubox/security/crowdsec' }, { name: 'Overview', path: 'admin/secubox/interceptor/overview' },
{ icon: '🔍', name: 'mitmproxy', path: 'admin/secubox/security/mitmproxy' }, { 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: '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' } { icon: '🍪', name: 'Cookie Track', path: 'admin/secubox/interceptor/cookies' }
]}, ]},
{ cat: 'Network', icon: '🌐', collapsed: true, items: [ { cat: 'Network', icon: '🌐', collapsed: true, items: [
{ icon: '⚖️', name: 'HAProxy', path: 'admin/services/haproxy' }, { icon: '⚖️', name: 'HAProxy', path: 'admin/services/haproxy', tabs: [
{ icon: '🔒', name: 'WireGuard', path: 'admin/services/wireguard' }, { name: 'Overview', path: 'admin/services/haproxy/overview' },
{ icon: '🌍', name: 'Tor Shield', path: 'admin/services/tor-shield' }, { name: 'Vhosts', path: 'admin/services/haproxy/vhosts' },
{ icon: '💾', name: 'CDN Cache', path: 'admin/services/cdn-cache' }, { 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: 'Bandwidth', path: 'admin/services/bandwidth-manager' },
{ icon: '📶', name: 'Traffic Shaper', path: 'admin/services/traffic-shaper' }, { icon: '📶', name: 'Traffic Shaper', path: 'admin/services/traffic-shaper' },
{ icon: '🌐', name: 'Network Modes', path: 'admin/services/network-modes' }, { icon: '🌐', name: 'Network Modes', path: 'admin/services/network-modes' },
{ icon: '🔌', name: 'Interfaces', path: 'admin/network/network' } { icon: '🔌', name: 'Interfaces', path: 'admin/network/network' }
]}, ]},
{ cat: 'AI & LLM', icon: '🤖', collapsed: true, items: [ { cat: 'AI & LLM', icon: '🤖', collapsed: true, items: [
{ icon: '🦙', name: 'Ollama', path: 'admin/services/ollama' }, { icon: '🦙', name: 'Ollama', path: 'admin/services/ollama', tabs: [
{ icon: '🤖', name: 'LocalAI', path: 'admin/services/localai' }, { name: 'Dashboard', path: 'admin/services/ollama/dashboard' },
{ icon: '💬', name: 'Chat', path: 'admin/services/ollama/chat' } { 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: [ { cat: 'Apps', icon: '📦', collapsed: true, items: [
{ icon: '🎬', name: 'Media Flow', path: 'admin/services/media-flow' }, { 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); color: ${c.green}; background: rgba(0,200,83,0.08);
border-left-color: ${c.green}; 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; } .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 === */ /* === Main Content === */
.kiss-main { .kiss-main {
margin-left: 220px; margin-top: 56px; padding: 20px; 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) { if (self.collapsedState[cat.cat] === undefined) {
// Auto-expand if current path is in this category // Auto-expand if current path is in this category
var hasActive = cat.items.some(function(item) { 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); 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' }, '▼') self.E('span', { 'class': 'kiss-nav-section-arrow' }, '▼')
])); ]));
// Items container // Items container with sub-tabs
var itemsContainer = self.E('div', { 'class': 'kiss-nav-items' }, var itemElements = [];
cat.items.map(function(item) { cat.items.forEach(function(item) {
var isExternal = !!item.url; var isExternal = !!item.url;
var href = isExternal ? item.url : '/cgi-bin/luci/' + item.path; var href = isExternal ? item.url : '/cgi-bin/luci/' + item.path;
var isActive = !isExternal && currentPath.indexOf(item.path) !== -1; var hasTabs = item.tabs && item.tabs.length > 0;
return self.E('a', {
'href': href, // Check if this item or any of its tabs is active
'class': 'kiss-nav-item' + (isActive ? ' active' : '') + (isExternal ? ' external' : ''), var isItemActive = !isExternal && item.path && currentPath.indexOf(item.path) !== -1;
'target': isExternal ? '_blank' : '', var isTabActive = hasTabs && item.tabs.some(function(tab) {
'onClick': function() { self.closeSidebar(); } return currentPath === tab.path || currentPath.indexOf(tab.path) !== -1;
}, [ });
self.E('span', { 'class': 'kiss-nav-icon' }, item.icon), var isActive = isItemActive || isTabActive;
self.E('span', {}, item.name),
isExternal ? self.E('span', { 'style': 'margin-left:auto;font-size:10px;opacity:0.5;' }, '↗') : null // 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); navItems.push(itemsContainer);
}); });

View File

@ -773,11 +773,18 @@ return view.extend({
uploadPromise.then(function(r) { uploadPromise.then(function(r) {
poll.start(); poll.start();
ui.hideModal();
if (r && r.success) { if (r && r.success) {
ui.addNotification(null, E('p', {}, _('App reuploaded: ') + id), 'success'); // Restart service to reload the updated file
self.refresh().then(function() { self.updateStatus(); }); 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 { } else {
ui.hideModal();
ui.addNotification(null, E('p', {}, (r && r.message) || _('Reupload failed')), 'error'); ui.addNotification(null, E('p', {}, (r && r.message) || _('Reupload failed')), 'error');
} }
}).catch(function(err) { }).catch(function(err) {