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:
parent
9a6aaf8caf
commit
fdc7467630
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user