secubox-openwrt/package/secubox/luci-app-bandwidth-manager/htdocs/luci-static/resources/view/bandwidth-manager/groups.js
CyberMind-FR 1bbd345cee refactor(luci): Mass KissTheme UI rework across all LuCI apps
Convert 90+ LuCI view files from legacy cbi-button-* classes to
KissTheme kiss-btn-* classes for consistent dark theme styling.

Pattern conversions applied:
- cbi-button-positive → kiss-btn-green
- cbi-button-negative/remove → kiss-btn-red
- cbi-button-apply → kiss-btn-cyan
- cbi-button-action → kiss-btn-blue
- cbi-button (plain) → kiss-btn

Also replaced hardcoded colors (#080, #c00, #888, etc.) with
CSS variables (--kiss-green, --kiss-red, --kiss-muted, etc.)
for proper dark theme compatibility.

Apps updated include: ai-gateway, auth-guardian, bandwidth-manager,
cloner, config-advisor, crowdsec-dashboard, dns-provider, exposure,
glances, haproxy, hexojs, iot-guard, jellyfin, ksm-manager,
mac-guardian, magicmirror2, master-link, meshname-dns, metablogizer,
metabolizer, mqtt-bridge, netdata-dashboard, picobrew, routes-status,
secubox-admin, secubox-mirror, secubox-p2p, secubox-security-threats,
service-registry, simplex, streamlit, system-hub, tor-shield,
traffic-shaper, vhost-manager, vortex-dns, vortex-firewall,
webradio, wireguard-dashboard, zigbee2mqtt, zkp, and more.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-12 11:09:34 +01:00

582 lines
20 KiB
JavaScript

'use strict';
'require view';
'require dom';
'require poll';
'require rpc';
'require ui';
'require secubox/kiss-theme';
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({
handleSaveApply: null,
handleSave: null,
handleReset: null,
groups: [],
clients: [],
classes: [],
load: function() {
return Promise.all([
callListGroups(),
callGetUsageRealtime(),
callGetClasses()
]);
},
renderStats: function() {
var c = KissTheme.colors;
var totalMembers = this.groups.reduce(function(sum, g) { return sum + (g.member_count || 0); }, 0);
var activeGroups = this.groups.filter(function(g) { return g.enabled; }).length;
return [
KissTheme.stat(this.groups.length, 'Total Groups', c.blue),
KissTheme.stat(activeGroups, 'Active', c.green),
KissTheme.stat(totalMembers, 'Total Members', c.purple),
KissTheme.stat(this.clients.length, 'Online Clients', c.cyan)
];
},
renderGroupsGrid: function() {
var self = this;
if (this.groups.length === 0) {
return E('div', {
'style': 'padding: 40px; text-align: center; color: var(--kiss-muted); background: var(--kiss-bg); border-radius: 10px; border: 1px dashed var(--kiss-line);'
}, [
E('div', { 'style': 'font-size: 48px; margin-bottom: 16px;' }, '\ud83d\udc65'),
E('div', { 'style': 'font-weight: 600; margin-bottom: 8px;' }, 'No Groups Created'),
E('div', { 'style': 'font-size: 13px; margin-bottom: 20px;' }, 'Create device groups to apply shared quotas and priorities'),
E('button', {
'class': 'kiss-btn kiss-btn-green',
'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'
};
return E('div', {
'style': 'display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px;'
}, this.groups.map(function(group) {
var icon = presetIcons[group.name] || '\ud83d\udc65';
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 ? 'var(--kiss-red)' : usagePercent > 70 ? 'var(--kiss-orange)' : 'var(--kiss-green)';
return E('div', {
'style': 'background: var(--kiss-bg2); border: 1px solid var(--kiss-line); border-radius: 10px; overflow: hidden;'
}, [
// Header
E('div', { 'style': 'padding: 16px; border-bottom: 1px solid var(--kiss-line);' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 12px;' }, [
E('div', {
'style': 'width: 40px; height: 40px; background: var(--kiss-bg); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 20px;'
}, icon),
E('div', { 'style': 'flex: 1;' }, [
E('div', { 'style': 'font-weight: 600;' }, group.name),
E('div', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, group.description || 'No description')
]),
group.enabled ? KissTheme.badge('Active', 'green') : KissTheme.badge('Disabled', 'red')
])
]),
// Stats
E('div', { 'style': 'padding: 16px;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 16px;' }, [
E('div', { 'style': 'text-align: center;' }, [
E('div', { 'style': 'font-size: 24px; font-weight: 700;' }, String(group.member_count)),
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted);' }, 'Devices')
]),
E('div', { 'style': 'text-align: center;' }, [
E('div', { 'style': 'font-size: 24px; font-weight: 700;' }, 'P' + (group.priority || 5)),
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted);' }, 'Priority')
]),
E('div', { 'style': 'text-align: center;' }, [
E('div', { 'style': 'font-size: 24px; font-weight: 700;' }, group.quota_mb > 0 ? self.formatMB(group.quota_mb) : '\u221e'),
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted);' }, 'Quota')
])
]),
// Usage Progress
group.quota_mb > 0 ? E('div', {}, [
E('div', { 'style': 'display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 4px;' }, [
E('span', { 'style': 'color: var(--kiss-muted);' }, 'Usage'),
E('span', {}, self.formatMB(group.used_mb) + ' / ' + self.formatMB(group.quota_mb) + ' (' + usagePercent + '%)')
]),
E('div', {
'style': 'height: 6px; background: var(--kiss-bg); border-radius: 3px; overflow: hidden;'
}, [
E('div', {
'style': 'height: 100%; width: ' + usagePercent + '%; background: ' + progressColor + ';'
})
])
]) : ''
]),
// Actions
E('div', { 'style': 'padding: 12px 16px; background: var(--kiss-bg); display: flex; gap: 8px;' }, [
E('button', {
'class': 'kiss-btn kiss-btn-blue',
'style': 'flex: 1; font-size: 12px;',
'click': function() { self.showGroupDetails(group.id); }
}, 'Manage'),
E('button', {
'class': 'kiss-btn',
'style': 'flex: 1; font-size: 12px;',
'click': function() { self.showEditGroupDialog(group); }
}, 'Edit'),
E('button', {
'class': 'kiss-btn kiss-btn-red',
'style': 'font-size: 12px; padding: 6px 12px;',
'click': function() { self.deleteGroup(group); }
}, '\u2717')
])
]);
}));
},
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) || [];
poll.add(L.bind(this.pollData, this), 15);
var content = [
// Header
E('div', { 'style': 'margin-bottom: 24px;' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'Device Groups'),
KissTheme.badge(this.groups.length + ' groups', 'blue'),
E('button', {
'class': 'kiss-btn kiss-btn-green',
'style': 'margin-left: auto;',
'click': function() { self.showCreateGroupDialog(); }
}, 'Create Group')
]),
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' },
'Organize devices into groups for shared quotas and unified QoS policies')
]),
// Stats
E('div', { 'class': 'kiss-grid kiss-grid-4', 'id': 'groups-stats', 'style': 'margin: 20px 0;' },
this.renderStats()),
// Groups Grid
E('div', { 'id': 'groups-container' }, [
this.renderGroupsGrid()
])
];
return KissTheme.wrap(content, 'admin/services/bandwidth-manager/groups');
},
pollData: function() {
var self = this;
return callListGroups().then(function(data) {
self.groups = (data && data.groups) || [];
var container = document.getElementById('groups-container');
var statsContainer = document.getElementById('groups-stats');
if (container) {
dom.content(container, self.renderGroupsGrid());
}
if (statsContainer) {
dom.content(statsContainer, self.renderStats());
}
});
},
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: 16px;' }, [
E('label', { 'style': 'display: block; margin-bottom: 8px; font-weight: 500;' }, 'Quick Presets'),
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 8px;' },
presets.map(function(preset) {
return E('button', {
'class': 'kiss-btn',
'style': 'display: flex; align-items: center; gap: 6px;',
'click': function() {
document.getElementById('group-name').value = preset.name;
document.getElementById('group-desc').value = preset.desc;
}
}, [preset.icon, ' ', preset.name]);
})
)
]),
E('hr', { 'style': 'margin: 16px 0; border-color: var(--kiss-line);' }),
E('div', { 'style': 'margin-bottom: 16px;' }, [
E('label', { 'style': 'display: block; margin-bottom: 6px;' }, 'Group Name *'),
E('input', {
'type': 'text',
'class': 'cbi-input-text',
'id': 'group-name',
'placeholder': 'Enter group name',
'style': 'width: 100%; padding: 8px; background: var(--kiss-bg); border: 1px solid var(--kiss-line); border-radius: 6px; color: var(--kiss-text);'
})
]),
E('div', { 'style': 'margin-bottom: 16px;' }, [
E('label', { 'style': 'display: block; margin-bottom: 6px;' }, 'Description'),
E('input', {
'type': 'text',
'class': 'cbi-input-text',
'id': 'group-desc',
'placeholder': 'Optional description',
'style': 'width: 100%; padding: 8px; background: var(--kiss-bg); border: 1px solid var(--kiss-line); border-radius: 6px; color: var(--kiss-text);'
})
]),
E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;' }, [
E('div', {}, [
E('label', { 'style': 'display: block; margin-bottom: 6px;' }, 'Shared Quota (MB)'),
E('input', {
'type': 'number',
'id': 'group-quota',
'value': '0',
'min': '0',
'placeholder': '0 = unlimited',
'style': 'width: 100%; padding: 8px; background: var(--kiss-bg); border: 1px solid var(--kiss-line); border-radius: 6px; color: var(--kiss-text);'
})
]),
E('div', {}, [
E('label', { 'style': 'display: block; margin-bottom: 6px;' }, 'Priority Class'),
E('select', {
'id': 'group-priority',
'style': 'width: 100%; padding: 8px; background: var(--kiss-bg); border: 1px solid var(--kiss-line); border-radius: 6px; color: var(--kiss-text);'
}, this.classes.map(function(c) {
return E('option', { 'value': c.priority, 'selected': c.priority === 5 }, c.priority + ' - ' + c.name);
}))
])
]),
E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px;' }, [
E('button', { 'class': 'kiss-btn', 'click': ui.hideModal }, 'Cancel'),
E('button', {
'class': 'kiss-btn kiss-btn-green',
'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: 16px;' }, [
E('label', { 'style': 'display: block; margin-bottom: 6px;' }, 'Group Name *'),
E('input', {
'type': 'text',
'id': 'edit-group-name',
'value': group.name,
'style': 'width: 100%; padding: 8px; background: var(--kiss-bg); border: 1px solid var(--kiss-line); border-radius: 6px; color: var(--kiss-text);'
})
]),
E('div', { 'style': 'margin-bottom: 16px;' }, [
E('label', { 'style': 'display: block; margin-bottom: 6px;' }, 'Description'),
E('input', {
'type': 'text',
'id': 'edit-group-desc',
'value': group.description || '',
'style': 'width: 100%; padding: 8px; background: var(--kiss-bg); border: 1px solid var(--kiss-line); border-radius: 6px; color: var(--kiss-text);'
})
]),
E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;' }, [
E('div', {}, [
E('label', { 'style': 'display: block; margin-bottom: 6px;' }, 'Shared Quota (MB)'),
E('input', {
'type': 'number',
'id': 'edit-group-quota',
'value': group.quota_mb || '0',
'min': '0',
'style': 'width: 100%; padding: 8px; background: var(--kiss-bg); border: 1px solid var(--kiss-line); border-radius: 6px; color: var(--kiss-text);'
})
]),
E('div', {}, [
E('label', { 'style': 'display: block; margin-bottom: 6px;' }, 'Priority Class'),
E('select', {
'id': 'edit-group-priority',
'style': 'width: 100%; padding: 8px; background: var(--kiss-bg); border: 1px solid var(--kiss-line); border-radius: 6px; color: var(--kiss-text);'
}, this.classes.map(function(c) {
return E('option', {
'value': c.priority,
'selected': c.priority === (group.priority || 5)
}, c.priority + ' - ' + c.name);
}))
])
]),
E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px;' }, [
E('button', { 'class': 'kiss-btn', 'click': ui.hideModal }, 'Cancel'),
E('button', {
'class': 'kiss-btn kiss-btn-green',
'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: 16px;' }, 'Group Members (' + members.length + ')'),
members.length > 0 ?
E('div', { 'style': 'max-height: 200px; overflow-y: auto; margin-bottom: 16px;' }, [
E('table', { 'class': 'kiss-table' }, [
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: 11px; color: var(--kiss-muted);' }, member.mac)
]),
E('td', {}, member.ip || '-'),
E('td', {}, self.formatMB(member.used_mb || 0)),
E('td', {}, [
E('button', {
'class': 'kiss-btn kiss-btn-red',
'style': 'font-size: 11px; padding: 4px 8px;',
'click': function() {
callRemoveFromGroup(groupId, member.mac).then(function(res) {
if (res.success) {
ui.hideModal();
self.showGroupDetails(groupId);
self.pollData();
}
});
}
}, 'Remove')
])
]);
})
)
])
]) :
E('div', {
'style': 'padding: 16px; text-align: center; color: var(--kiss-muted); background: var(--kiss-bg); border-radius: 8px; margin-bottom: 16px;'
}, 'No devices in this group'),
E('h4', { 'style': 'margin-bottom: 16px;' }, 'Add Devices'),
availableClients.length > 0 ?
E('div', { 'style': 'max-height: 200px; overflow-y: auto; margin-bottom: 16px;' }, [
E('table', { 'class': 'kiss-table' }, [
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: 11px; color: var(--kiss-muted);' }, client.mac)
]),
E('td', {}, client.ip || '-'),
E('td', {}, [
E('button', {
'class': 'kiss-btn kiss-btn-green',
'style': 'font-size: 11px; padding: 4px 8px;',
'click': function() {
callAddToGroup(groupId, client.mac).then(function(res) {
if (res.success) {
ui.hideModal();
self.showGroupDetails(groupId);
self.pollData();
}
});
}
}, 'Add')
])
]);
})
)
])
]) :
E('div', {
'style': 'padding: 16px; text-align: center; color: var(--kiss-muted); background: var(--kiss-bg); border-radius: 8px; margin-bottom: 16px;'
}, 'No available devices to add'),
E('div', { 'style': 'display: flex; justify-content: flex-end;' }, [
E('button', { 'class': 'kiss-btn', '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(--kiss-muted);' }, 'This will not affect the devices in the group.'),
E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px;' }, [
E('button', { 'class': 'kiss-btn', 'click': ui.hideModal }, 'Cancel'),
E('button', {
'class': 'kiss-btn kiss-btn-red',
'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';
}
});