Advanced Bandwidth Manager features v0.5.0 Smart QoS (DPI Integration): - Real-time application detection via nDPId - Smart traffic suggestions based on detected patterns - One-click DPI rule creation for applications - Gaming, streaming, video conferencing detection - Heavy downloader identification Device Groups: - Create device groups (Family, IoT, Work, Gaming, Kids, Guests) - Shared quota across group members - Unified priority assignment per group - Easy member management via drag-drop UI - Group usage tracking and visualization Analytics Dashboard: - Traffic summary with download/upload totals - Active client count and per-client averages - Application traffic breakdown charts - Protocol distribution pie chart - Top bandwidth users leaderboard - Download/upload ratio analysis - Historical data retention (30 days) - Period selection (1h, 6h, 24h, 7d, 30d) Backend Enhancements: - get_dpi_applications: Fetch detected apps from nDPId - get_smart_suggestions: AI-powered QoS recommendations - apply_dpi_rule: Create rules based on app detection - list_groups/create_group/update_group/delete_group - add_to_group/remove_from_group: Member management - get_analytics_summary: Traffic statistics - get_hourly_data: Historical trends - record_stats: Cron-based data collection Menu Additions: - Smart QoS (order: 10) - Device Groups (order: 11) - Analytics (order: 12) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
400 lines
12 KiB
JavaScript
400 lines
12 KiB
JavaScript
'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
|
|
});
|