secubox-openwrt/package/secubox/luci-app-bandwidth-manager/htdocs/luci-static/resources/view/bandwidth-manager/analytics.js
CyberMind-FR 1bbd345cee refactor(luci): Mass KissTheme UI rework across all LuCI apps
Convert 90+ LuCI view files from legacy cbi-button-* classes to
KissTheme kiss-btn-* classes for consistent dark theme styling.

Pattern conversions applied:
- cbi-button-positive → kiss-btn-green
- cbi-button-negative/remove → kiss-btn-red
- cbi-button-apply → kiss-btn-cyan
- cbi-button-action → kiss-btn-blue
- cbi-button (plain) → kiss-btn

Also replaced hardcoded colors (#080, #c00, #888, etc.) with
CSS variables (--kiss-green, --kiss-red, --kiss-muted, etc.)
for proper dark theme compatibility.

Apps updated include: ai-gateway, auth-guardian, bandwidth-manager,
cloner, config-advisor, crowdsec-dashboard, dns-provider, exposure,
glances, haproxy, hexojs, iot-guard, jellyfin, ksm-manager,
mac-guardian, magicmirror2, master-link, meshname-dns, metablogizer,
metabolizer, mqtt-bridge, netdata-dashboard, picobrew, routes-status,
secubox-admin, secubox-mirror, secubox-p2p, secubox-security-threats,
service-registry, simplex, streamlit, system-hub, tor-shield,
traffic-shaper, vhost-manager, vortex-dns, vortex-firewall,
webradio, wireguard-dashboard, zigbee2mqtt, zkp, and more.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-12 11:09:34 +01:00

297 lines
10 KiB
JavaScript

'use strict';
'require view';
'require dom';
'require poll';
'require rpc';
'require ui';
'require secubox/kiss-theme';
var callGetAnalyticsSummary = rpc.declare({
object: 'luci.bandwidth-manager',
method: 'get_analytics_summary',
params: ['period'],
expect: {}
});
var callGetHourlyData = rpc.declare({
object: 'luci.bandwidth-manager',
method: 'get_hourly_data',
params: ['days'],
expect: { hourly_data: [] }
});
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
summary: {},
hourlyData: [],
selectedPeriod: '24h',
load: function() {
return Promise.all([
callGetAnalyticsSummary('24h'),
callGetHourlyData(7)
]);
},
renderStats: function() {
var c = KissTheme.colors;
var totalTraffic = (this.summary.total_rx_bytes || 0) + (this.summary.total_tx_bytes || 0);
return [
KissTheme.stat(this.formatBytes(this.summary.total_rx_bytes || 0), 'Download', c.green),
KissTheme.stat(this.formatBytes(this.summary.total_tx_bytes || 0), 'Upload', c.blue),
KissTheme.stat(this.summary.active_clients || 0, 'Active Clients', c.purple),
KissTheme.stat(this.formatBytes(totalTraffic), 'Total Traffic', c.orange)
];
},
renderPeriodSelector: function() {
var self = this;
var periods = [
{ id: '1h', label: '1 Hour' },
{ id: '6h', label: '6 Hours' },
{ id: '24h', label: '24 Hours' },
{ id: '7d', label: '7 Days' },
{ id: '30d', label: '30 Days' }
];
return E('div', { 'style': 'display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px;' },
periods.map(function(p) {
var isActive = self.selectedPeriod === p.id;
return E('button', {
'class': 'kiss-btn' + (isActive ? ' kiss-btn-blue' : ''),
'data-period': p.id,
'click': function() {
self.selectedPeriod = p.id;
document.querySelectorAll('[data-period]').forEach(function(btn) {
btn.classList.remove('kiss-btn-blue');
});
this.classList.add('kiss-btn-blue');
self.pollData();
}
}, p.label);
})
);
},
renderAppBreakdown: function() {
var self = this;
var appBreakdown = this.summary.app_breakdown || [];
if (appBreakdown.length === 0) {
return E('p', { 'style': 'color: var(--kiss-muted); text-align: center; padding: 30px;' },
'No application data available');
}
var maxBytes = Math.max.apply(null, appBreakdown.map(function(a) { return a.bytes || 0; })) || 1;
var colors = ['var(--kiss-blue)', 'var(--kiss-green)', 'var(--kiss-orange)', 'var(--kiss-purple)', 'var(--kiss-cyan)', 'var(--kiss-red)'];
return E('div', { 'style': 'display: flex; flex-direction: column; gap: 12px;' },
appBreakdown.slice(0, 8).map(function(app, idx) {
var percent = Math.round((app.bytes / maxBytes) * 100);
var color = colors[idx % colors.length];
return E('div', {}, [
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 4px;' }, [
E('span', { 'style': 'font-size: 13px;' }, app.app || 'Unknown'),
E('span', { 'style': 'font-size: 13px; color: var(--kiss-muted);' }, self.formatBytes(app.bytes || 0))
]),
E('div', {
'style': 'height: 8px; background: var(--kiss-bg); border-radius: 4px; overflow: hidden;'
}, [
E('div', {
'style': 'height: 100%; width: ' + percent + '%; background: ' + color + ';'
})
])
]);
})
);
},
renderProtocolBreakdown: function() {
var protocols = this.summary.protocol_breakdown || [];
if (protocols.length === 0) {
return E('p', { 'style': 'color: var(--kiss-muted); text-align: center; padding: 30px;' },
'No protocol data available');
}
var total = protocols.reduce(function(sum, p) { return sum + (p.bytes || 0); }, 0) || 1;
var colors = ['var(--kiss-blue)', 'var(--kiss-green)', 'var(--kiss-orange)', 'var(--kiss-purple)', 'var(--kiss-cyan)'];
return E('div', { 'style': 'display: flex; flex-direction: column; gap: 8px;' },
protocols.slice(0, 5).map(function(p, idx) {
var percent = Math.round((p.bytes || 0) / total * 100);
return E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
E('div', {
'style': 'width: 12px; height: 12px; background: ' + colors[idx] + '; border-radius: 2px;'
}),
E('span', { 'style': 'flex: 1;' }, p.protocol || 'Unknown'),
E('span', { 'style': 'color: var(--kiss-muted);' }, percent + '%')
]);
})
);
},
renderTopTalkers: function() {
var self = this;
var topTalkers = this.summary.top_talkers || [];
if (topTalkers.length === 0) {
return E('p', { 'style': 'color: var(--kiss-muted); text-align: center; padding: 30px;' },
'No usage data available');
}
var medals = ['\ud83e\udd47', '\ud83e\udd48', '\ud83e\udd49'];
var rows = topTalkers.map(function(client, idx) {
return E('tr', {}, [
E('td', {}, [
E('div', { 'style': 'display: flex; align-items: center; gap: 8px;' }, [
E('span', { 'style': 'font-size: 18px;' }, medals[idx] || ''),
E('div', {}, [
E('div', { 'style': 'font-weight: 500;' }, client.hostname || 'Unknown'),
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted);' }, client.mac)
])
])
]),
E('td', {}, client.ip || '-'),
E('td', { 'style': 'text-align: right; font-weight: 600;' }, self.formatMB(client.used_mb || 0))
]);
});
return E('table', { 'class': 'kiss-table' }, [
E('thead', {}, [
E('tr', {}, [
E('th', {}, 'Device'),
E('th', {}, 'IP'),
E('th', { 'style': 'text-align: right;' }, 'Usage')
])
]),
E('tbody', {}, rows)
]);
},
renderSummaryStats: function() {
var self = this;
var rxTxRatio = this.summary.total_tx_bytes > 0 ?
((this.summary.total_rx_bytes || 0) / (this.summary.total_tx_bytes || 1)).toFixed(1) + ':1' : 'N/A';
var avgPerClient = this.summary.active_clients > 0 ?
this.formatBytes(((this.summary.total_rx_bytes || 0) + (this.summary.total_tx_bytes || 0)) / this.summary.active_clients) : 'N/A';
return E('div', { 'style': 'display: flex; flex-direction: column; gap: 12px;' }, [
E('div', { 'style': 'padding: 12px; background: var(--kiss-bg); border-radius: 8px;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 4px;' }, [
E('span', { 'style': 'color: var(--kiss-muted);' }, 'Download/Upload Ratio'),
E('span', { 'style': 'font-weight: 600;' }, rxTxRatio)
]),
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted);' }, 'Typical ratio is 5:1 to 10:1 for home networks')
]),
E('div', { 'style': 'padding: 12px; background: var(--kiss-bg); border-radius: 8px;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 4px;' }, [
E('span', { 'style': 'color: var(--kiss-muted);' }, 'Average per Client'),
E('span', { 'style': 'font-weight: 600;' }, avgPerClient)
]),
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted);' }, 'Based on ' + (this.summary.active_clients || 0) + ' active devices')
]),
E('div', { 'style': 'padding: 12px; background: var(--kiss-bg); border-radius: 8px;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 4px;' }, [
E('span', { 'style': 'color: var(--kiss-muted);' }, 'Applications Detected'),
E('span', { 'style': 'font-weight: 600;' }, (this.summary.app_breakdown || []).length.toString())
]),
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted);' }, 'Via Deep Packet Inspection')
])
]);
},
render: function(data) {
var self = this;
this.summary = data[0] || {};
this.hourlyData = (data[1] && data[1].hourly_data) || [];
poll.add(L.bind(this.pollData, this), 30);
var content = [
// Header
E('div', { 'style': 'margin-bottom: 24px;' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'Bandwidth Analytics'),
KissTheme.badge(this.selectedPeriod, 'blue')
]),
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' },
'Traffic analysis, usage trends, and application breakdown')
]),
// Period Selector
this.renderPeriodSelector(),
// Stats
E('div', { 'class': 'kiss-grid kiss-grid-4', 'id': 'analytics-stats', 'style': 'margin: 20px 0;' },
this.renderStats()),
// Charts Row
E('div', { 'class': 'kiss-grid kiss-grid-2', 'id': 'analytics-charts', 'style': 'margin-bottom: 20px;' }, [
KissTheme.card('Traffic by Application', E('div', { 'id': 'app-breakdown' }, this.renderAppBreakdown())),
KissTheme.card('Traffic by Protocol', E('div', { 'id': 'protocol-breakdown' }, this.renderProtocolBreakdown()))
]),
// Details Row
E('div', { 'class': 'kiss-grid kiss-grid-2', 'id': 'analytics-details' }, [
KissTheme.card(
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
E('span', {}, 'Top Bandwidth Users'),
KissTheme.badge((this.summary.top_talkers || []).length + ' users', 'purple')
]),
E('div', { 'id': 'top-talkers' }, this.renderTopTalkers())
),
KissTheme.card('Analytics Summary', E('div', { 'id': 'summary-stats' }, this.renderSummaryStats()))
])
];
return KissTheme.wrap(content, 'admin/services/bandwidth-manager/analytics');
},
pollData: function() {
var self = this;
return callGetAnalyticsSummary(this.selectedPeriod).then(function(data) {
self.summary = data || {};
self.updateDisplay();
});
},
updateDisplay: function() {
var statsEl = document.getElementById('analytics-stats');
var appEl = document.getElementById('app-breakdown');
var protoEl = document.getElementById('protocol-breakdown');
var talkersEl = document.getElementById('top-talkers');
var summaryEl = document.getElementById('summary-stats');
if (statsEl) dom.content(statsEl, this.renderStats());
if (appEl) dom.content(appEl, this.renderAppBreakdown());
if (protoEl) dom.content(protoEl, this.renderProtocolBreakdown());
if (talkersEl) dom.content(talkersEl, this.renderTopTalkers());
if (summaryEl) dom.content(summaryEl, this.renderSummaryStats());
},
formatBytes: function(bytes) {
if (!bytes || bytes === 0) return '0 B';
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = 0;
while (bytes >= 1024 && i < units.length - 1) {
bytes /= 1024;
i++;
}
return bytes.toFixed(1) + ' ' + units[i];
},
formatMB: function(mb) {
if (!mb || mb === 0) return '0 MB';
if (mb >= 1024) {
return (mb / 1024).toFixed(1) + ' GB';
}
return mb + ' MB';
}
});