From 2d9beb6f67e3128b6280302cc91cd785bf4d5279 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Thu, 12 Feb 2026 06:56:26 +0100 Subject: [PATCH] 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 --- .../resources/view/ollama/dashboard.js | 165 +++++++++++++++++- .../root/usr/libexec/rpcd/luci.ollama | 116 ++++++++++++ .../usr/share/rpcd/acl.d/luci-app-ollama.json | 5 +- .../resources/secubox/kiss-theme.js | 127 +++++++++++--- .../luci/menu.d/luci-app-vortex-firewall.json | 9 +- 5 files changed, 382 insertions(+), 40 deletions(-) diff --git a/package/secubox/luci-app-ollama/htdocs/luci-static/resources/view/ollama/dashboard.js b/package/secubox/luci-app-ollama/htdocs/luci-static/resources/view/ollama/dashboard.js index 983995c1..39d4ab65 100644 --- a/package/secubox/luci-app-ollama/htdocs/luci-static/resources/view/ollama/dashboard.js +++ b/package/secubox/luci-app-ollama/htdocs/luci-static/resources/view/ollama/dashboard.js @@ -9,6 +9,9 @@ var api = { status: rpc.declare({ object: 'luci.ollama', method: 'status' }), models: rpc.declare({ object: 'luci.ollama', method: 'models' }), 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' }), stop: rpc.declare({ object: 'luci.ollama', method: 'stop' }), 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.success { background: var(--ol-success); 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() { return Promise.all([ 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 status = data[0] || {}; var models = (data[1] && data[1].models) || []; + var sysInfo = data[2] || {}; + var logs = (data[3] && data[3].logs) || []; this.isRunning = status.running; + this.sysInfo = sysInfo; + this.logs = logs; var view = E('div', { 'class': 'ol-wrap' }, [ 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') ]) ]) + ]), + + // 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) { + var sysInfo = this.sysInfo || {}; + var memPct = (sysInfo.memory && sysInfo.memory.percent) || 0; + var diskPct = (sysInfo.disk && sysInfo.disk.percent) || 0; return [ E('div', { 'class': 'ol-stat' }, [ 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' }, [ - E('div', { 'class': 'ol-stat-val' }, (status.api_port || 11434).toString()), - E('div', { 'class': 'ol-stat-lbl' }, 'API Port') + E('div', { 'class': 'ol-stat-val' }, memPct + '%'), + E('div', { 'class': 'ol-stat-lbl' }, 'RAM Used') ]), E('div', { 'class': 'ol-stat' }, [ - E('div', { 'class': 'ol-stat-val' }, status.runtime || '-'), - E('div', { 'class': 'ol-stat-lbl' }, 'Runtime') + E('div', { 'class': 'ol-stat-val' }, diskPct + '%'), + 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: [ { 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' }, @@ -266,11 +412,14 @@ return view.extend({ var self = this; return Promise.all([ 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) { var status = data[0] || {}; var models = (data[1] && data[1].models) || []; + var sysInfo = data[2] || {}; self.isRunning = status.running; + self.sysInfo = sysInfo; var badge = document.getElementById('ol-status'); if (badge) { @@ -290,6 +439,10 @@ return view.extend({ var svcEl = document.getElementById('svc-status'); 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 var sel = document.getElementById('chat-model'); if (sel && models.length > 0) { diff --git a/package/secubox/luci-app-ollama/root/usr/libexec/rpcd/luci.ollama b/package/secubox/luci-app-ollama/root/usr/libexec/rpcd/luci.ollama index 19b8715b..b71cec31 100755 --- a/package/secubox/luci-app-ollama/root/usr/libexec/rpcd/luci.ollama +++ b/package/secubox/luci-app-ollama/root/usr/libexec/rpcd/luci.ollama @@ -312,6 +312,108 @@ do_generate() { 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 <&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 </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 ;; stop) do_stop ;; restart) do_restart ;; diff --git a/package/secubox/luci-app-ollama/root/usr/share/rpcd/acl.d/luci-app-ollama.json b/package/secubox/luci-app-ollama/root/usr/share/rpcd/acl.d/luci-app-ollama.json index 34265f37..3da7efbe 100644 --- a/package/secubox/luci-app-ollama/root/usr/share/rpcd/acl.d/luci-app-ollama.json +++ b/package/secubox/luci-app-ollama/root/usr/share/rpcd/acl.d/luci-app-ollama.json @@ -7,7 +7,10 @@ "status", "models", "config", - "health" + "health", + "system_info", + "logs", + "model_info" ], "system": [ "info", "board" ], "file": [ "read", "stat", "exec" ] diff --git a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js index 545940df..25b3ba66 100644 --- a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js +++ b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/secubox/kiss-theme.js @@ -7,38 +7,62 @@ */ var KissThemeClass = baseclass.extend({ - // Navigation config - organized by category + // Navigation config - organized by category with collapsible sections nav: [ - { cat: 'Dashboard', items: [ + { cat: 'Dashboard', icon: '📊', collapsed: false, items: [ { icon: '🏠', name: 'Home', path: 'admin/secubox-home' }, { icon: '📊', name: 'Dashboard', path: 'admin/secubox/dashboard' }, { 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: 'CrowdSec', path: 'admin/secubox/security/crowdsec' }, { icon: '🔍', name: 'mitmproxy', path: 'admin/secubox/security/mitmproxy' }, { 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: '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: '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: [ - { icon: '🎬', name: 'Media Flow', path: 'admin/services/media-flow' }, + { cat: 'AI & LLM', icon: '🤖', collapsed: true, items: [ + { icon: '🦙', name: 'Ollama', path: 'admin/services/ollama' }, { icon: '🤖', name: 'LocalAI', path: 'admin/services/localai' }, + { icon: '💬', name: 'Chat', path: 'admin/services/ollama/chat' } + ]}, + { 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' } ]}, - { 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: '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 colors: { bg: '#0a0e17', @@ -121,20 +145,28 @@ var KissThemeClass = baseclass.extend({ .kiss-sidebar::-webkit-scrollbar-thumb { background: ${c.line}; border-radius: 2px; } .kiss-nav { padding: 8px 0; } .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; + 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 { - display: flex; align-items: center; gap: 10px; padding: 10px 16px; - text-decoration: none; font-size: 13px; color: ${c.muted}; - transition: all 0.2s; border-left: 3px solid transparent; margin: 2px 0; + display: flex; align-items: center; gap: 10px; padding: 8px 16px 8px 32px; + text-decoration: none; font-size: 12px; color: ${c.muted}; + 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.active { color: ${c.green}; background: rgba(0,200,83,0.08); 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 === */ .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 renderSidebar: function(activePath) { var self = this; var currentPath = activePath || window.location.pathname.replace('/cgi-bin/luci/', ''); var navItems = []; + // Initialize collapsed state from nav config this.nav.forEach(function(cat) { - navItems.push(self.E('div', { 'class': 'kiss-nav-section' }, cat.cat)); - cat.items.forEach(function(item) { - var isActive = currentPath.indexOf(item.path) !== -1; - navItems.push(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) - ])); - }); + if (self.collapsedState[cat.cat] === undefined) { + // Auto-expand if current path is in this category + var hasActive = cat.items.some(function(item) { + return currentPath.indexOf(item.path) !== -1; + }); + self.collapsedState[cat.cat] = hasActive ? false : (cat.collapsed || false); + } + }); + + 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' }, [ diff --git a/package/secubox/luci-app-vortex-firewall/root/usr/share/luci/menu.d/luci-app-vortex-firewall.json b/package/secubox/luci-app-vortex-firewall/root/usr/share/luci/menu.d/luci-app-vortex-firewall.json index 29755329..4b09b6aa 100644 --- a/package/secubox/luci-app-vortex-firewall/root/usr/share/luci/menu.d/luci-app-vortex-firewall.json +++ b/package/secubox/luci-app-vortex-firewall/root/usr/share/luci/menu.d/luci-app-vortex-firewall.json @@ -1,14 +1,13 @@ { - "admin/services/vortex-firewall": { - "title": "Vortex DNS Firewall", - "order": 85, + "admin/secubox/security/vortex-firewall": { + "title": "Vortex Firewall", + "order": 40, "action": { "type": "view", "path": "vortex-firewall/overview" }, "depends": { - "acl": ["luci-app-vortex-firewall"], - "uci": { "vortex-firewall": true } + "acl": ["luci-app-vortex-firewall"] } } }