secubox-openwrt/package/secubox/luci-app-bandwidth-manager/htdocs/luci-static/resources/view/bandwidth-manager/groups.js
CyberMind-FR fb9722ccd6 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>
2026-01-09 14:07:54 +01:00

593 lines
19 KiB
JavaScript

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