'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 });