secubox-openwrt/luci-app-netifyd-dashboard/htdocs/luci-static/resources/view/netifyd-dashboard/overview.js

243 lines
8.6 KiB
JavaScript

'use strict';
'require view';
'require secubox-theme/bootstrap as Theme';
'require poll';
'require dom';
'require ui';
'require netifyd-dashboard.api as api';
return view.extend({
title: _('Netifyd Dashboard'),
load: function() {
return Promise.all([
api.getAllData(),
api.getSecuboxLogs()
]);
},
renderDonut: function(data, size) {
size = size || 160;
var total = data.reduce(function(sum, d) { return sum + d.value; }, 0);
var colors = ['#8b5cf6', '#3b82f6', '#06b6d4', '#10b981', '#f59e0b', '#ef4444'];
var cumulative = 0;
var paths = data.map(function(d, i) {
var pct = d.value / total;
var startAngle = cumulative * 2 * Math.PI;
cumulative += pct;
var endAngle = cumulative * 2 * Math.PI;
var r = size / 2 - 10;
var cx = size / 2;
var cy = size / 2;
var x1 = cx + r * Math.sin(startAngle);
var y1 = cy - r * Math.cos(startAngle);
var x2 = cx + r * Math.sin(endAngle);
var y2 = cy - r * Math.cos(endAngle);
var largeArc = pct > 0.5 ? 1 : 0;
return E('path', {
'd': 'M ' + cx + ' ' + cy + ' L ' + x1 + ' ' + y1 + ' A ' + r + ' ' + r + ' 0 ' + largeArc + ' 1 ' + x2 + ' ' + y2 + ' Z',
'fill': colors[i % colors.length]
});
});
return E('svg', { 'width': size, 'height': size, 'viewBox': '0 0 ' + size + ' ' + size }, [
E('g', {}, paths),
E('circle', { 'cx': size/2, 'cy': size/2, 'r': size/2 - 35, 'fill': '#111827' })
]);
},
render: function(payload) {
var self = this;
var data = payload[0] || {};
var logEntries = (payload[1] && payload[1].entries) || [];
var status = data.status || {};
var stats = data.stats || {};
var apps = (data.applications || {}).applications || [];
var protos = (data.protocols || {}).protocols || [];
var totalFlows = stats.total_flows || 0;
var tcpFlows = stats.tcp_flows || 0;
var udpFlows = stats.udp_flows || 0;
var protoData = [
{ name: 'TCP', value: tcpFlows },
{ name: 'UDP', value: udpFlows },
{ name: 'Other', value: Math.max(0, totalFlows - tcpFlows - udpFlows) }
].filter(function(p) { return p.value > 0; });
var topApps = apps.slice(0, 6);
var maxAppBytes = topApps.length > 0 ? Math.max.apply(null, topApps.map(function(a) { return a.bytes; })) : 1;
var view = E('div', { 'class': 'netifyd-dashboard' }, [
// Header
E('div', { 'class': 'nf-header' }, [
E('div', { 'class': 'nf-logo' }, [
E('div', { 'class': 'nf-logo-icon' }, '🔍'),
E('div', { 'class': 'nf-logo-text' }, ['Netif', E('span', {}, 'yd')])
]),
E('div', { 'class': 'nf-header-info' }, [
E('div', {
'class': 'nf-status-badge ' + (status.running ? '' : 'offline')
}, [
E('span', { 'class': 'nf-status-dot' }),
status.running ? 'DPI Active' : 'Offline'
]),
E('div', { 'class': 'nf-version' }, 'v' + (status.version || '4.x'))
])
]),
// Quick Stats
E('div', { 'class': 'nf-quick-stats' }, [
E('div', { 'class': 'nf-quick-stat', 'style': '--stat-color: #8b5cf6' }, [
E('div', { 'class': 'nf-quick-stat-icon' }, '🔄'),
E('div', { 'class': 'nf-quick-stat-value' }, totalFlows),
E('div', { 'class': 'nf-quick-stat-label' }, 'Active Flows'),
E('div', { 'class': 'nf-quick-stat-sub' }, tcpFlows + ' TCP / ' + udpFlows + ' UDP')
]),
E('div', { 'class': 'nf-quick-stat', 'style': '--stat-color: #3b82f6' }, [
E('div', { 'class': 'nf-quick-stat-icon' }, '📱'),
E('div', { 'class': 'nf-quick-stat-value' }, stats.applications || 0),
E('div', { 'class': 'nf-quick-stat-label' }, 'Applications'),
E('div', { 'class': 'nf-quick-stat-sub' }, 'Detected by DPI')
]),
E('div', { 'class': 'nf-quick-stat', 'style': '--stat-color: #06b6d4' }, [
E('div', { 'class': 'nf-quick-stat-icon' }, '💻'),
E('div', { 'class': 'nf-quick-stat-value' }, stats.devices || 0),
E('div', { 'class': 'nf-quick-stat-label' }, 'Devices'),
E('div', { 'class': 'nf-quick-stat-sub' }, 'On network')
]),
E('div', { 'class': 'nf-quick-stat', 'style': '--stat-color: #10b981' }, [
E('div', { 'class': 'nf-quick-stat-icon' }, '📥'),
E('div', { 'class': 'nf-quick-stat-value' }, api.formatBytes(stats.total_rx_bytes || 0)),
E('div', { 'class': 'nf-quick-stat-label' }, 'Downloaded'),
E('div', { 'class': 'nf-quick-stat-sub' }, 'Total RX')
]),
E('div', { 'class': 'nf-quick-stat', 'style': '--stat-color: #f59e0b' }, [
E('div', { 'class': 'nf-quick-stat-icon' }, '📤'),
E('div', { 'class': 'nf-quick-stat-value' }, api.formatBytes(stats.total_tx_bytes || 0)),
E('div', { 'class': 'nf-quick-stat-label' }, 'Uploaded'),
E('div', { 'class': 'nf-quick-stat-sub' }, 'Total TX')
]),
E('div', { 'class': 'nf-quick-stat', 'style': '--stat-color: #ec4899' }, [
E('div', { 'class': 'nf-quick-stat-icon' }, '🏷️'),
E('div', { 'class': 'nf-quick-stat-value' }, stats.categories || 8),
E('div', { 'class': 'nf-quick-stat-label' }, 'Categories'),
E('div', { 'class': 'nf-quick-stat-sub' }, 'Traffic types')
])
]),
// Charts
E('div', { 'class': 'nf-charts-grid' }, [
// Protocol Distribution
E('div', { 'class': 'nf-card' }, [
E('div', { 'class': 'nf-card-header' }, [
E('div', { 'class': 'nf-card-title' }, [
E('span', { 'class': 'nf-card-title-icon' }, '📊'),
'Protocol Distribution'
]),
E('div', { 'class': 'nf-card-badge' }, totalFlows + ' flows')
]),
E('div', { 'class': 'nf-card-body' }, [
E('div', { 'class': 'nf-donut-container' }, [
E('div', { 'class': 'nf-donut' }, [
protoData.length > 0 ? this.renderDonut(protoData, 160) : '',
E('div', { 'class': 'nf-donut-center' }, [
E('div', { 'class': 'nf-donut-value' }, totalFlows),
E('div', { 'class': 'nf-donut-label' }, 'Flows')
])
]),
E('div', { 'class': 'nf-legend' },
protoData.map(function(p, i) {
var colors = ['#8b5cf6', '#3b82f6', '#06b6d4'];
return E('div', { 'class': 'nf-legend-item' }, [
E('div', { 'class': 'nf-legend-dot', 'style': 'background:' + colors[i] }),
E('span', { 'class': 'nf-legend-name' }, p.name),
E('span', { 'class': 'nf-legend-value' }, p.value)
]);
})
)
])
])
]),
// Top Applications
E('div', { 'class': 'nf-card' }, [
E('div', { 'class': 'nf-card-header' }, [
E('div', { 'class': 'nf-card-title' }, [
E('span', { 'class': 'nf-card-title-icon' }, '📱'),
'Top Applications'
]),
E('div', { 'class': 'nf-card-badge' }, apps.length + ' detected')
]),
E('div', { 'class': 'nf-card-body' }, [
E('div', { 'class': 'nf-bar-chart' },
topApps.map(function(app) {
var pct = (app.bytes / maxAppBytes) * 100;
return E('div', { 'class': 'nf-bar-item' }, [
E('div', { 'class': 'nf-bar-label' }, [
E('span', { 'class': 'nf-bar-label-icon' }, api.getAppIcon(app.name)),
app.name
]),
E('div', { 'class': 'nf-bar-track' }, [
E('div', { 'class': 'nf-bar-fill', 'style': 'width:' + pct + '%' })
]),
E('span', { 'class': 'nf-bar-value' }, api.formatBytes(app.bytes))
]);
})
)
])
])
]),
this.renderLogCard(logEntries)
]);
// Include CSS
var cssLink = E('link', { 'rel': 'stylesheet', 'href': L.resource('netifyd-dashboard/dashboard.css') });
document.head.appendChild(cssLink);
return view;
},
renderLogCard: function(entries) {
return E('div', { 'class': 'nf-log-card' }, [
E('div', { 'class': 'nf-log-header' }, [
E('strong', {}, _('SecuBox log tail')),
E('button', {
'class': 'nf-log-btn',
'click': L.bind(this.handleSnapshot, this)
}, _('Snapshot'))
]),
entries && entries.length ?
E('pre', { 'class': 'nf-log-output' }, entries.join('\n')) :
E('p', { 'class': 'nf-log-empty' }, _('Log file empty'))
]);
},
handleSnapshot: function() {
ui.showModal(_('Collecting snapshot'), [
E('p', {}, _('Aggregating dmesg + logread into SecuBox log…')),
E('div', { 'class': 'spinning' })
]);
api.collectDebugSnapshot().then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, _('Snapshot appended to /var/log/seccubox.log')), 'info');
} else {
ui.addNotification(null, E('p', {}, (result && result.error) || _('Snapshot failed')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});