feat(kiss): Collapsible multi-level navigation with extended Ollama features

- KISS Theme v2.1: Collapsible nav sections with icons, auto-expand active
- Add comprehensive navigation with all SecuBox apps organized by category
- Fix Client Guardian path to admin/secubox/security/guardian
- Fix Cookie Tracker path to admin/secubox/interceptor/cookies

- Ollama: Add system resources card (RAM/disk usage with progress bars)
- Ollama: Add API endpoints card with copy-to-clipboard
- Ollama: Add container logs viewer with refresh
- Ollama: Add system_info, logs, model_info RPCD methods
- Ollama: Update stats to show RAM/disk usage

- Fix Vortex Firewall menu path to admin/secubox/security

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-12 06:56:26 +01:00
parent ea5880a76b
commit 2d9beb6f67
5 changed files with 382 additions and 40 deletions

View File

@ -9,6 +9,9 @@ var api = {
status: rpc.declare({ object: 'luci.ollama', method: 'status' }), status: rpc.declare({ object: 'luci.ollama', method: 'status' }),
models: rpc.declare({ object: 'luci.ollama', method: 'models' }), models: rpc.declare({ object: 'luci.ollama', method: 'models' }),
health: rpc.declare({ object: 'luci.ollama', method: 'health' }), health: rpc.declare({ object: 'luci.ollama', method: 'health' }),
systemInfo: rpc.declare({ object: 'luci.ollama', method: 'system_info' }),
logs: rpc.declare({ object: 'luci.ollama', method: 'logs', params: ['lines'] }),
modelInfo: rpc.declare({ object: 'luci.ollama', method: 'model_info', params: ['name'] }),
start: rpc.declare({ object: 'luci.ollama', method: 'start' }), start: rpc.declare({ object: 'luci.ollama', method: 'start' }),
stop: rpc.declare({ object: 'luci.ollama', method: 'stop' }), stop: rpc.declare({ object: 'luci.ollama', method: 'stop' }),
restart: rpc.declare({ object: 'luci.ollama', method: 'restart' }), restart: rpc.declare({ object: 'luci.ollama', method: 'restart' }),
@ -86,12 +89,30 @@ return view.extend({
.ol-toast { position: fixed; bottom: 1rem; right: 1rem; padding: 0.75rem 1rem; border-radius: 0.375rem; font-size: 0.875rem; z-index: 9999; } .ol-toast { position: fixed; bottom: 1rem; right: 1rem; padding: 0.75rem 1rem; border-radius: 0.375rem; font-size: 0.875rem; z-index: 9999; }
.ol-toast.success { background: var(--ol-success); color: #fff; } .ol-toast.success { background: var(--ol-success); color: #fff; }
.ol-toast.error { background: var(--ol-danger); color: #fff; } .ol-toast.error { background: var(--ol-danger); color: #fff; }
.ol-progress { height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden; margin-top: 0.5rem; }
.ol-progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s; }
.ol-progress-fill.green { background: linear-gradient(90deg, #22c55e, #4ade80); }
.ol-progress-fill.yellow { background: linear-gradient(90deg, #eab308, #facc15); }
.ol-progress-fill.red { background: linear-gradient(90deg, #ef4444, #f87171); }
.ol-logs { font-family: monospace; font-size: 0.75rem; max-height: 200px; overflow-y: auto; background: #0a0f1a; border: 1px solid var(--ol-border); border-radius: 0.375rem; padding: 0.75rem; }
.ol-logs-line { color: var(--ol-muted); margin-bottom: 0.25rem; word-break: break-all; }
.ol-logs-line.error { color: var(--ol-danger); }
.ol-logs-line.info { color: var(--ol-accent); }
.ol-api-url { font-family: monospace; font-size: 0.8rem; padding: 0.5rem; background: var(--ol-bg); border-radius: 0.25rem; display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem; }
.ol-api-url code { color: var(--ol-accent); }
.ol-api-url button { padding: 0.25rem 0.5rem; font-size: 0.7rem; }
.ol-tabs { display: flex; gap: 0.25rem; margin-bottom: 1rem; border-bottom: 1px solid var(--ol-border); padding-bottom: 0.5rem; }
.ol-tab { padding: 0.5rem 1rem; background: transparent; border: none; color: var(--ol-muted); cursor: pointer; font-size: 0.85rem; border-radius: 0.25rem 0.25rem 0 0; }
.ol-tab:hover { color: var(--ol-text); }
.ol-tab.active { color: var(--ol-accent); background: rgba(249,115,22,0.1); border-bottom: 2px solid var(--ol-accent); }
`, `,
load: function() { load: function() {
return Promise.all([ return Promise.all([
api.status().catch(function() { return {}; }), api.status().catch(function() { return {}; }),
api.models().catch(function() { return { models: [] }; }) api.models().catch(function() { return { models: [] }; }),
api.systemInfo().catch(function() { return {}; }),
api.logs(30).catch(function() { return { logs: [] }; })
]); ]);
}, },
@ -99,7 +120,11 @@ return view.extend({
var self = this; var self = this;
var status = data[0] || {}; var status = data[0] || {};
var models = (data[1] && data[1].models) || []; var models = (data[1] && data[1].models) || [];
var sysInfo = data[2] || {};
var logs = (data[3] && data[3].logs) || [];
this.isRunning = status.running; this.isRunning = status.running;
this.sysInfo = sysInfo;
this.logs = logs;
var view = E('div', { 'class': 'ol-wrap' }, [ var view = E('div', { 'class': 'ol-wrap' }, [
E('style', {}, this.css), E('style', {}, this.css),
@ -181,6 +206,50 @@ return view.extend({
E('button', { 'class': 'ol-btn ol-btn-primary', 'id': 'chat-send', 'click': function() { self.sendChat(); } }, 'Send') E('button', { 'class': 'ol-btn ol-btn-primary', 'id': 'chat-send', 'click': function() { self.sendChat(); } }, 'Send')
]) ])
]) ])
]),
// System Resources
E('div', { 'class': 'ol-card' }, [
E('div', { 'class': 'ol-card-head' }, 'System Resources'),
E('div', { 'class': 'ol-card-body', 'id': 'sys-resources' }, this.renderSystemResources(sysInfo))
]),
// API Endpoints
E('div', { 'class': 'ol-card' }, [
E('div', { 'class': 'ol-card-head' }, 'API Endpoints'),
E('div', { 'class': 'ol-card-body' }, [
E('div', { 'class': 'ol-api-url' }, [
E('span', {}, ['Base: ', E('code', {}, 'http://localhost:' + (status.api_port || 11434))]),
E('button', { 'class': 'ol-btn ol-btn-sm', 'click': function() { self.copyToClipboard('http://localhost:' + (status.api_port || 11434)); } }, 'Copy')
]),
E('div', { 'class': 'ol-api-url' }, [
E('span', {}, ['Chat: ', E('code', {}, '/api/chat')]),
E('button', { 'class': 'ol-btn ol-btn-sm', 'click': function() { self.copyToClipboard('/api/chat'); } }, 'Copy')
]),
E('div', { 'class': 'ol-api-url' }, [
E('span', {}, ['Generate: ', E('code', {}, '/api/generate')]),
E('button', { 'class': 'ol-btn ol-btn-sm', 'click': function() { self.copyToClipboard('/api/generate'); } }, 'Copy')
]),
E('div', { 'class': 'ol-api-url' }, [
E('span', {}, ['Models: ', E('code', {}, '/api/tags')]),
E('button', { 'class': 'ol-btn ol-btn-sm', 'click': function() { self.copyToClipboard('/api/tags'); } }, 'Copy')
]),
E('div', { 'class': 'ol-api-url' }, [
E('span', {}, ['Embeddings: ', E('code', {}, '/api/embeddings')]),
E('button', { 'class': 'ol-btn ol-btn-sm', 'click': function() { self.copyToClipboard('/api/embeddings'); } }, 'Copy')
])
])
]),
// Logs
E('div', { 'class': 'ol-card' }, [
E('div', { 'class': 'ol-card-head' }, [
'Container Logs',
E('button', { 'class': 'ol-btn ol-btn-sm', 'click': function() { self.refreshLogs(); } }, 'Refresh')
]),
E('div', { 'class': 'ol-card-body' }, [
E('div', { 'class': 'ol-logs', 'id': 'ol-logs' }, this.renderLogs(logs))
])
]) ])
]) ])
]); ]);
@ -190,6 +259,9 @@ return view.extend({
}, },
renderStats: function(status, models) { renderStats: function(status, models) {
var sysInfo = this.sysInfo || {};
var memPct = (sysInfo.memory && sysInfo.memory.percent) || 0;
var diskPct = (sysInfo.disk && sysInfo.disk.percent) || 0;
return [ return [
E('div', { 'class': 'ol-stat' }, [ E('div', { 'class': 'ol-stat' }, [
E('div', { 'class': 'ol-stat-val' }, models.length.toString()), E('div', { 'class': 'ol-stat-val' }, models.length.toString()),
@ -200,16 +272,90 @@ return view.extend({
E('div', { 'class': 'ol-stat-lbl' }, 'Uptime') E('div', { 'class': 'ol-stat-lbl' }, 'Uptime')
]), ]),
E('div', { 'class': 'ol-stat' }, [ E('div', { 'class': 'ol-stat' }, [
E('div', { 'class': 'ol-stat-val' }, (status.api_port || 11434).toString()), E('div', { 'class': 'ol-stat-val' }, memPct + '%'),
E('div', { 'class': 'ol-stat-lbl' }, 'API Port') E('div', { 'class': 'ol-stat-lbl' }, 'RAM Used')
]), ]),
E('div', { 'class': 'ol-stat' }, [ E('div', { 'class': 'ol-stat' }, [
E('div', { 'class': 'ol-stat-val' }, status.runtime || '-'), E('div', { 'class': 'ol-stat-val' }, diskPct + '%'),
E('div', { 'class': 'ol-stat-lbl' }, 'Runtime') E('div', { 'class': 'ol-stat-lbl' }, 'Disk Used')
]) ])
]; ];
}, },
renderSystemResources: function(sysInfo) {
var mem = sysInfo.memory || {};
var disk = sysInfo.disk || {};
var container = sysInfo.container || {};
var memPct = mem.percent || 0;
var memColor = memPct > 80 ? 'red' : memPct > 60 ? 'yellow' : 'green';
var diskPct = disk.percent || 0;
var diskColor = diskPct > 80 ? 'red' : diskPct > 60 ? 'yellow' : 'green';
return [
E('div', { 'class': 'ol-row' }, [
E('span', { 'class': 'ol-row-lbl' }, 'System RAM'),
E('span', {}, fmtBytes((mem.used_kb || 0) * 1024) + ' / ' + fmtBytes((mem.total_kb || 0) * 1024))
]),
E('div', { 'class': 'ol-progress' }, [
E('div', { 'class': 'ol-progress-fill ' + memColor, 'style': 'width:' + memPct + '%' })
]),
E('div', { 'class': 'ol-row', 'style': 'margin-top: 1rem;' }, [
E('span', { 'class': 'ol-row-lbl' }, 'Data Disk (' + (disk.path || '/srv/ollama') + ')'),
E('span', {}, fmtBytes((disk.used_kb || 0) * 1024) + ' / ' + fmtBytes((disk.total_kb || 0) * 1024))
]),
E('div', { 'class': 'ol-progress' }, [
E('div', { 'class': 'ol-progress-fill ' + diskColor, 'style': 'width:' + diskPct + '%' })
]),
E('div', { 'class': 'ol-row', 'style': 'margin-top: 1rem;' }, [
E('span', { 'class': 'ol-row-lbl' }, 'Container RAM'),
E('span', {}, container.memory || '-')
]),
E('div', { 'class': 'ol-row' }, [
E('span', { 'class': 'ol-row-lbl' }, 'Container CPU'),
E('span', {}, container.cpu || '-')
])
];
},
renderLogs: function(logs) {
if (!logs || logs.length === 0) {
return E('div', { 'style': 'color: var(--ol-muted); text-align: center;' }, 'No logs available');
}
return logs.map(function(line) {
var cls = 'ol-logs-line';
if (line.match(/error|fail/i)) cls += ' error';
else if (line.match(/info|start|ready/i)) cls += ' info';
return E('div', { 'class': cls }, line);
});
},
copyToClipboard: function(text) {
var self = this;
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function() {
self.toast('Copied!', true);
});
} else {
var ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
self.toast('Copied!', true);
}
},
refreshLogs: function() {
var self = this;
api.logs(50).then(function(data) {
self.logs = (data && data.logs) || [];
var el = document.getElementById('ol-logs');
if (el) dom.content(el, self.renderLogs(self.logs));
});
},
suggestedModels: [ suggestedModels: [
{ name: 'tinyllama', desc: 'Tiny but capable, fast inference', size: '637 MB' }, { name: 'tinyllama', desc: 'Tiny but capable, fast inference', size: '637 MB' },
{ name: 'llama3.2:1b', desc: 'Meta Llama 3.2 1B - lightweight', size: '1.3 GB' }, { name: 'llama3.2:1b', desc: 'Meta Llama 3.2 1B - lightweight', size: '1.3 GB' },
@ -266,11 +412,14 @@ return view.extend({
var self = this; var self = this;
return Promise.all([ return Promise.all([
api.status().catch(function() { return {}; }), api.status().catch(function() { return {}; }),
api.models().catch(function() { return { models: [] }; }) api.models().catch(function() { return { models: [] }; }),
api.systemInfo().catch(function() { return {}; })
]).then(function(data) { ]).then(function(data) {
var status = data[0] || {}; var status = data[0] || {};
var models = (data[1] && data[1].models) || []; var models = (data[1] && data[1].models) || [];
var sysInfo = data[2] || {};
self.isRunning = status.running; self.isRunning = status.running;
self.sysInfo = sysInfo;
var badge = document.getElementById('ol-status'); var badge = document.getElementById('ol-status');
if (badge) { if (badge) {
@ -290,6 +439,10 @@ return view.extend({
var svcEl = document.getElementById('svc-status'); var svcEl = document.getElementById('svc-status');
if (svcEl) svcEl.textContent = status.running ? 'Running' : 'Stopped'; if (svcEl) svcEl.textContent = status.running ? 'Running' : 'Stopped';
// Update system resources
var sysEl = document.getElementById('sys-resources');
if (sysEl) dom.content(sysEl, self.renderSystemResources(sysInfo));
// Update chat model select // Update chat model select
var sel = document.getElementById('chat-model'); var sel = document.getElementById('chat-model');
if (sel && models.length > 0) { if (sel && models.length > 0) {

View File

@ -312,6 +312,108 @@ do_generate() {
fi fi
} }
# Get system resources
get_system_info() {
load_config
local rt=$(detect_runtime)
# Memory info
local mem_total=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
local mem_free=$(awk '/MemAvailable/ {print $2}' /proc/meminfo)
local mem_used=$((mem_total - mem_free))
local mem_pct=$((mem_used * 100 / mem_total))
# Disk space for data path
local disk_info=$(df -k "$DATA_PATH" 2>/dev/null | tail -1)
local disk_total=$(echo "$disk_info" | awk '{print $2}')
local disk_used=$(echo "$disk_info" | awk '{print $3}')
local disk_pct=$(echo "$disk_info" | awk '{print $5}' | tr -d '%')
[ -z "$disk_pct" ] && disk_pct=0
# Container stats if running
local container_mem=0
local container_cpu=""
if is_running && [ -n "$rt" ]; then
local stats=$($rt stats --no-stream --format '{{.MemUsage}} {{.CPUPerc}}' ollama 2>/dev/null | head -1)
container_mem=$(echo "$stats" | awk '{print $1}' | sed 's/[^0-9.]//g')
container_cpu=$(echo "$stats" | awk '{print $NF}')
fi
cat <<EOF
{
"memory": {
"total_kb": $mem_total,
"used_kb": $mem_used,
"percent": $mem_pct
},
"disk": {
"total_kb": ${disk_total:-0},
"used_kb": ${disk_used:-0},
"percent": ${disk_pct:-0},
"path": "$DATA_PATH"
},
"container": {
"memory": "${container_mem:-0}",
"cpu": "${container_cpu:-0%}"
}
}
EOF
}
# Get recent logs
get_logs() {
local rt=$(detect_runtime)
local lines="${1:-50}"
echo '{"logs":['
local first=1
if [ -n "$rt" ]; then
$rt logs --tail "$lines" ollama 2>&1 | while IFS= read -r line; do
[ $first -eq 0 ] && printf ','
first=0
line=$(printf '%s' "$line" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ')
printf '"%s"' "$line"
done
fi
echo ']}'
}
# Get model details
get_model_info() {
load_config
local name="$1"
[ -z "$name" ] && { echo '{"error":"Model name required"}'; return; }
if ! is_running; then
echo '{"error":"Ollama not running"}'
return
fi
local rt=$(detect_runtime)
local info=$($rt exec ollama ollama show "$name" 2>&1)
if [ $? -eq 0 ]; then
local params=$(echo "$info" | grep -E "^parameters" | awk '{print $2}')
local family=$(echo "$info" | grep -E "^family" | awk '{print $2}')
local format=$(echo "$info" | grep -E "^format" | awk '{print $2}')
local quant=$(echo "$info" | grep -E "^quantization" | awk '{print $2}')
cat <<EOF
{
"name": "$name",
"parameters": "${params:-unknown}",
"family": "${family:-unknown}",
"format": "${format:-unknown}",
"quantization": "${quant:-unknown}"
}
EOF
else
echo "{\"error\":\"Model not found\"}"
fi
}
# UBUS method list # UBUS method list
case "$1" in case "$1" in
list) list)
@ -321,6 +423,9 @@ case "$1" in
"models": {}, "models": {},
"config": {}, "config": {},
"health": {}, "health": {},
"system_info": {},
"logs": {"lines": 50},
"model_info": {"name": "string"},
"start": {}, "start": {},
"stop": {}, "stop": {},
"restart": {}, "restart": {},
@ -337,6 +442,17 @@ EOF
models) get_models ;; models) get_models ;;
config) get_config ;; config) get_config ;;
health) get_health ;; health) get_health ;;
system_info) get_system_info ;;
logs)
read -r input
lines=$(echo "$input" | jsonfilter -e '@.lines' 2>/dev/null)
get_logs "${lines:-50}"
;;
model_info)
read -r input
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
get_model_info "$name"
;;
start) do_start ;; start) do_start ;;
stop) do_stop ;; stop) do_stop ;;
restart) do_restart ;; restart) do_restart ;;

View File

@ -7,7 +7,10 @@
"status", "status",
"models", "models",
"config", "config",
"health" "health",
"system_info",
"logs",
"model_info"
], ],
"system": [ "info", "board" ], "system": [ "info", "board" ],
"file": [ "read", "stat", "exec" ] "file": [ "read", "stat", "exec" ]

View File

@ -7,38 +7,62 @@
*/ */
var KissThemeClass = baseclass.extend({ var KissThemeClass = baseclass.extend({
// Navigation config - organized by category // Navigation config - organized by category with collapsible sections
nav: [ nav: [
{ cat: 'Dashboard', items: [ { cat: 'Dashboard', icon: '📊', collapsed: false, items: [
{ icon: '🏠', name: 'Home', path: 'admin/secubox-home' }, { icon: '🏠', name: 'Home', path: 'admin/secubox-home' },
{ icon: '📊', name: 'Dashboard', path: 'admin/secubox/dashboard' }, { icon: '📊', name: 'Dashboard', path: 'admin/secubox/dashboard' },
{ icon: '🖥️', name: 'System Hub', path: 'admin/secubox/system/system-hub' } { icon: '🖥️', name: 'System Hub', path: 'admin/secubox/system/system-hub' }
]}, ]},
{ cat: 'Security', items: [ { cat: 'Security', icon: '🛡️', collapsed: false, items: [
{ icon: '🧙', name: 'InterceptoR', path: 'admin/secubox/interceptor' }, { icon: '🧙', name: 'InterceptoR', path: 'admin/secubox/interceptor' },
{ icon: '🛡️', name: 'CrowdSec', path: 'admin/secubox/security/crowdsec' }, { icon: '🛡️', name: 'CrowdSec', path: 'admin/secubox/security/crowdsec' },
{ icon: '🔍', name: 'mitmproxy', path: 'admin/secubox/security/mitmproxy' }, { icon: '🔍', name: 'mitmproxy', path: 'admin/secubox/security/mitmproxy' },
{ 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/services/client-guardian' } { icon: '👁️', name: 'Client Guard', path: 'admin/secubox/security/guardian' },
{ icon: '🍪', name: 'Cookie Track', path: 'admin/secubox/interceptor/cookies' }
]}, ]},
{ cat: 'Services', items: [ { cat: 'Network', icon: '🌐', collapsed: true, items: [
{ icon: '⚖️', name: 'HAProxy', path: 'admin/services/haproxy' }, { icon: '⚖️', name: 'HAProxy', path: 'admin/services/haproxy' },
{ icon: '🔒', name: 'WireGuard', path: 'admin/services/wireguard' }, { 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: 'CDN Cache', path: 'admin/services/cdn-cache' },
{ icon: '🌐', name: 'Network', path: 'admin/network' }, { 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: 'Network Modes', path: 'admin/services/network-modes' },
{ icon: '🔌', name: 'Interfaces', path: 'admin/network/network' }
]}, ]},
{ cat: 'Apps', items: [ { cat: 'AI & LLM', icon: '🤖', collapsed: true, items: [
{ icon: '🎬', name: 'Media Flow', path: 'admin/services/media-flow' }, { icon: '🦙', name: 'Ollama', path: 'admin/services/ollama' },
{ icon: '🤖', name: 'LocalAI', path: 'admin/services/localai' }, { icon: '🤖', name: 'LocalAI', path: 'admin/services/localai' },
{ icon: '💬', name: 'Chat', path: 'admin/services/ollama/chat' }
]},
{ cat: 'Apps', icon: '📦', collapsed: true, items: [
{ icon: '🎬', name: 'Media Flow', path: 'admin/services/media-flow' },
{ icon: '🪞', name: 'MagicMirror', path: 'admin/services/magicmirror2' },
{ icon: '📰', name: 'HexoJS', path: 'admin/services/hexojs' },
{ icon: '📺', name: 'Netdata', path: 'admin/services/netdata-dashboard' },
{ icon: '🏠', name: 'Vhost Manager', path: 'admin/services/vhost-manager' },
{ icon: '📦', name: 'App Store', path: 'admin/secubox/apps' } { icon: '📦', name: 'App Store', path: 'admin/secubox/apps' }
]}, ]},
{ cat: 'System', items: [ { cat: 'P2P & Mesh', icon: '🔗', collapsed: true, items: [
{ icon: '🔗', name: 'P2P Network', path: 'admin/services/secubox-p2p' },
{ icon: '🌳', name: 'Netifyd', path: 'admin/services/secubox-netifyd' },
{ icon: '📡', name: 'Exposure', path: 'admin/services/exposure' }
]},
{ cat: 'System', icon: '⚙️', collapsed: true, items: [
{ icon: '⚙️', name: 'Settings', path: 'admin/system' }, { icon: '⚙️', name: 'Settings', path: 'admin/system' },
{ icon: '🌳', name: 'LuCI Menu', path: 'admin/secubox/luci-tree' } { icon: '📊', name: 'Status', path: 'admin/status/overview' },
{ icon: '🛠️', name: 'KSM Manager', path: 'admin/services/ksm-manager' },
{ icon: '🔄', name: 'Cloner', path: 'admin/services/cloner' },
{ icon: '🌳', name: 'LuCI Menu', path: 'admin/secubox/luci-tree' },
{ icon: '🔧', name: 'Software', path: 'admin/system/opkg' }
]} ]}
], ],
// Track collapsed state per category
collapsedState: {},
// Core palette // Core palette
colors: { colors: {
bg: '#0a0e17', bg: '#0a0e17',
@ -121,20 +145,28 @@ var KissThemeClass = baseclass.extend({
.kiss-sidebar::-webkit-scrollbar-thumb { background: ${c.line}; border-radius: 2px; } .kiss-sidebar::-webkit-scrollbar-thumb { background: ${c.line}; border-radius: 2px; }
.kiss-nav { padding: 8px 0; } .kiss-nav { padding: 8px 0; }
.kiss-nav-section { .kiss-nav-section {
padding: 12px 16px 6px; font-size: 10px; letter-spacing: 1.5px; padding: 10px 16px 8px; font-size: 11px; letter-spacing: 0.5px;
text-transform: uppercase; color: ${c.muted}; font-weight: 600; text-transform: uppercase; color: ${c.muted}; font-weight: 600;
cursor: pointer; display: flex; align-items: center; gap: 8px;
transition: all 0.2s; border-radius: 6px; margin: 2px 8px;
} }
.kiss-nav-section:hover { background: rgba(255,255,255,0.05); color: ${c.text}; }
.kiss-nav-section-icon { font-size: 14px; }
.kiss-nav-section-arrow { margin-left: auto; font-size: 10px; transition: transform 0.2s; }
.kiss-nav-section.collapsed .kiss-nav-section-arrow { transform: rotate(-90deg); }
.kiss-nav-section.collapsed + .kiss-nav-items { display: none; }
.kiss-nav-items { overflow: hidden; transition: all 0.2s; }
.kiss-nav-item { .kiss-nav-item {
display: flex; align-items: center; gap: 10px; padding: 10px 16px; display: flex; align-items: center; gap: 10px; padding: 8px 16px 8px 32px;
text-decoration: none; font-size: 13px; color: ${c.muted}; text-decoration: none; font-size: 12px; color: ${c.muted};
transition: all 0.2s; border-left: 3px solid transparent; margin: 2px 0; transition: all 0.2s; border-left: 3px solid transparent; margin: 1px 0;
} }
.kiss-nav-item:hover { background: rgba(255,255,255,0.05); color: ${c.text}; } .kiss-nav-item:hover { background: rgba(255,255,255,0.05); color: ${c.text}; }
.kiss-nav-item.active { .kiss-nav-item.active {
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-icon { font-size: 16px; width: 22px; text-align: center; flex-shrink: 0; } .kiss-nav-icon { font-size: 14px; width: 20px; text-align: center; flex-shrink: 0; }
/* === Main Content === */ /* === Main Content === */
.kiss-main { .kiss-main {
@ -461,25 +493,64 @@ var KissThemeClass = baseclass.extend({
]); ]);
}, },
// Toggle category collapsed state
toggleCategory: function(catName) {
this.collapsedState[catName] = !this.collapsedState[catName];
var section = document.querySelector('.kiss-nav-section[data-cat="' + catName + '"]');
if (section) {
section.classList.toggle('collapsed', this.collapsedState[catName]);
}
},
// Render sidebar // Render sidebar
renderSidebar: function(activePath) { renderSidebar: function(activePath) {
var self = this; var self = this;
var currentPath = activePath || window.location.pathname.replace('/cgi-bin/luci/', ''); var currentPath = activePath || window.location.pathname.replace('/cgi-bin/luci/', '');
var navItems = []; var navItems = [];
// Initialize collapsed state from nav config
this.nav.forEach(function(cat) { this.nav.forEach(function(cat) {
navItems.push(self.E('div', { 'class': 'kiss-nav-section' }, cat.cat)); if (self.collapsedState[cat.cat] === undefined) {
cat.items.forEach(function(item) { // Auto-expand if current path is in this category
var isActive = currentPath.indexOf(item.path) !== -1; var hasActive = cat.items.some(function(item) {
navItems.push(self.E('a', { return currentPath.indexOf(item.path) !== -1;
'href': '/cgi-bin/luci/' + item.path, });
'class': 'kiss-nav-item' + (isActive ? ' active' : ''), self.collapsedState[cat.cat] = hasActive ? false : (cat.collapsed || false);
'onClick': function() { self.closeSidebar(); } }
}, [ });
self.E('span', { 'class': 'kiss-nav-icon' }, item.icon),
self.E('span', {}, item.name) this.nav.forEach(function(cat) {
])); var isCollapsed = self.collapsedState[cat.cat];
});
// Section header (clickable to expand/collapse)
navItems.push(self.E('div', {
'class': 'kiss-nav-section' + (isCollapsed ? ' collapsed' : ''),
'data-cat': cat.cat,
'onClick': function(e) {
e.preventDefault();
self.toggleCategory(cat.cat);
}
}, [
self.E('span', { 'class': 'kiss-nav-section-icon' }, cat.icon || '📁'),
self.E('span', {}, cat.cat),
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 isActive = currentPath.indexOf(item.path) !== -1;
return self.E('a', {
'href': '/cgi-bin/luci/' + item.path,
'class': 'kiss-nav-item' + (isActive ? ' active' : ''),
'onClick': function() { self.closeSidebar(); }
}, [
self.E('span', { 'class': 'kiss-nav-icon' }, item.icon),
self.E('span', {}, item.name)
]);
})
);
navItems.push(itemsContainer);
}); });
return this.E('nav', { 'class': 'kiss-sidebar' }, [ return this.E('nav', { 'class': 'kiss-sidebar' }, [

View File

@ -1,14 +1,13 @@
{ {
"admin/services/vortex-firewall": { "admin/secubox/security/vortex-firewall": {
"title": "Vortex DNS Firewall", "title": "Vortex Firewall",
"order": 85, "order": 40,
"action": { "action": {
"type": "view", "type": "view",
"path": "vortex-firewall/overview" "path": "vortex-firewall/overview"
}, },
"depends": { "depends": {
"acl": ["luci-app-vortex-firewall"], "acl": ["luci-app-vortex-firewall"]
"uci": { "vortex-firewall": true }
} }
} }
} }