secubox-openwrt/package/secubox/luci-app-localai/htdocs/luci-static/resources/view/localai/dashboard.js
CyberMind-FR e58f479cd4 feat(waf): Update WAF scenarios with 2024-2025 CVEs and OWASP threats
Add detection patterns for latest actively exploited vulnerabilities:
- CVE-2025-55182 (React2Shell, CVSS 10.0)
- CVE-2025-8110 (Gogs RCE), CVE-2025-53770 (SharePoint)
- CVE-2025-52691 (SmarterMail), CVE-2025-40551 (SolarWinds)
- CVE-2024-47575 (FortiManager), CVE-2024-21887 (Ivanti)
- CVE-2024-3400, CVE-2024-0012, CVE-2024-9474 (PAN-OS)

New attack categories based on OWASP Top 10 2025:
- HTTP Request Smuggling (TE.CL/CL.TE conflicts)
- AI/LLM Prompt Injection (ChatML, instruction markers)
- WAF Bypass techniques (Unicode normalization, double encoding)
- Supply Chain attacks (CI/CD poisoning, dependency confusion)
- Extended SSTI (Jinja2, Freemarker, Velocity, Thymeleaf)
- API Abuse (BOLA/IDOR, mass assignment)

CrowdSec scenarios split into 11 separate files for reliability.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-12 05:02:57 +01:00

607 lines
17 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require ui';
'require rpc';
'require secubox/kiss-theme';
var callStatus = rpc.declare({
object: 'luci.localai',
method: 'status',
expect: { }
});
var callModels = rpc.declare({
object: 'luci.localai',
method: 'models',
expect: { models: [] }
});
var callHealth = rpc.declare({
object: 'luci.localai',
method: 'health',
expect: { }
});
var callMetrics = rpc.declare({
object: 'luci.localai',
method: 'metrics',
expect: { }
});
var callStart = rpc.declare({
object: 'luci.localai',
method: 'start',
expect: { }
});
var callStop = rpc.declare({
object: 'luci.localai',
method: 'stop',
expect: { }
});
var callRestart = rpc.declare({
object: 'luci.localai',
method: 'restart',
expect: { }
});
function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatUptime(seconds) {
if (!seconds) return 'N/A';
var days = Math.floor(seconds / 86400);
var hours = Math.floor((seconds % 86400) / 3600);
var mins = Math.floor((seconds % 3600) / 60);
if (days > 0) return days + 'd ' + hours + 'h';
if (hours > 0) return hours + 'h ' + mins + 'm';
return mins + 'm';
}
return view.extend({
title: _('LocalAI Dashboard'),
refreshInterval: 5000,
data: null,
load: function() {
return Promise.all([
callStatus(),
callModels(),
callHealth(),
callMetrics()
]).then(function(results) {
console.log('LocalAI Dashboard RPC results:', JSON.stringify(results));
// RPC with expect returns arrays directly, not wrapped objects
var modelsData = Array.isArray(results[1]) ? results[1] : [];
return {
status: results[0] || {},
models: modelsData,
health: results[2] || {},
metrics: results[3] || {}
};
});
},
render: function(data) {
var self = this;
this.data = data;
var container = E('div', { 'class': 'localai-dashboard' }, [
// Header
E('div', { 'class': 'lai-header' }, [
E('div', { 'class': 'lai-logo' }, [
E('div', { 'class': 'lai-logo-icon' }, '🤖'),
E('div', { 'class': 'lai-logo-text' }, [
E('span', {}, 'Local'),
'AI'
])
]),
E('div', { 'class': 'lai-header-info' }, [
E('div', {
'class': 'lai-status-badge ' + (data.status.running ? '' : 'offline'),
'id': 'lai-status-badge'
}, [
E('span', { 'class': 'lai-status-dot' }),
data.status.running ? _('Running') : _('Stopped')
])
])
]),
// Quick Stats
E('div', { 'class': 'lai-quick-stats' }, [
E('div', { 'class': 'lai-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #a855f7, #6366f1)' }, [
E('div', { 'class': 'lai-quick-stat-header' }, [
E('span', { 'class': 'lai-quick-stat-icon' }, '📊'),
E('span', { 'class': 'lai-quick-stat-label' }, _('Models'))
]),
E('div', { 'class': 'lai-quick-stat-value', 'id': 'models-count' },
(data.models || []).length.toString()
),
E('div', { 'class': 'lai-quick-stat-sub' }, _('Installed'))
]),
E('div', { 'class': 'lai-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #10b981, #059669)' }, [
E('div', { 'class': 'lai-quick-stat-header' }, [
E('span', { 'class': 'lai-quick-stat-icon' }, '💾'),
E('span', { 'class': 'lai-quick-stat-label' }, _('Memory'))
]),
E('div', { 'class': 'lai-quick-stat-value', 'id': 'memory-used' },
formatBytes(data.metrics.memory_used || 0)
),
E('div', { 'class': 'lai-quick-stat-sub' }, _('Used'))
]),
E('div', { 'class': 'lai-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #06b6d4, #0ea5e9)' }, [
E('div', { 'class': 'lai-quick-stat-header' }, [
E('span', { 'class': 'lai-quick-stat-icon' }, ''),
E('span', { 'class': 'lai-quick-stat-label' }, _('Uptime'))
]),
E('div', { 'class': 'lai-quick-stat-value', 'id': 'uptime' },
data.status.running ? formatUptime(data.status.uptime) : '--'
),
E('div', { 'class': 'lai-quick-stat-sub' }, _('Running'))
]),
E('div', { 'class': 'lai-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #f59e0b, #d97706)' }, [
E('div', { 'class': 'lai-quick-stat-header' }, [
E('span', { 'class': 'lai-quick-stat-icon' }, '🔌'),
E('span', { 'class': 'lai-quick-stat-label' }, _('API Port'))
]),
E('div', { 'class': 'lai-quick-stat-value' }, data.status.api_port || '8080'),
E('div', { 'class': 'lai-quick-stat-sub' }, _('Endpoint'))
])
]),
// Main Cards Grid
E('div', { 'class': 'lai-cards-grid' }, [
// Service Control Card
E('div', { 'class': 'lai-card' }, [
E('div', { 'class': 'lai-card-header' }, [
E('div', { 'class': 'lai-card-title' }, [
E('span', { 'class': 'lai-card-title-icon' }, ''),
_('Service Control')
]),
E('div', {
'class': 'lai-card-badge ' + (data.status.running ? 'running' : 'stopped')
}, data.status.running ? _('Active') : _('Inactive'))
]),
E('div', { 'class': 'lai-card-body' }, [
E('div', { 'class': 'lai-service-info' }, [
E('div', { 'class': 'lai-service-row' }, [
E('span', { 'class': 'lai-service-label' }, _('Status')),
E('span', {
'class': 'lai-service-value ' + (data.status.running ? 'running' : 'stopped'),
'id': 'service-status'
}, data.status.running ? _('Running') : _('Stopped'))
]),
E('div', { 'class': 'lai-service-row' }, [
E('span', { 'class': 'lai-service-label' }, _('Memory Limit')),
E('span', { 'class': 'lai-service-value' }, data.status.memory_limit || '2G')
]),
E('div', { 'class': 'lai-service-row' }, [
E('span', { 'class': 'lai-service-label' }, _('Threads')),
E('span', { 'class': 'lai-service-value' }, data.status.threads || '4')
]),
E('div', { 'class': 'lai-service-row' }, [
E('span', { 'class': 'lai-service-label' }, _('Context Size')),
E('span', { 'class': 'lai-service-value' }, data.status.context_size || '2048')
])
]),
E('div', { 'class': 'lai-service-controls' }, [
E('button', {
'class': 'lai-btn lai-btn-success' + (data.status.running ? ' disabled' : ''),
'click': function() { self.handleServiceAction('start'); },
'disabled': data.status.running
}, [E('span', {}, ''), _('Start')]),
E('button', {
'class': 'lai-btn lai-btn-danger' + (!data.status.running ? ' disabled' : ''),
'click': function() { self.handleServiceAction('stop'); },
'disabled': !data.status.running
}, [E('span', {}, ''), _('Stop')]),
E('button', {
'class': 'lai-btn lai-btn-warning',
'click': function() { self.handleServiceAction('restart'); }
}, [E('span', {}, '🔄'), _('Restart')])
])
])
]),
// Models Card
E('div', { 'class': 'lai-card' }, [
E('div', { 'class': 'lai-card-header' }, [
E('div', { 'class': 'lai-card-title' }, [
E('span', { 'class': 'lai-card-title-icon' }, '🧠'),
_('Installed Models')
]),
E('div', { 'class': 'lai-card-badge' },
(data.models || []).length + ' ' + _('models')
)
]),
E('div', { 'class': 'lai-card-body' }, [
this.renderModelsList(data.models || [])
])
])
]),
// API Info Card
E('div', { 'class': 'lai-card', 'style': 'margin-top: 20px' }, [
E('div', { 'class': 'lai-card-header' }, [
E('div', { 'class': 'lai-card-title' }, [
E('span', { 'class': 'lai-card-title-icon' }, '🔗'),
_('API Endpoints')
])
]),
E('div', { 'class': 'lai-card-body' }, [
E('div', { 'class': 'lai-api-info' }, [
E('div', { 'class': 'lai-api-endpoint' }, [
E('code', {}, 'http://' + window.location.hostname + ':' + (data.status.api_port || '8080') + '/v1/chat/completions'),
E('span', { 'class': 'lai-api-method' }, 'POST'),
E('span', { 'class': 'lai-api-desc' }, _('Chat completion'))
]),
E('div', { 'class': 'lai-api-endpoint' }, [
E('code', {}, 'http://' + window.location.hostname + ':' + (data.status.api_port || '8080') + '/v1/models'),
E('span', { 'class': 'lai-api-method get' }, 'GET'),
E('span', { 'class': 'lai-api-desc' }, _('List models'))
])
])
])
])
]);
// Include CSS
var style = E('style', {}, this.getCSS());
container.insertBefore(style, container.firstChild);
return KissTheme.wrap(container, 'admin/services/localai/dashboard');
},
renderModelsList: function(models) {
if (!models || models.length === 0) {
return E('div', { 'class': 'lai-empty' }, [
E('div', { 'class': 'lai-empty-icon' }, '📦'),
E('div', { 'class': 'lai-empty-text' }, _('No models installed')),
E('div', { 'class': 'lai-empty-hint' }, [
_('Install a model with: '),
E('code', {}, 'localaictl model-install tinyllama')
])
]);
}
return E('div', { 'class': 'lai-models-list' },
models.map(function(model) {
var displayName = model.id || model.name;
return E('div', { 'class': 'lai-model-item' + (model.loaded ? ' loaded' : '') }, [
E('div', { 'class': 'lai-model-icon' }, model.loaded ? '✅' : '🤖'),
E('div', { 'class': 'lai-model-info' }, [
E('div', { 'class': 'lai-model-name' }, displayName),
E('div', { 'class': 'lai-model-meta' }, [
model.size > 0 ? E('span', { 'class': 'lai-model-size' }, formatBytes(model.size)) : null,
E('span', { 'class': 'lai-model-type' }, model.loaded ? _('Active') : model.type)
].filter(Boolean))
])
]);
})
);
},
handleServiceAction: function(action) {
var self = this;
ui.showModal(_('Service Control'), [
E('p', {}, _('Processing...')),
E('div', { 'class': 'spinning' })
]);
var actionFn;
switch(action) {
case 'start': actionFn = callStart(); break;
case 'stop': actionFn = callStop(); break;
case 'restart': actionFn = callRestart(); break;
}
actionFn.then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Service ' + action + ' successful')), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', result.error || _('Operation failed')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', err.message), 'error');
});
},
getCSS: function() {
return `
.localai-dashboard {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: #030712;
color: #f8fafc;
min-height: 100vh;
padding: 16px;
}
.lai-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0 20px;
border-bottom: 1px solid #334155;
margin-bottom: 20px;
}
.lai-logo {
display: flex;
align-items: center;
gap: 14px;
}
.lai-logo-icon {
width: 46px;
height: 46px;
background: linear-gradient(135deg, #a855f7, #6366f1);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.lai-logo-text {
font-size: 24px;
font-weight: 700;
}
.lai-logo-text span {
background: linear-gradient(135deg, #a855f7, #6366f1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.lai-status-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 24px;
background: rgba(16, 185, 129, 0.15);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
font-weight: 600;
}
.lai-status-badge.offline {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border-color: rgba(239, 68, 68, 0.3);
}
.lai-status-dot {
width: 10px;
height: 10px;
background: currentColor;
border-radius: 50%;
}
.lai-quick-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 14px;
margin-bottom: 24px;
}
.lai-quick-stat {
background: #0f172a;
border: 1px solid #334155;
border-radius: 12px;
padding: 20px;
position: relative;
overflow: hidden;
}
.lai-quick-stat::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--stat-gradient);
}
.lai-quick-stat-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.lai-quick-stat-icon { font-size: 22px; }
.lai-quick-stat-label {
font-size: 11px;
text-transform: uppercase;
color: #64748b;
}
.lai-quick-stat-value {
font-size: 32px;
font-weight: 700;
background: var(--stat-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.lai-quick-stat-sub {
font-size: 11px;
color: #64748b;
margin-top: 6px;
}
.lai-cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
}
.lai-card {
background: #0f172a;
border: 1px solid #334155;
border-radius: 12px;
overflow: hidden;
}
.lai-card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #334155;
background: rgba(0, 0, 0, 0.3);
}
.lai-card-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 15px;
font-weight: 600;
}
.lai-card-title-icon { font-size: 20px; }
.lai-card-badge {
font-size: 12px;
padding: 5px 12px;
border-radius: 16px;
background: linear-gradient(135deg, #a855f7, #6366f1);
color: white;
}
.lai-card-badge.running { background: linear-gradient(135deg, #10b981, #059669); }
.lai-card-badge.stopped { background: rgba(100, 116, 139, 0.3); color: #94a3b8; }
.lai-card-body { padding: 20px; }
.lai-service-info {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 20px;
}
.lai-service-row {
display: flex;
justify-content: space-between;
padding: 8px 12px;
background: #030712;
border-radius: 8px;
}
.lai-service-label { color: #94a3b8; font-size: 13px; }
.lai-service-value { font-size: 13px; }
.lai-service-value.running { color: #10b981; }
.lai-service-value.stopped { color: #ef4444; }
.lai-service-controls {
display: flex;
gap: 10px;
}
.lai-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 16px;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
}
.lai-btn-success {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
}
.lai-btn-danger {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
}
.lai-btn-warning {
background: linear-gradient(135deg, #f59e0b, #d97706);
color: white;
}
.lai-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.lai-models-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.lai-model-item {
display: flex;
align-items: center;
gap: 14px;
padding: 14px;
background: #1e293b;
border-radius: 10px;
}
.lai-model-item.loaded {
border: 1px solid rgba(16, 185, 129, 0.3);
background: rgba(16, 185, 129, 0.05);
}
.lai-model-icon {
width: 44px;
height: 44px;
background: linear-gradient(135deg, #a855f7, #6366f1);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.lai-model-name {
font-weight: 600;
margin-bottom: 4px;
}
.lai-model-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: #94a3b8;
}
.lai-model-type {
padding: 2px 8px;
background: #334155;
border-radius: 4px;
}
.lai-empty {
text-align: center;
padding: 40px 20px;
color: #64748b;
}
.lai-empty-icon { font-size: 48px; margin-bottom: 12px; }
.lai-empty-text { font-size: 16px; margin-bottom: 8px; }
.lai-empty-hint { font-size: 13px; }
.lai-empty-hint code {
background: #1e293b;
padding: 4px 8px;
border-radius: 4px;
}
.lai-api-info {
display: flex;
flex-direction: column;
gap: 10px;
}
.lai-api-endpoint {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #030712;
border-radius: 8px;
}
.lai-api-endpoint code {
font-size: 12px;
color: #06b6d4;
flex: 1;
}
.lai-api-method {
padding: 4px 8px;
background: #f59e0b;
color: #030712;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
}
.lai-api-method.get { background: #10b981; }
.lai-api-desc {
font-size: 12px;
color: #94a3b8;
min-width: 120px;
}
`;
}
});