feat(bandwidth-manager): Add Smart QoS, Device Groups, and Analytics (Phase 5)

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>
This commit is contained in:
CyberMind-FR 2026-01-09 14:07:54 +01:00
parent a9e5bc0262
commit fb9722ccd6
5 changed files with 2034 additions and 1 deletions

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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"
}
}
}