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:
parent
ea5880a76b
commit
2d9beb6f67
@ -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) {
|
||||||
|
|||||||
@ -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 ;;
|
||||||
|
|||||||
@ -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" ]
|
||||||
|
|||||||
@ -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' }, [
|
||||||
|
|||||||
@ -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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user