diff --git a/package/secubox/luci-app-bandwidth-manager/htdocs/luci-static/resources/view/bandwidth-manager/analytics.js b/package/secubox/luci-app-bandwidth-manager/htdocs/luci-static/resources/view/bandwidth-manager/analytics.js new file mode 100644 index 00000000..c589c984 --- /dev/null +++ b/package/secubox/luci-app-bandwidth-manager/htdocs/luci-static/resources/view/bandwidth-manager/analytics.js @@ -0,0 +1,383 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require rpc'; +'require ui'; + +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({ + summary: {}, + hourlyData: [], + selectedPeriod: '24h', + + load: function() { + return Promise.all([ + callGetAnalyticsSummary('24h'), + callGetHourlyData(7) + ]); + }, + + render: function(data) { + var self = this; + this.summary = data[0] || {}; + this.hourlyData = (data[1] && data[1].hourly_data) || []; + + document.body.setAttribute('data-secubox-app', 'bandwidth'); + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', { 'class': 'cbi-map-title' }, 'Bandwidth Analytics'), + E('div', { 'class': 'cbi-map-descr' }, + 'Traffic analysis, usage trends, and application breakdown'), + + // Period Selector + E('div', { 'style': 'margin-bottom: 1.5rem;' }, [ + E('div', { 'style': 'display: flex; gap: 0.5rem; flex-wrap: wrap;' }, [ + this.renderPeriodButton('1h', '1 Hour'), + this.renderPeriodButton('6h', '6 Hours'), + this.renderPeriodButton('24h', '24 Hours', true), + this.renderPeriodButton('7d', '7 Days'), + this.renderPeriodButton('30d', '30 Days') + ]) + ]), + + // Stats Grid + E('div', { 'id': 'stats-container' }, [ + this.renderStatsGrid() + ]), + + // Charts Section + E('div', { 'id': 'charts-container', 'style': 'margin-top: 1.5rem;' }, [ + this.renderCharts() + ]), + + // Top Talkers & App Breakdown + E('div', { 'id': 'details-container', 'style': 'margin-top: 1.5rem;' }, [ + this.renderDetails() + ]) + ]); + + poll.add(L.bind(this.pollData, this), 30); + + return view; + }, + + pollData: function() { + var self = this; + return callGetAnalyticsSummary(this.selectedPeriod).then(function(data) { + self.summary = data || {}; + self.updateDisplay(); + }); + }, + + updateDisplay: function() { + var statsEl = document.getElementById('stats-container'); + var chartsEl = document.getElementById('charts-container'); + var detailsEl = document.getElementById('details-container'); + + if (statsEl) { + statsEl.innerHTML = ''; + statsEl.appendChild(this.renderStatsGrid()); + } + if (chartsEl) { + chartsEl.innerHTML = ''; + chartsEl.appendChild(this.renderCharts()); + } + if (detailsEl) { + detailsEl.innerHTML = ''; + detailsEl.appendChild(this.renderDetails()); + } + }, + + renderPeriodButton: function(period, label, isDefault) { + var self = this; + var isActive = this.selectedPeriod === period || (isDefault && !this.selectedPeriod); + + return E('button', { + 'class': 'cbi-button' + (isActive ? ' cbi-button-action' : ''), + 'style': 'padding: 0.5rem 1rem;', + 'click': function() { + self.selectedPeriod = period; + document.querySelectorAll('.cbi-button[data-period]').forEach(function(btn) { + btn.classList.remove('cbi-button-action'); + }); + this.classList.add('cbi-button-action'); + self.pollData(); + }, + 'data-period': period + }, label); + }, + + renderStatsGrid: function() { + var stats = [ + { + icon: '\u2b07\ufe0f', + label: 'Total Download', + value: this.formatBytes(this.summary.total_rx_bytes || 0), + color: '#22c55e' + }, + { + icon: '\u2b06\ufe0f', + label: 'Total Upload', + value: this.formatBytes(this.summary.total_tx_bytes || 0), + color: '#3b82f6' + }, + { + icon: '\ud83d\udcf1', + label: 'Active Clients', + value: (this.summary.active_clients || 0).toString(), + color: '#8b5cf6' + }, + { + icon: '\ud83d\udcc8', + label: 'Total Traffic', + value: this.formatBytes((this.summary.total_rx_bytes || 0) + (this.summary.total_tx_bytes || 0)), + color: '#f59e0b' + } + ]; + + return E('div', { + 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;' + }, stats.map(function(stat) { + return E('div', { + 'style': 'background: var(--cyber-bg-secondary, #141419); border: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.08)); border-radius: 12px; padding: 1.25rem;' + }, [ + E('div', { 'style': 'display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem;' }, [ + E('div', { + 'style': 'width: 40px; height: 40px; background: ' + stat.color + '20; color: ' + stat.color + '; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.25rem;' + }, stat.icon), + E('span', { 'style': 'font-size: 0.875rem; color: var(--cyber-text-secondary, #a1a1aa);' }, stat.label) + ]), + E('div', { 'style': 'font-size: 1.75rem; font-weight: 700;' }, stat.value) + ]); + })); + }, + + renderCharts: function() { + var self = this; + + // Create a simple SVG bar chart for traffic distribution + var appBreakdown = this.summary.app_breakdown || []; + var maxBytes = Math.max.apply(null, appBreakdown.map(function(a) { return a.bytes || 0; })) || 1; + + return E('div', { + 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1.5rem;' + }, [ + // Application Traffic Chart + E('div', { + 'style': 'background: var(--cyber-bg-secondary, #141419); border: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.08)); border-radius: 12px; padding: 1.25rem;' + }, [ + E('h4', { 'style': 'margin: 0 0 1rem 0; font-size: 1rem;' }, 'Traffic by Application'), + appBreakdown.length > 0 ? + E('div', { 'style': 'display: flex; flex-direction: column; gap: 0.75rem;' }, + appBreakdown.slice(0, 8).map(function(app, idx) { + var percent = Math.round((app.bytes / maxBytes) * 100); + var colors = ['#3b82f6', '#22c55e', '#f59e0b', '#ec4899', '#8b5cf6', '#06b6d4', '#ef4444', '#14b8a6']; + var color = colors[idx % colors.length]; + + return E('div', {}, [ + E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 0.25rem;' }, [ + E('span', { 'style': 'font-size: 0.875rem; color: var(--cyber-text-primary);' }, app.app || 'Unknown'), + E('span', { 'style': 'font-size: 0.875rem; color: var(--cyber-text-secondary);' }, self.formatBytes(app.bytes || 0)) + ]), + E('div', { + 'style': 'height: 8px; background: var(--cyber-bg-tertiary, rgba(255,255,255,0.05)); border-radius: 4px; overflow: hidden;' + }, [ + E('div', { + 'style': 'height: 100%; width: ' + percent + '%; background: ' + color + '; transition: width 0.3s ease;' + }) + ]) + ]); + }) + ) : + E('div', { + 'style': 'padding: 2rem; text-align: center; color: var(--cyber-text-secondary);' + }, 'No application data available') + ]), + + // Protocol Breakdown + E('div', { + 'style': 'background: var(--cyber-bg-secondary, #141419); border: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.08)); border-radius: 12px; padding: 1.25rem;' + }, [ + E('h4', { 'style': 'margin: 0 0 1rem 0; font-size: 1rem;' }, 'Traffic by Protocol'), + this.renderProtocolPieChart() + ]) + ]); + }, + + renderProtocolPieChart: function() { + var protocols = this.summary.protocol_breakdown || []; + if (protocols.length === 0) { + return E('div', { + 'style': 'padding: 2rem; text-align: center; color: var(--cyber-text-secondary);' + }, 'No protocol data available'); + } + + var total = protocols.reduce(function(sum, p) { return sum + (p.bytes || 0); }, 0) || 1; + var colors = ['#3b82f6', '#22c55e', '#f59e0b', '#ec4899', '#8b5cf6']; + + return E('div', { 'style': 'display: flex; align-items: center; gap: 2rem;' }, [ + // Simple donut representation + E('div', { + 'style': 'width: 120px; height: 120px; border-radius: 50%; background: conic-gradient(' + + protocols.slice(0, 5).map(function(p, idx) { + var startPercent = protocols.slice(0, idx).reduce(function(sum, pr) { + return sum + ((pr.bytes || 0) / total * 100); + }, 0); + var endPercent = startPercent + ((p.bytes || 0) / total * 100); + return colors[idx] + ' ' + startPercent + '% ' + endPercent + '%'; + }).join(', ') + '); position: relative;' + }, [ + E('div', { + 'style': 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 70px; height: 70px; background: var(--cyber-bg-secondary, #141419); border-radius: 50%;' + }) + ]), + // Legend + E('div', { 'style': 'display: flex; flex-direction: column; gap: 0.5rem;' }, + 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: 0.5rem;' }, [ + E('div', { + 'style': 'width: 12px; height: 12px; background: ' + colors[idx] + '; border-radius: 2px;' + }), + E('span', { 'style': 'font-size: 0.875rem;' }, (p.protocol || 'Unknown') + ' (' + percent + '%)') + ]); + }) + ) + ]); + }, + + renderDetails: function() { + var self = this; + var topTalkers = this.summary.top_talkers || []; + + return E('div', { + 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1.5rem;' + }, [ + // Top Talkers + E('div', { + 'style': 'background: var(--cyber-bg-secondary, #141419); border: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.08)); border-radius: 12px; overflow: hidden;' + }, [ + E('div', { + 'style': 'padding: 1rem 1.25rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.08));' + }, [ + E('h4', { 'style': 'margin: 0; font-size: 1rem;' }, 'Top Bandwidth Users') + ]), + topTalkers.length > 0 ? + E('table', { 'class': 'table', 'style': 'width: 100%;' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', { 'style': 'padding: 0.75rem 1.25rem;' }, 'Device'), + E('th', { 'style': 'padding: 0.75rem 1.25rem;' }, 'IP'), + E('th', { 'style': 'padding: 0.75rem 1.25rem; text-align: right;' }, 'Usage') + ]) + ]), + E('tbody', {}, + topTalkers.map(function(client, idx) { + var medals = ['\ud83e\udd47', '\ud83e\udd48', '\ud83e\udd49', '', '']; + return E('tr', {}, [ + E('td', { 'style': 'padding: 0.75rem 1.25rem;' }, [ + E('div', { 'style': 'display: flex; align-items: center; gap: 0.5rem;' }, [ + E('span', { 'style': 'font-size: 1.25rem;' }, medals[idx] || ''), + E('div', {}, [ + E('div', { 'style': 'font-weight: 500;' }, client.hostname || 'Unknown'), + E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-tertiary);' }, client.mac) + ]) + ]) + ]), + E('td', { 'style': 'padding: 0.75rem 1.25rem;' }, client.ip || '-'), + E('td', { 'style': 'padding: 0.75rem 1.25rem; text-align: right; font-weight: 600;' }, + self.formatMB(client.used_mb || 0)) + ]); + }) + ) + ]) : + E('div', { + 'style': 'padding: 2rem; text-align: center; color: var(--cyber-text-secondary);' + }, 'No usage data available') + ]), + + // Quick Actions + E('div', { + 'style': 'background: var(--cyber-bg-secondary, #141419); border: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.08)); border-radius: 12px; padding: 1.25rem;' + }, [ + E('h4', { 'style': 'margin: 0 0 1rem 0; font-size: 1rem;' }, 'Analytics Summary'), + E('div', { 'style': 'display: flex; flex-direction: column; gap: 1rem;' }, [ + E('div', { + 'style': 'padding: 1rem; background: var(--cyber-bg-tertiary, rgba(255,255,255,0.05)); border-radius: 8px;' + }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 0.5rem;' }, [ + E('span', { 'style': 'color: var(--cyber-text-secondary);' }, 'Download/Upload Ratio'), + E('span', { 'style': 'font-weight: 600;' }, + this.summary.total_tx_bytes > 0 ? + ((this.summary.total_rx_bytes || 0) / (this.summary.total_tx_bytes || 1)).toFixed(1) + ':1' : + 'N/A') + ]), + E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-tertiary);' }, + 'Typical ratio is 5:1 to 10:1 for home networks') + ]), + E('div', { + 'style': 'padding: 1rem; background: var(--cyber-bg-tertiary, rgba(255,255,255,0.05)); border-radius: 8px;' + }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 0.5rem;' }, [ + E('span', { 'style': 'color: var(--cyber-text-secondary);' }, 'Average per Client'), + E('span', { 'style': 'font-weight: 600;' }, + 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') + ]), + E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-tertiary);' }, + 'Based on ' + (this.summary.active_clients || 0) + ' active devices') + ]), + E('div', { + 'style': 'padding: 1rem; background: var(--cyber-bg-tertiary, rgba(255,255,255,0.05)); border-radius: 8px;' + }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 0.5rem;' }, [ + E('span', { 'style': 'color: var(--cyber-text-secondary);' }, 'Applications Detected'), + E('span', { 'style': 'font-weight: 600;' }, + (this.summary.app_breakdown || []).length.toString()) + ]), + E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-tertiary);' }, + 'Via Deep Packet Inspection') + ]) + ]) + ]) + ]); + }, + + 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'; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-bandwidth-manager/htdocs/luci-static/resources/view/bandwidth-manager/groups.js b/package/secubox/luci-app-bandwidth-manager/htdocs/luci-static/resources/view/bandwidth-manager/groups.js new file mode 100644 index 00000000..b2cc0b2d --- /dev/null +++ b/package/secubox/luci-app-bandwidth-manager/htdocs/luci-static/resources/view/bandwidth-manager/groups.js @@ -0,0 +1,592 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require rpc'; +'require ui'; + +var callListGroups = rpc.declare({ + object: 'luci.bandwidth-manager', + method: 'list_groups', + expect: { groups: [] } +}); + +var callGetGroup = rpc.declare({ + object: 'luci.bandwidth-manager', + method: 'get_group', + params: ['group_id'], + expect: { success: false } +}); + +var callCreateGroup = rpc.declare({ + object: 'luci.bandwidth-manager', + method: 'create_group', + params: ['name', 'description', 'quota_mb', 'priority', 'members'], + expect: { success: false, message: '' } +}); + +var callUpdateGroup = rpc.declare({ + object: 'luci.bandwidth-manager', + method: 'update_group', + params: ['group_id', 'name', 'description', 'quota_mb', 'priority', 'members'], + expect: { success: false, message: '' } +}); + +var callDeleteGroup = rpc.declare({ + object: 'luci.bandwidth-manager', + method: 'delete_group', + params: ['group_id'], + expect: { success: false, message: '' } +}); + +var callAddToGroup = rpc.declare({ + object: 'luci.bandwidth-manager', + method: 'add_to_group', + params: ['group_id', 'mac'], + expect: { success: false, message: '' } +}); + +var callRemoveFromGroup = rpc.declare({ + object: 'luci.bandwidth-manager', + method: 'remove_from_group', + params: ['group_id', 'mac'], + expect: { success: false, message: '' } +}); + +var callGetUsageRealtime = rpc.declare({ + object: 'luci.bandwidth-manager', + method: 'get_usage_realtime', + expect: { clients: [] } +}); + +var callGetClasses = rpc.declare({ + object: 'luci.bandwidth-manager', + method: 'get_classes', + expect: { classes: [] } +}); + +return view.extend({ + groups: [], + clients: [], + classes: [], + + load: function() { + return Promise.all([ + callListGroups(), + callGetUsageRealtime(), + callGetClasses() + ]); + }, + + render: function(data) { + var self = this; + this.groups = (data[0] && data[0].groups) || []; + this.clients = (data[1] && data[1].clients) || []; + this.classes = (data[2] && data[2].classes) || []; + + document.body.setAttribute('data-secubox-app', 'bandwidth'); + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', { 'class': 'cbi-map-title' }, 'Device Groups'), + E('div', { 'class': 'cbi-map-descr' }, + 'Organize devices into groups for shared quotas and unified QoS policies'), + + // Create Group Button + E('div', { 'style': 'margin-bottom: 1rem;' }, [ + E('button', { + 'class': 'cbi-button cbi-button-add', + 'click': function() { self.showCreateGroupDialog(); } + }, 'Create New Group') + ]), + + // Groups Grid + E('div', { 'id': 'groups-container' }, [ + this.renderGroupsGrid() + ]) + ]); + + poll.add(L.bind(this.pollData, this), 15); + + return view; + }, + + pollData: function() { + var self = this; + return callListGroups().then(function(data) { + self.groups = (data && data.groups) || []; + var container = document.getElementById('groups-container'); + if (container) { + container.innerHTML = ''; + container.appendChild(self.renderGroupsGrid()); + } + }); + }, + + renderGroupsGrid: function() { + var self = this; + + if (this.groups.length === 0) { + return E('div', { + 'style': 'padding: 3rem; text-align: center; color: var(--cyber-text-secondary, #a1a1aa); background: var(--cyber-bg-secondary, #141419); border-radius: 12px; border: 1px dashed var(--cyber-border-subtle, rgba(255,255,255,0.15));' + }, [ + E('div', { 'style': 'font-size: 3rem; margin-bottom: 1rem;' }, '\ud83d\udc65'), + E('div', { 'style': 'font-weight: 600; margin-bottom: 0.5rem;' }, 'No Groups Created'), + E('div', { 'style': 'font-size: 0.875rem;' }, 'Create device groups to apply shared quotas and priorities'), + E('br'), + E('button', { + 'class': 'cbi-button cbi-button-add', + 'click': function() { self.showCreateGroupDialog(); } + }, 'Create First Group') + ]); + } + + var presetIcons = { + 'Family': '\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc66', + 'IoT': '\ud83d\udce1', + 'Work': '\ud83d\udcbc', + 'Gaming': '\ud83c\udfae', + 'Kids': '\ud83d\udc76', + 'Guests': '\ud83d\udc64' + }; + + var presetColors = { + 'Family': '#8b5cf6', + 'IoT': '#06b6d4', + 'Work': '#3b82f6', + 'Gaming': '#ec4899', + 'Kids': '#22c55e', + 'Guests': '#f59e0b' + }; + + return E('div', { + 'style': 'display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1.25rem;' + }, this.groups.map(function(group) { + var icon = presetIcons[group.name] || '\ud83d\udc65'; + var color = presetColors[group.name] || '#667eea'; + + var usagePercent = 0; + if (group.quota_mb && group.quota_mb > 0) { + usagePercent = Math.min(100, Math.round((group.used_mb / group.quota_mb) * 100)); + } + + var progressColor = usagePercent > 90 ? '#ef4444' : usagePercent > 70 ? '#f59e0b' : '#22c55e'; + + return E('div', { + 'style': 'background: var(--cyber-bg-secondary, #141419); border: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.08)); border-radius: 12px; overflow: hidden; transition: all 0.2s ease;' + }, [ + // Header + E('div', { + 'style': 'padding: 1.25rem; border-bottom: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.08));' + }, [ + E('div', { 'style': 'display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem;' }, [ + E('div', { + 'style': 'width: 44px; height: 44px; background: ' + color + '20; color: ' + color + '; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.25rem;' + }, icon), + E('div', { 'style': 'flex: 1;' }, [ + E('div', { 'style': 'font-weight: 600; font-size: 1rem;' }, group.name), + E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-tertiary, #71717a);' }, + group.description || 'No description') + ]), + E('span', { + 'style': 'padding: 0.25rem 0.5rem; font-size: 0.6875rem; font-weight: 600; border-radius: 4px; background: ' + (group.enabled ? 'rgba(34, 197, 94, 0.15)' : 'rgba(239, 68, 68, 0.15)') + '; color: ' + (group.enabled ? '#22c55e' : '#ef4444') + ';' + }, group.enabled ? 'Active' : 'Disabled') + ]) + ]), + + // Stats + E('div', { 'style': 'padding: 1rem 1.25rem;' }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 1rem;' }, [ + E('div', {}, [ + E('div', { 'style': 'font-size: 1.5rem; font-weight: 700;' }, group.member_count.toString()), + E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-secondary);' }, 'Devices') + ]), + E('div', { 'style': 'text-align: center;' }, [ + E('div', { 'style': 'font-size: 1.5rem; font-weight: 700;' }, 'P' + (group.priority || 5)), + E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-secondary);' }, 'Priority') + ]), + E('div', { 'style': 'text-align: right;' }, [ + E('div', { 'style': 'font-size: 1.5rem; font-weight: 700;' }, + group.quota_mb > 0 ? self.formatMB(group.quota_mb) : '\u221e'), + E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-secondary);' }, 'Quota') + ]) + ]), + + // Usage Progress + group.quota_mb > 0 ? E('div', { 'style': 'margin-bottom: 0.5rem;' }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; font-size: 0.75rem; margin-bottom: 0.25rem;' }, [ + E('span', { 'style': 'color: var(--cyber-text-secondary);' }, 'Usage'), + E('span', {}, self.formatMB(group.used_mb) + ' / ' + self.formatMB(group.quota_mb) + ' (' + usagePercent + '%)') + ]), + E('div', { + 'style': 'height: 6px; background: var(--cyber-bg-tertiary, rgba(255,255,255,0.05)); border-radius: 3px; overflow: hidden;' + }, [ + E('div', { + 'style': 'height: 100%; width: ' + usagePercent + '%; background: ' + progressColor + '; transition: width 0.3s ease;' + }) + ]) + ]) : null + ]), + + // Actions + E('div', { + 'style': 'padding: 0.75rem 1.25rem; background: var(--cyber-bg-tertiary, rgba(255,255,255,0.03)); display: flex; gap: 0.5rem;' + }, [ + E('button', { + 'class': 'cbi-button', + 'style': 'flex: 1; font-size: 0.75rem;', + 'click': function() { self.showGroupDetails(group.id); } + }, 'Manage'), + E('button', { + 'class': 'cbi-button', + 'style': 'flex: 1; font-size: 0.75rem;', + 'click': function() { self.showEditGroupDialog(group); } + }, 'Edit'), + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'style': 'font-size: 0.75rem;', + 'click': function() { self.deleteGroup(group); } + }, '\u2717') + ]) + ]); + })); + }, + + showCreateGroupDialog: function() { + var self = this; + + var presets = [ + { name: 'Family', desc: 'Family members devices', icon: '\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc66' }, + { name: 'IoT', desc: 'Smart home devices', icon: '\ud83d\udce1' }, + { name: 'Work', desc: 'Work/business devices', icon: '\ud83d\udcbc' }, + { name: 'Gaming', desc: 'Gaming consoles and PCs', icon: '\ud83c\udfae' }, + { name: 'Kids', desc: 'Children\'s devices', icon: '\ud83d\udc76' }, + { name: 'Guests', desc: 'Guest network devices', icon: '\ud83d\udc64' } + ]; + + ui.showModal('Create Device Group', [ + E('div', { 'style': 'margin-bottom: 1rem;' }, [ + E('label', { 'style': 'display: block; margin-bottom: 0.5rem; font-weight: 500;' }, 'Quick Presets'), + E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 0.5rem;' }, + presets.map(function(preset) { + return E('button', { + 'class': 'cbi-button', + 'style': 'display: flex; align-items: center; gap: 0.5rem;', + 'click': function() { + document.getElementById('group-name').value = preset.name; + document.getElementById('group-desc').value = preset.desc; + } + }, [preset.icon, ' ', preset.name]); + }) + ) + ]), + E('hr', { 'style': 'margin: 1rem 0; border-color: var(--cyber-border-subtle);' }), + E('div', { 'style': 'margin-bottom: 1rem;' }, [ + E('label', { 'style': 'display: block; margin-bottom: 0.5rem;' }, 'Group Name *'), + E('input', { + 'type': 'text', + 'class': 'cbi-input-text', + 'id': 'group-name', + 'placeholder': 'Enter group name' + }) + ]), + E('div', { 'style': 'margin-bottom: 1rem;' }, [ + E('label', { 'style': 'display: block; margin-bottom: 0.5rem;' }, 'Description'), + E('input', { + 'type': 'text', + 'class': 'cbi-input-text', + 'id': 'group-desc', + 'placeholder': 'Optional description' + }) + ]), + E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem;' }, [ + E('div', {}, [ + E('label', { 'style': 'display: block; margin-bottom: 0.5rem;' }, 'Shared Quota (MB)'), + E('input', { + 'type': 'number', + 'class': 'cbi-input-text', + 'id': 'group-quota', + 'value': '0', + 'min': '0', + 'placeholder': '0 = unlimited' + }) + ]), + E('div', {}, [ + E('label', { 'style': 'display: block; margin-bottom: 0.5rem;' }, 'Priority Class'), + E('select', { 'class': 'cbi-input-select', 'id': 'group-priority' }, + this.classes.map(function(c) { + return E('option', { 'value': c.priority, 'selected': c.priority === 5 }, c.priority + ' - ' + c.name); + }) + ) + ]) + ]), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'cbi-button', + 'click': ui.hideModal + }, 'Cancel'), + ' ', + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': function() { + var name = document.getElementById('group-name').value.trim(); + var desc = document.getElementById('group-desc').value.trim(); + var quota = parseInt(document.getElementById('group-quota').value) || 0; + var priority = parseInt(document.getElementById('group-priority').value) || 5; + + if (!name) { + ui.addNotification(null, E('p', {}, 'Group name is required'), 'error'); + return; + } + + ui.hideModal(); + callCreateGroup(name, desc, quota, priority, '').then(function(res) { + if (res.success) { + ui.addNotification(null, E('p', {}, 'Group created successfully'), 'success'); + self.pollData(); + } else { + ui.addNotification(null, E('p', {}, res.message || 'Failed to create group'), 'error'); + } + }); + } + }, 'Create Group') + ]) + ]); + }, + + showEditGroupDialog: function(group) { + var self = this; + + ui.showModal('Edit Group: ' + group.name, [ + E('div', { 'style': 'margin-bottom: 1rem;' }, [ + E('label', { 'style': 'display: block; margin-bottom: 0.5rem;' }, 'Group Name *'), + E('input', { + 'type': 'text', + 'class': 'cbi-input-text', + 'id': 'edit-group-name', + 'value': group.name + }) + ]), + E('div', { 'style': 'margin-bottom: 1rem;' }, [ + E('label', { 'style': 'display: block; margin-bottom: 0.5rem;' }, 'Description'), + E('input', { + 'type': 'text', + 'class': 'cbi-input-text', + 'id': 'edit-group-desc', + 'value': group.description || '' + }) + ]), + E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem;' }, [ + E('div', {}, [ + E('label', { 'style': 'display: block; margin-bottom: 0.5rem;' }, 'Shared Quota (MB)'), + E('input', { + 'type': 'number', + 'class': 'cbi-input-text', + 'id': 'edit-group-quota', + 'value': group.quota_mb || '0', + 'min': '0' + }) + ]), + E('div', {}, [ + E('label', { 'style': 'display: block; margin-bottom: 0.5rem;' }, 'Priority Class'), + E('select', { 'class': 'cbi-input-select', 'id': 'edit-group-priority' }, + this.classes.map(function(c) { + return E('option', { + 'value': c.priority, + 'selected': c.priority === (group.priority || 5) + }, c.priority + ' - ' + c.name); + }) + ) + ]) + ]), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'cbi-button', + 'click': ui.hideModal + }, 'Cancel'), + ' ', + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': function() { + var name = document.getElementById('edit-group-name').value.trim(); + var desc = document.getElementById('edit-group-desc').value.trim(); + var quota = parseInt(document.getElementById('edit-group-quota').value) || 0; + var priority = parseInt(document.getElementById('edit-group-priority').value) || 5; + + if (!name) { + ui.addNotification(null, E('p', {}, 'Group name is required'), 'error'); + return; + } + + ui.hideModal(); + callUpdateGroup(group.id, name, desc, quota, priority, '').then(function(res) { + if (res.success) { + ui.addNotification(null, E('p', {}, 'Group updated successfully'), 'success'); + self.pollData(); + } else { + ui.addNotification(null, E('p', {}, res.message || 'Failed to update group'), 'error'); + } + }); + } + }, 'Save Changes') + ]) + ]); + }, + + showGroupDetails: function(groupId) { + var self = this; + + callGetGroup(groupId).then(function(group) { + if (!group.success) { + ui.addNotification(null, E('p', {}, 'Failed to load group details'), 'error'); + return; + } + + var members = group.members || []; + var availableClients = self.clients.filter(function(c) { + return !members.some(function(m) { return m.mac.toLowerCase() === c.mac.toLowerCase(); }); + }); + + ui.showModal('Manage Group: ' + group.name, [ + E('h4', { 'style': 'margin-bottom: 1rem;' }, 'Group Members (' + members.length + ')'), + + members.length > 0 ? + E('div', { 'style': 'max-height: 200px; overflow-y: auto; margin-bottom: 1rem;' }, [ + E('table', { 'class': 'table', 'style': 'width: 100%;' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, 'Device'), + E('th', {}, 'IP'), + E('th', {}, 'Usage'), + E('th', {}, 'Action') + ]) + ]), + E('tbody', {}, + members.map(function(member) { + return E('tr', {}, [ + E('td', {}, [ + E('div', { 'style': 'font-weight: 500;' }, member.hostname || 'Unknown'), + E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-tertiary);' }, member.mac) + ]), + E('td', {}, member.ip || '-'), + E('td', {}, self.formatMB(member.used_mb || 0)), + E('td', {}, [ + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'style': 'font-size: 0.75rem; padding: 0.25rem 0.5rem;', + 'click': function() { + callRemoveFromGroup(groupId, member.mac).then(function(res) { + if (res.success) { + ui.hideModal(); + self.showGroupDetails(groupId); + self.pollData(); + } + }); + } + }, 'Remove') + ]) + ]); + }) + ) + ]) + ]) : + E('div', { + 'style': 'padding: 1rem; text-align: center; color: var(--cyber-text-secondary); background: var(--cyber-bg-tertiary); border-radius: 8px; margin-bottom: 1rem;' + }, 'No devices in this group'), + + E('h4', { 'style': 'margin-bottom: 1rem;' }, 'Add Devices'), + + availableClients.length > 0 ? + E('div', { 'style': 'max-height: 200px; overflow-y: auto; margin-bottom: 1rem;' }, [ + E('table', { 'class': 'table', 'style': 'width: 100%;' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, 'Device'), + E('th', {}, 'IP'), + E('th', {}, 'Action') + ]) + ]), + E('tbody', {}, + availableClients.map(function(client) { + return E('tr', {}, [ + E('td', {}, [ + E('div', { 'style': 'font-weight: 500;' }, client.hostname || 'Unknown'), + E('div', { 'style': 'font-size: 0.75rem; color: var(--cyber-text-tertiary);' }, client.mac) + ]), + E('td', {}, client.ip || '-'), + E('td', {}, [ + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'style': 'font-size: 0.75rem; padding: 0.25rem 0.5rem;', + 'click': function() { + callAddToGroup(groupId, client.mac).then(function(res) { + if (res.success) { + ui.hideModal(); + self.showGroupDetails(groupId); + self.pollData(); + } + }); + } + }, 'Add') + ]) + ]); + }) + ) + ]) + ]) : + E('div', { + 'style': 'padding: 1rem; text-align: center; color: var(--cyber-text-secondary); background: var(--cyber-bg-tertiary); border-radius: 8px; margin-bottom: 1rem;' + }, 'No available devices to add'), + + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'cbi-button', + 'click': ui.hideModal + }, 'Close') + ]) + ]); + }); + }, + + deleteGroup: function(group) { + var self = this; + + ui.showModal('Delete Group', [ + E('p', {}, 'Are you sure you want to delete the group "' + group.name + '"?'), + E('p', { 'style': 'color: var(--cyber-text-secondary);' }, + 'This will not affect the devices in the group.'), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'cbi-button', + 'click': ui.hideModal + }, 'Cancel'), + ' ', + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'click': function() { + ui.hideModal(); + callDeleteGroup(group.id).then(function(res) { + if (res.success) { + ui.addNotification(null, E('p', {}, 'Group deleted'), 'success'); + self.pollData(); + } else { + ui.addNotification(null, E('p', {}, res.message || 'Failed to delete group'), 'error'); + } + }); + } + }, 'Delete') + ]) + ]); + }, + + formatMB: function(mb) { + if (!mb || mb === 0) return '0 MB'; + if (mb >= 1024) { + return (mb / 1024).toFixed(1) + ' GB'; + } + return mb + ' MB'; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-bandwidth-manager/htdocs/luci-static/resources/view/bandwidth-manager/smart-qos.js b/package/secubox/luci-app-bandwidth-manager/htdocs/luci-static/resources/view/bandwidth-manager/smart-qos.js new file mode 100644 index 00000000..707e36a3 --- /dev/null +++ b/package/secubox/luci-app-bandwidth-manager/htdocs/luci-static/resources/view/bandwidth-manager/smart-qos.js @@ -0,0 +1,399 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require rpc'; +'require ui'; + +var callGetDpiApplications = rpc.declare({ + object: 'luci.bandwidth-manager', + method: 'get_dpi_applications', + expect: { applications: [], dpi_source: 'none' } +}); + +var callGetSmartSuggestions = rpc.declare({ + object: 'luci.bandwidth-manager', + method: 'get_smart_suggestions', + expect: { suggestions: [] } +}); + +var callApplyDpiRule = rpc.declare({ + object: 'luci.bandwidth-manager', + method: 'apply_dpi_rule', + params: ['app_name', 'priority', 'limit_down', 'limit_up'], + expect: { success: false, message: '' } +}); + +var callGetClasses = rpc.declare({ + object: 'luci.bandwidth-manager', + method: 'get_classes', + expect: { classes: [] } +}); + +return view.extend({ + applications: [], + suggestions: [], + classes: [], + dpiSource: 'none', + + load: function() { + return Promise.all([ + callGetDpiApplications(), + callGetSmartSuggestions(), + callGetClasses() + ]); + }, + + render: function(data) { + var self = this; + var dpiData = data[0] || { applications: [], dpi_source: 'none' }; + var suggestionsData = data[1] || { suggestions: [] }; + var classesData = data[2] || { classes: [] }; + + this.applications = dpiData.applications || []; + this.dpiSource = dpiData.dpi_source || 'none'; + this.suggestions = suggestionsData.suggestions || []; + this.classes = classesData.classes || []; + + document.body.setAttribute('data-secubox-app', 'bandwidth'); + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', { 'class': 'cbi-map-title' }, 'Smart QoS'), + E('div', { 'class': 'cbi-map-descr' }, + 'AI-powered traffic classification using Deep Packet Inspection'), + + // DPI Status + this.renderDpiStatus(), + + // Smart Suggestions + this.renderSuggestions(), + + // Detected Applications + this.renderApplications() + ]); + + poll.add(L.bind(this.pollData, this), 10); + + return view; + }, + + pollData: function() { + var self = this; + return Promise.all([ + callGetDpiApplications(), + callGetSmartSuggestions() + ]).then(function(data) { + self.applications = (data[0] && data[0].applications) || []; + self.dpiSource = (data[0] && data[0].dpi_source) || 'none'; + self.suggestions = (data[1] && data[1].suggestions) || []; + + var statusEl = document.getElementById('dpi-status-container'); + var suggestionsEl = document.getElementById('suggestions-container'); + var appsEl = document.getElementById('apps-container'); + + if (statusEl) { + statusEl.innerHTML = ''; + statusEl.appendChild(self.renderDpiStatusContent()); + } + if (suggestionsEl) { + suggestionsEl.innerHTML = ''; + suggestionsEl.appendChild(self.renderSuggestionsContent()); + } + if (appsEl) { + appsEl.innerHTML = ''; + appsEl.appendChild(self.renderApplicationsContent()); + } + }); + }, + + renderDpiStatus: function() { + return E('div', { 'class': 'cbi-section', 'id': 'dpi-status-container' }, [ + this.renderDpiStatusContent() + ]); + }, + + renderDpiStatusContent: function() { + var statusColor, statusText, statusIcon; + switch (this.dpiSource) { + case 'ndpid': + statusColor = '#22c55e'; + statusText = 'nDPId Active'; + statusIcon = '\u2713'; + break; + case 'netifyd': + statusColor = '#3b82f6'; + statusText = 'Netifyd Active'; + statusIcon = '\u2713'; + break; + default: + statusColor = '#ef4444'; + statusText = 'No DPI Engine'; + statusIcon = '\u2717'; + } + + return E('div', { + 'style': 'display: flex; align-items: center; gap: 1rem; padding: 1rem; background: var(--cyber-bg-secondary, #141419); border-radius: 8px; border-left: 4px solid ' + statusColor + }, [ + E('div', { + 'style': 'width: 48px; height: 48px; background: ' + statusColor + '20; color: ' + statusColor + '; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.5rem;' + }, statusIcon), + E('div', {}, [ + E('div', { 'style': 'font-weight: 600; color: ' + statusColor }, statusText), + E('div', { 'style': 'font-size: 0.875rem; color: var(--cyber-text-secondary, #a1a1aa);' }, + this.dpiSource !== 'none' + ? 'Deep Packet Inspection is analyzing your network traffic' + : 'Install nDPId or netifyd to enable application detection') + ]), + E('div', { 'style': 'margin-left: auto; font-size: 0.875rem;' }, [ + E('span', { 'style': 'color: var(--cyber-text-secondary);' }, 'Detected Apps: '), + E('strong', { 'style': 'color: var(--cyber-text-primary);' }, this.applications.length.toString()) + ]) + ]); + }, + + renderSuggestions: function() { + return E('div', { 'class': 'cbi-section' }, [ + E('h3', { 'class': 'cbi-section-title' }, 'Smart Suggestions'), + E('div', { 'id': 'suggestions-container' }, [ + this.renderSuggestionsContent() + ]) + ]); + }, + + renderSuggestionsContent: function() { + var self = this; + + if (this.suggestions.length === 0) { + return E('div', { + 'style': 'padding: 2rem; text-align: center; color: var(--cyber-text-secondary, #a1a1aa); background: var(--cyber-bg-secondary, #141419); border-radius: 8px;' + }, [ + E('div', { 'style': 'font-size: 2rem; margin-bottom: 0.5rem;' }, '\ud83d\udd0d'), + 'Analyzing traffic patterns...', + E('br'), + this.dpiSource === 'none' + ? 'Enable a DPI engine to get smart suggestions' + : 'No optimization suggestions at this time' + ]); + } + + var typeIcons = { + gaming: '\ud83c\udfae', + streaming: '\ud83c\udfa5', + videoconf: '\ud83d\udcf9', + downloads: '\u2b07\ufe0f' + }; + + var typeColors = { + gaming: '#8b5cf6', + streaming: '#ec4899', + videoconf: '#3b82f6', + downloads: '#f59e0b' + }; + + return E('div', { 'style': 'display: grid; gap: 1rem;' }, + this.suggestions.map(function(suggestion) { + var icon = typeIcons[suggestion.type] || '\ud83d\udca1'; + var color = typeColors[suggestion.type] || '#667eea'; + + return E('div', { + 'style': 'display: flex; align-items: flex-start; gap: 1rem; padding: 1rem; background: var(--cyber-bg-secondary, #141419); border-radius: 8px; border: 1px solid var(--cyber-border-subtle, rgba(255,255,255,0.08));' + }, [ + E('div', { + 'style': 'width: 44px; height: 44px; background: ' + color + '20; color: ' + color + '; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; flex-shrink: 0;' + }, icon), + E('div', { 'style': 'flex: 1;' }, [ + E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25rem;' }, suggestion.title), + E('div', { 'style': 'font-size: 0.875rem; color: var(--cyber-text-secondary, #a1a1aa); margin-bottom: 0.5rem;' }, suggestion.description), + E('div', { 'style': 'display: flex; gap: 0.5rem; font-size: 0.75rem;' }, [ + E('span', { + 'style': 'padding: 0.25rem 0.5rem; background: var(--cyber-bg-tertiary, rgba(255,255,255,0.05)); border-radius: 4px;' + }, 'Priority: ' + suggestion.priority), + E('span', { + 'style': 'padding: 0.25rem 0.5rem; background: var(--cyber-bg-tertiary, rgba(255,255,255,0.05)); border-radius: 4px;' + }, suggestion.affected_devices + ' device(s)') + ]) + ]), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'flex-shrink: 0;', + 'click': function() { self.applySuggestion(suggestion); } + }, 'Apply') + ]); + }) + ); + }, + + renderApplications: function() { + return E('div', { 'class': 'cbi-section' }, [ + E('h3', { 'class': 'cbi-section-title' }, 'Detected Applications'), + E('div', { 'id': 'apps-container' }, [ + this.renderApplicationsContent() + ]) + ]); + }, + + renderApplicationsContent: function() { + var self = this; + + if (this.applications.length === 0) { + return E('div', { + 'style': 'padding: 2rem; text-align: center; color: var(--cyber-text-secondary, #a1a1aa); background: var(--cyber-bg-secondary, #141419); border-radius: 8px;' + }, 'No applications detected'); + } + + // Sort by bytes + var sortedApps = this.applications.slice().sort(function(a, b) { + return (b.total_bytes || 0) - (a.total_bytes || 0); + }); + + return E('div', { 'style': 'overflow-x: auto;' }, [ + E('table', { 'class': 'table cbi-section-table', 'style': 'width: 100%;' }, [ + E('thead', {}, [ + E('tr', { 'class': 'tr cbi-section-table-titles' }, [ + E('th', { 'class': 'th' }, 'Application'), + E('th', { 'class': 'th' }, 'Flows'), + E('th', { 'class': 'th' }, 'Traffic'), + E('th', { 'class': 'th' }, 'Actions') + ]) + ]), + E('tbody', {}, + sortedApps.slice(0, 20).map(function(app) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'style': 'font-weight: 500;' }, app.name || 'Unknown'), + E('td', { 'class': 'td' }, (app.flow_count || 0).toString()), + E('td', { 'class': 'td' }, self.formatBytes(app.total_bytes || 0)), + E('td', { 'class': 'td' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'font-size: 0.75rem; padding: 0.25rem 0.5rem;', + 'click': function() { self.showRuleDialog(app); } + }, 'Create Rule') + ]) + ]); + }) + ) + ]) + ]); + }, + + applySuggestion: function(suggestion) { + var self = this; + + ui.showModal('Apply Suggestion', [ + E('p', {}, suggestion.description), + E('p', {}, 'This will create a QoS rule with priority ' + suggestion.priority + '.'), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'cbi-button', + 'click': ui.hideModal + }, 'Cancel'), + ' ', + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': function() { + ui.hideModal(); + // Apply rule based on suggestion type + var appName = ''; + switch (suggestion.type) { + case 'gaming': + appName = 'Gaming'; + break; + case 'streaming': + appName = 'Streaming'; + break; + case 'videoconf': + appName = 'Video Conferencing'; + break; + case 'downloads': + appName = 'Downloads'; + break; + } + callApplyDpiRule(appName, suggestion.priority, 0, 0).then(function(res) { + if (res.success) { + ui.addNotification(null, E('p', {}, res.message), 'success'); + } else { + ui.addNotification(null, E('p', {}, res.message || 'Failed to apply rule'), 'error'); + } + }); + } + }, 'Apply') + ]) + ]); + }, + + showRuleDialog: function(app) { + var self = this; + + var prioritySelect = E('select', { 'class': 'cbi-input-select', 'id': 'rule-priority' }, + this.classes.map(function(c) { + return E('option', { 'value': c.priority }, c.priority + ' - ' + c.name); + }) + ); + + ui.showModal('Create QoS Rule for ' + app.name, [ + E('div', { 'style': 'margin-bottom: 1rem;' }, [ + E('label', { 'style': 'display: block; margin-bottom: 0.5rem;' }, 'Priority Class'), + prioritySelect + ]), + E('div', { 'style': 'margin-bottom: 1rem;' }, [ + E('label', { 'style': 'display: block; margin-bottom: 0.5rem;' }, 'Download Limit (Kbps, 0 = unlimited)'), + E('input', { + 'type': 'number', + 'class': 'cbi-input-text', + 'id': 'rule-limit-down', + 'value': '0', + 'min': '0' + }) + ]), + E('div', { 'style': 'margin-bottom: 1rem;' }, [ + E('label', { 'style': 'display: block; margin-bottom: 0.5rem;' }, 'Upload Limit (Kbps, 0 = unlimited)'), + E('input', { + 'type': 'number', + 'class': 'cbi-input-text', + 'id': 'rule-limit-up', + 'value': '0', + 'min': '0' + }) + ]), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'cbi-button', + 'click': ui.hideModal + }, 'Cancel'), + ' ', + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': function() { + var priority = parseInt(document.getElementById('rule-priority').value) || 5; + var limitDown = parseInt(document.getElementById('rule-limit-down').value) || 0; + var limitUp = parseInt(document.getElementById('rule-limit-up').value) || 0; + + ui.hideModal(); + callApplyDpiRule(app.name, priority, limitDown, limitUp).then(function(res) { + if (res.success) { + ui.addNotification(null, E('p', {}, 'Rule created for ' + app.name), 'success'); + } else { + ui.addNotification(null, E('p', {}, res.message || 'Failed to create rule'), 'error'); + } + }); + } + }, 'Create Rule') + ]) + ]); + }, + + 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]; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-bandwidth-manager/root/usr/libexec/rpcd/luci.bandwidth-manager b/package/secubox/luci-app-bandwidth-manager/root/usr/libexec/rpcd/luci.bandwidth-manager index eadd5260..cfe022a1 100755 --- a/package/secubox/luci-app-bandwidth-manager/root/usr/libexec/rpcd/luci.bandwidth-manager +++ b/package/secubox/luci-app-bandwidth-manager/root/usr/libexec/rpcd/luci.bandwidth-manager @@ -579,6 +579,615 @@ get_media() { json_dump } +# ============================================ +# Smart QoS - DPI Integration with nDPId +# ============================================ + +NDPID_FLOWS="/tmp/ndpid-flows.json" +DPI_RULES_FILE="/tmp/bandwidth_dpi_rules.json" + +# Get DPI detected applications from nDPId +get_dpi_applications() { + json_init + json_add_array "applications" + + if [ -f "$NDPID_FLOWS" ] && pgrep -x ndpid >/dev/null 2>&1; then + # Extract unique applications from nDPId flows + jq -r '.flows[]? | .app // "Unknown"' "$NDPID_FLOWS" 2>/dev/null | sort -u | while read app; do + [ -z "$app" ] && continue + local count=$(jq -r ".flows[] | select(.app == \"$app\") | .src_ip" "$NDPID_FLOWS" 2>/dev/null | wc -l) + local bytes=$(jq -r ".flows[] | select(.app == \"$app\") | .bytes_rx + .bytes_tx" "$NDPID_FLOWS" 2>/dev/null | awk '{sum+=$1} END {print sum}') + + json_add_object "" + json_add_string "name" "$app" + json_add_int "flow_count" "$count" + json_add_int "total_bytes" "${bytes:-0}" + json_close_object + done + fi + + json_close_array + + # Check DPI source status + local dpi_source="none" + if [ -f "$NDPID_FLOWS" ] && pgrep -x ndpid >/dev/null 2>&1; then + dpi_source="ndpid" + elif [ -f "/var/run/netifyd/status.json" ] && pgrep -x netifyd >/dev/null 2>&1; then + dpi_source="netifyd" + fi + json_add_string "dpi_source" "$dpi_source" + + json_dump +} + +# Get smart QoS suggestions based on detected traffic +get_smart_suggestions() { + json_init + json_add_array "suggestions" + + if [ -f "$NDPID_FLOWS" ]; then + # Check for gaming traffic + local gaming_count=$(jq -r '.flows[]? | select(.category == "Game" or .app | test("Steam|Xbox|PlayStation|Nintendo|Epic|Riot"; "i")) | .src_ip' "$NDPID_FLOWS" 2>/dev/null | sort -u | wc -l) + if [ "$gaming_count" -gt 0 ]; then + json_add_object "" + json_add_string "type" "gaming" + json_add_string "title" "Gaming Traffic Detected" + json_add_string "description" "Detected $gaming_count devices with gaming traffic. Recommend priority class 1-2 for low latency." + json_add_int "priority" 2 + json_add_int "affected_devices" "$gaming_count" + json_close_object + fi + + # Check for streaming traffic + local streaming_count=$(jq -r '.flows[]? | select(.app | test("YouTube|Netflix|Twitch|Disney|HBO|Amazon.*Video|Spotify"; "i")) | .src_ip' "$NDPID_FLOWS" 2>/dev/null | sort -u | wc -l) + if [ "$streaming_count" -gt 0 ]; then + json_add_object "" + json_add_string "type" "streaming" + json_add_string "title" "Streaming Traffic Detected" + json_add_string "description" "Detected $streaming_count devices streaming video/audio. Recommend priority class 4 for consistent quality." + json_add_int "priority" 4 + json_add_int "affected_devices" "$streaming_count" + json_close_object + fi + + # Check for video conferencing + local video_conf_count=$(jq -r '.flows[]? | select(.app | test("Zoom|Teams|Meet|WebEx|Skype"; "i")) | .src_ip' "$NDPID_FLOWS" 2>/dev/null | sort -u | wc -l) + if [ "$video_conf_count" -gt 0 ]; then + json_add_object "" + json_add_string "type" "videoconf" + json_add_string "title" "Video Conferencing Detected" + json_add_string "description" "Detected $video_conf_count devices in video calls. Recommend priority class 1 for real-time communication." + json_add_int "priority" 1 + json_add_int "affected_devices" "$video_conf_count" + json_close_object + fi + + # Check for heavy downloaders + local download_bytes=$(jq -r '.flows[]? | select(.bytes_rx > 100000000) | "\(.src_ip) \(.bytes_rx)"' "$NDPID_FLOWS" 2>/dev/null | sort -t' ' -k2 -nr | head -3) + if [ -n "$download_bytes" ]; then + local heavy_users=$(echo "$download_bytes" | wc -l) + json_add_object "" + json_add_string "type" "downloads" + json_add_string "title" "Heavy Bandwidth Users" + json_add_string "description" "Detected $heavy_users devices downloading heavily. Consider priority class 6-7 or quotas." + json_add_int "priority" 7 + json_add_int "affected_devices" "$heavy_users" + json_close_object + fi + fi + + json_close_array + json_dump +} + +# Apply smart QoS rule based on app detection +apply_dpi_rule() { + read -r input + json_load "$input" + + local app_name priority limit_down limit_up + json_get_var app_name app_name + json_get_var priority priority "5" + json_get_var limit_down limit_down "0" + json_get_var limit_up limit_up "0" + json_cleanup + + if [ -z "$app_name" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "message" "Application name is required" + json_dump + return 1 + fi + + # Create DPI-based QoS rule + local rule_id="dpi_rule_$(echo "$app_name" | tr ' ' '_' | tr '[:upper:]' '[:lower:]')_$(date +%s)" + + uci -q batch << EOF +set bandwidth.$rule_id=rule +set bandwidth.$rule_id.name='DPI: $app_name' +set bandwidth.$rule_id.type='dpi_app' +set bandwidth.$rule_id.target='$app_name' +set bandwidth.$rule_id.limit_down='$limit_down' +set bandwidth.$rule_id.limit_up='$limit_up' +set bandwidth.$rule_id.priority='$priority' +set bandwidth.$rule_id.enabled='1' +set bandwidth.$rule_id.dpi_match='1' +commit bandwidth +EOF + + json_init + json_add_boolean "success" 1 + json_add_string "rule_id" "$rule_id" + json_add_string "message" "DPI rule created for $app_name" + json_dump +} + +# ============================================ +# Device Groups Management +# ============================================ + +# List all device groups +list_groups() { + config_load bandwidth + json_init + json_add_array "groups" + + _add_group() { + local name description quota_mb priority members enabled + config_get name "$1" name "" + config_get description "$1" description "" + config_get quota_mb "$1" quota_mb "0" + config_get priority "$1" priority "5" + config_get members "$1" members "" + config_get enabled "$1" enabled "1" + + # Calculate group usage + local total_used=0 + local member_count=0 + for mac in $members; do + member_count=$(( member_count + 1 )) + local used=$(get_mac_usage "$mac") + total_used=$(( total_used + used )) + done + + json_add_object "" + json_add_string "id" "$1" + json_add_string "name" "$name" + json_add_string "description" "$description" + json_add_int "quota_mb" "$quota_mb" + json_add_int "priority" "$priority" + json_add_int "member_count" "$member_count" + json_add_int "used_mb" "$total_used" + json_add_boolean "enabled" "$enabled" + json_close_object + } + + config_foreach _add_group group + + json_close_array + json_dump +} + +# Get group details with members +get_group() { + read -r input + json_load "$input" + + local group_id + json_get_var group_id group_id + json_cleanup + + if [ -z "$group_id" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "message" "Group ID is required" + json_dump + return 1 + fi + + config_load bandwidth + + local name description quota_mb priority members enabled + config_get name "$group_id" name "" + config_get description "$group_id" description "" + config_get quota_mb "$group_id" quota_mb "0" + config_get priority "$group_id" priority "5" + config_get members "$group_id" members "" + config_get enabled "$group_id" enabled "1" + + if [ -z "$name" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "message" "Group not found" + json_dump + return 1 + fi + + json_init + json_add_boolean "success" 1 + json_add_string "id" "$group_id" + json_add_string "name" "$name" + json_add_string "description" "$description" + json_add_int "quota_mb" "$quota_mb" + json_add_int "priority" "$priority" + json_add_boolean "enabled" "$enabled" + + # Add members with details + json_add_array "members" + for mac in $members; do + local hostname ip used_mb + hostname=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $4}') + ip=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $3}') + used_mb=$(get_mac_usage "$mac") + + json_add_object "" + json_add_string "mac" "$mac" + json_add_string "hostname" "${hostname:-unknown}" + json_add_string "ip" "${ip:-unknown}" + json_add_int "used_mb" "$used_mb" + json_close_object + done + json_close_array + + json_dump +} + +# Create device group +create_group() { + read -r input + json_load "$input" + + local name description quota_mb priority members + json_get_var name name + json_get_var description description "" + json_get_var quota_mb quota_mb "0" + json_get_var priority priority "5" + json_get_var members members "" + json_cleanup + + if [ -z "$name" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "message" "Group name is required" + json_dump + return 1 + fi + + local group_id="group_$(date +%s)" + + uci -q batch << EOF +set bandwidth.$group_id=group +set bandwidth.$group_id.name='$name' +set bandwidth.$group_id.description='$description' +set bandwidth.$group_id.quota_mb='$quota_mb' +set bandwidth.$group_id.priority='$priority' +set bandwidth.$group_id.members='$members' +set bandwidth.$group_id.enabled='1' +commit bandwidth +EOF + + json_init + json_add_boolean "success" 1 + json_add_string "group_id" "$group_id" + json_add_string "message" "Group created successfully" + json_dump +} + +# Update device group +update_group() { + read -r input + json_load "$input" + + local group_id name description quota_mb priority members + json_get_var group_id group_id + json_get_var name name + json_get_var description description "" + json_get_var quota_mb quota_mb "0" + json_get_var priority priority "5" + json_get_var members members "" + json_cleanup + + if [ -z "$group_id" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "message" "Group ID is required" + json_dump + return 1 + fi + + # Check if group exists + config_load bandwidth + local existing_name + config_get existing_name "$group_id" name "" + if [ -z "$existing_name" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "message" "Group not found" + json_dump + return 1 + fi + + uci -q batch << EOF +set bandwidth.$group_id.name='$name' +set bandwidth.$group_id.description='$description' +set bandwidth.$group_id.quota_mb='$quota_mb' +set bandwidth.$group_id.priority='$priority' +set bandwidth.$group_id.members='$members' +commit bandwidth +EOF + + json_init + json_add_boolean "success" 1 + json_add_string "message" "Group updated successfully" + json_dump +} + +# Delete device group +delete_group() { + read -r input + json_load "$input" + + local group_id + json_get_var group_id group_id + json_cleanup + + if [ -z "$group_id" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "message" "Group ID is required" + json_dump + return 1 + fi + + # Check if group exists + if ! uci -q get bandwidth.$group_id >/dev/null 2>&1; then + json_init + json_add_boolean "success" 0 + json_add_string "message" "Group not found" + json_dump + return 1 + fi + + uci -q delete bandwidth.$group_id + uci -q commit bandwidth + + json_init + json_add_boolean "success" 1 + json_add_string "message" "Group deleted successfully" + json_dump +} + +# Add device to group +add_to_group() { + read -r input + json_load "$input" + + local group_id mac + json_get_var group_id group_id + json_get_var mac mac + json_cleanup + + if [ -z "$group_id" ] || [ -z "$mac" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "message" "Group ID and MAC address are required" + json_dump + return 1 + fi + + config_load bandwidth + local members + config_get members "$group_id" members "" + + # Check if already in group + if echo "$members" | grep -qi "$mac"; then + json_init + json_add_boolean "success" 0 + json_add_string "message" "Device already in group" + json_dump + return 1 + fi + + members="$members $mac" + uci -q set bandwidth.$group_id.members="$members" + uci -q commit bandwidth + + json_init + json_add_boolean "success" 1 + json_add_string "message" "Device added to group" + json_dump +} + +# Remove device from group +remove_from_group() { + read -r input + json_load "$input" + + local group_id mac + json_get_var group_id group_id + json_get_var mac mac + json_cleanup + + if [ -z "$group_id" ] || [ -z "$mac" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "message" "Group ID and MAC address are required" + json_dump + return 1 + fi + + config_load bandwidth + local members + config_get members "$group_id" members "" + + # Remove MAC from members list + members=$(echo "$members" | sed "s/$mac//gi" | tr -s ' ') + uci -q set bandwidth.$group_id.members="$members" + uci -q commit bandwidth + + json_init + json_add_boolean "success" 1 + json_add_string "message" "Device removed from group" + json_dump +} + +# ============================================ +# Analytics +# ============================================ + +ANALYTICS_DB="/tmp/bandwidth_analytics.db" + +# Initialize analytics database +init_analytics_db() { + if [ ! -f "$ANALYTICS_DB" ]; then + cat > "$ANALYTICS_DB" << 'EOF' +# Hour|Date|Total_RX|Total_TX|Peak_RX|Peak_TX|Client_Count +EOF + fi +} + +# Get analytics summary +get_analytics_summary() { + read -r input + json_load "$input" + + local period + json_get_var period period "24h" + json_cleanup + + json_init + + # Current totals + local interface + config_load bandwidth + config_get interface global interface "br-lan" + + if [ -d "/sys/class/net/$interface" ]; then + local rx_bytes=$(cat /sys/class/net/$interface/statistics/rx_bytes 2>/dev/null || echo 0) + local tx_bytes=$(cat /sys/class/net/$interface/statistics/tx_bytes 2>/dev/null || echo 0) + json_add_int "total_rx_bytes" "$rx_bytes" + json_add_int "total_tx_bytes" "$tx_bytes" + fi + + # Client count + local client_count=$(cat /tmp/dhcp.leases 2>/dev/null | wc -l) + json_add_int "active_clients" "$client_count" + + # Top talkers + json_add_array "top_talkers" + if [ -f /tmp/dhcp.leases ]; then + while read -r expires mac ip hostname clientid; do + local used_mb=$(get_mac_usage "$mac") + [ "$used_mb" -gt 0 ] && echo "$used_mb|$mac|$ip|${hostname:-unknown}" + done < /tmp/dhcp.leases | sort -t'|' -k1 -nr | head -5 | while IFS='|' read used mac ip hostname; do + json_add_object "" + json_add_string "mac" "$mac" + json_add_string "ip" "$ip" + json_add_string "hostname" "$hostname" + json_add_int "used_mb" "$used" + json_close_object + done + fi + json_close_array + + # Application breakdown (from nDPId) + json_add_array "app_breakdown" + if [ -f "$NDPID_FLOWS" ]; then + jq -r '[.flows[]? | {app: (.app // "Unknown"), bytes: (.bytes_rx + .bytes_tx)}] | group_by(.app) | map({app: .[0].app, total_bytes: (map(.bytes) | add)}) | sort_by(.total_bytes) | reverse | .[:10][] | "\(.app)|\(.total_bytes)"' "$NDPID_FLOWS" 2>/dev/null | while IFS='|' read app bytes; do + json_add_object "" + json_add_string "app" "$app" + json_add_int "bytes" "${bytes:-0}" + json_close_object + done + fi + json_close_array + + # Protocol breakdown (from nDPId) + json_add_array "protocol_breakdown" + if [ -f "$NDPID_FLOWS" ]; then + jq -r '[.flows[]? | {protocol: (.l4_proto // "Unknown"), bytes: (.bytes_rx + .bytes_tx)}] | group_by(.protocol) | map({protocol: .[0].protocol, total_bytes: (map(.bytes) | add)}) | sort_by(.total_bytes) | reverse | .[:5][] | "\(.protocol)|\(.total_bytes)"' "$NDPID_FLOWS" 2>/dev/null | while IFS='|' read proto bytes; do + json_add_object "" + json_add_string "protocol" "$proto" + json_add_int "bytes" "${bytes:-0}" + json_close_object + done + fi + json_close_array + + json_dump +} + +# Get hourly usage data for charts +get_hourly_data() { + read -r input + json_load "$input" + + local days + json_get_var days days "7" + json_cleanup + + init_analytics_db + + json_init + json_add_array "hourly_data" + + # Read from analytics DB + local threshold_date=$(date -d "$days days ago" +%Y%m%d 2>/dev/null || date +%Y%m%d) + if [ -f "$ANALYTICS_DB" ]; then + while IFS='|' read hour date rx tx peak_rx peak_tx clients; do + [ "$hour" = "# Hour" ] && continue + [ "$date" -lt "$threshold_date" ] 2>/dev/null && continue + + json_add_object "" + json_add_int "hour" "$hour" + json_add_string "date" "$date" + json_add_int "rx_bytes" "$rx" + json_add_int "tx_bytes" "$tx" + json_add_int "peak_rx" "$peak_rx" + json_add_int "peak_tx" "$peak_tx" + json_add_int "clients" "$clients" + json_close_object + done < "$ANALYTICS_DB" + fi + + json_close_array + json_dump +} + +# Record current stats (called by cron) +record_stats() { + init_analytics_db + + local interface + config_load bandwidth + config_get interface global interface "br-lan" + + local hour=$(date +%H) + local date=$(date +%Y%m%d) + local rx_bytes=0 tx_bytes=0 + + if [ -d "/sys/class/net/$interface" ]; then + rx_bytes=$(cat /sys/class/net/$interface/statistics/rx_bytes 2>/dev/null || echo 0) + tx_bytes=$(cat /sys/class/net/$interface/statistics/tx_bytes 2>/dev/null || echo 0) + fi + + local client_count=$(cat /tmp/dhcp.leases 2>/dev/null | wc -l) + + echo "$hour|$date|$rx_bytes|$tx_bytes|$rx_bytes|$tx_bytes|$client_count" >> "$ANALYTICS_DB" + + # Keep only last 30 days + local old_date=$(date -d "30 days ago" +%Y%m%d 2>/dev/null || echo "0") + if [ -f "$ANALYTICS_DB" ]; then + awk -F'|' -v old="$old_date" '$2 >= old || /^#/' "$ANALYTICS_DB" > "${ANALYTICS_DB}.tmp" + mv "${ANALYTICS_DB}.tmp" "$ANALYTICS_DB" + fi + + json_init + json_add_boolean "success" 1 + json_dump +} + # Get QoS priority classes get_classes() { json_init @@ -676,7 +1285,20 @@ case "$1" in "get_usage_realtime": {}, "get_usage_history": { "timeframe": "24h", "mac": "" }, "get_media": {}, - "get_classes": {} + "get_classes": {}, + "get_dpi_applications": {}, + "get_smart_suggestions": {}, + "apply_dpi_rule": { "app_name": "string", "priority": 5, "limit_down": 0, "limit_up": 0 }, + "list_groups": {}, + "get_group": { "group_id": "string" }, + "create_group": { "name": "string", "description": "", "quota_mb": 0, "priority": 5, "members": "" }, + "update_group": { "group_id": "string", "name": "string", "description": "", "quota_mb": 0, "priority": 5, "members": "" }, + "delete_group": { "group_id": "string" }, + "add_to_group": { "group_id": "string", "mac": "string" }, + "remove_from_group": { "group_id": "string", "mac": "string" }, + "get_analytics_summary": { "period": "24h" }, + "get_hourly_data": { "days": 7 }, + "record_stats": {} } EOF ;; @@ -694,6 +1316,19 @@ EOF get_usage_history) get_usage_history ;; get_media) get_media ;; get_classes) get_classes ;; + get_dpi_applications) get_dpi_applications ;; + get_smart_suggestions) get_smart_suggestions ;; + apply_dpi_rule) apply_dpi_rule ;; + list_groups) list_groups ;; + get_group) get_group ;; + create_group) create_group ;; + update_group) update_group ;; + delete_group) delete_group ;; + add_to_group) add_to_group ;; + remove_from_group) remove_from_group ;; + get_analytics_summary) get_analytics_summary ;; + get_hourly_data) get_hourly_data ;; + record_stats) record_stats ;; *) json_init json_add_boolean "success" 0 diff --git a/package/secubox/luci-app-bandwidth-manager/root/usr/share/luci/menu.d/luci-app-bandwidth-manager.json b/package/secubox/luci-app-bandwidth-manager/root/usr/share/luci/menu.d/luci-app-bandwidth-manager.json index 1a1c1fb3..a7729d83 100644 --- a/package/secubox/luci-app-bandwidth-manager/root/usr/share/luci/menu.d/luci-app-bandwidth-manager.json +++ b/package/secubox/luci-app-bandwidth-manager/root/usr/share/luci/menu.d/luci-app-bandwidth-manager.json @@ -80,5 +80,29 @@ "type": "view", "path": "bandwidth-manager/schedules" } + }, + "admin/secubox/network/bandwidth-manager/smart-qos": { + "title": "Smart QoS", + "order": 10, + "action": { + "type": "view", + "path": "bandwidth-manager/smart-qos" + } + }, + "admin/secubox/network/bandwidth-manager/groups": { + "title": "Device Groups", + "order": 11, + "action": { + "type": "view", + "path": "bandwidth-manager/groups" + } + }, + "admin/secubox/network/bandwidth-manager/analytics": { + "title": "Analytics", + "order": 12, + "action": { + "type": "view", + "path": "bandwidth-manager/analytics" + } } }