Major feature expansion for luci-app-bandwidth-manager: - Device Profiles: Gaming, Streaming, IoT, Work, Kids presets with custom QoS settings, bandwidth limits, and latency modes - Parental Controls: Quick preset modes (Bedtime, Homework, Family Time), access schedules, content filtering categories - Bandwidth Alerts: Threshold monitoring (80/90/100%), new device alerts, email/SMS notifications with configurable settings - Traffic Graphs: Real-time bandwidth charts, historical data visualization, top talkers list, protocol breakdown pie charts - Time Schedules: Full CRUD with day selection, limits, priority settings Backend additions: - ~30 new RPCD methods for all features - Alert monitoring cron job (every 5 minutes) - Shared alerts.sh library for email/SMS Frontend views: - profiles.js, parental-controls.js, alerts.js, traffic-graphs.js - Shared graphs.js utility for canvas drawing - parental.css for parental controls styling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
553 lines
23 KiB
JavaScript
553 lines
23 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require ui';
|
|
'require rpc';
|
|
'require poll';
|
|
'require bandwidth-manager/api as API';
|
|
|
|
var PROFILE_ICONS = {
|
|
'gamepad': { icon: '🎮', color: '#8b5cf6' },
|
|
'play': { icon: '▶️', color: '#06b6d4' },
|
|
'cpu': { icon: '🔌', color: '#10b981' },
|
|
'briefcase': { icon: '💼', color: '#3b82f6' },
|
|
'child': { icon: '👶', color: '#f59e0b' },
|
|
'tag': { icon: '🏷️', color: '#6366f1' },
|
|
'shield': { icon: '🛡️', color: '#ef4444' },
|
|
'star': { icon: '⭐', color: '#eab308' },
|
|
'home': { icon: '🏠', color: '#14b8a6' },
|
|
'globe': { icon: '🌐', color: '#8b5cf6' }
|
|
};
|
|
|
|
return L.view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
API.listProfiles(),
|
|
API.getBuiltinProfiles(),
|
|
API.listProfileAssignments(),
|
|
API.getUsageRealtime(),
|
|
API.listGroups()
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var profiles = (data[0] && data[0].profiles) || [];
|
|
var builtinProfiles = (data[1] && data[1].profiles) || [];
|
|
var assignments = (data[2] && data[2].assignments) || [];
|
|
var clients = (data[3] && data[3].clients) || [];
|
|
var groups = (data[4] && data[4].groups) || [];
|
|
var self = this;
|
|
|
|
var allProfiles = builtinProfiles.concat(profiles);
|
|
|
|
var v = E('div', { 'class': 'cbi-map' }, [
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('bandwidth-manager/dashboard.css') }),
|
|
E('style', {}, this.getCustomStyles()),
|
|
E('h2', {}, _('Device Profiles')),
|
|
E('div', { 'class': 'cbi-map-descr' }, _('Create and manage device profiles with custom QoS settings, bandwidth limits, and latency modes'))
|
|
]);
|
|
|
|
// Quick Actions Bar
|
|
var actionsBar = E('div', { 'class': 'profile-actions' }, [
|
|
E('button', {
|
|
'class': 'bw-btn bw-btn-primary',
|
|
'click': function() { self.showCreateProfileModal(clients, groups); }
|
|
}, [E('span', {}, '+'), ' ' + _('Create Profile')]),
|
|
E('button', {
|
|
'class': 'bw-btn bw-btn-secondary',
|
|
'click': function() { self.showAssignModal(allProfiles, clients); }
|
|
}, [E('span', {}, '📱'), ' ' + _('Assign to Device')])
|
|
]);
|
|
v.appendChild(actionsBar);
|
|
|
|
// Profile Cards Grid
|
|
var profilesGrid = E('div', { 'class': 'profiles-grid' });
|
|
|
|
allProfiles.forEach(function(profile) {
|
|
var iconInfo = PROFILE_ICONS[profile.icon] || PROFILE_ICONS['tag'];
|
|
var assignedCount = assignments.filter(function(a) {
|
|
return a.profile === profile.id;
|
|
}).length;
|
|
|
|
var profileCard = E('div', {
|
|
'class': 'profile-card' + (profile.builtin ? ' builtin' : ''),
|
|
'style': '--profile-color: ' + (profile.color || iconInfo.color),
|
|
'data-profile-id': profile.id
|
|
}, [
|
|
E('div', { 'class': 'profile-header' }, [
|
|
E('div', { 'class': 'profile-icon' }, iconInfo.icon),
|
|
E('div', { 'class': 'profile-info' }, [
|
|
E('div', { 'class': 'profile-name' }, profile.name),
|
|
E('div', { 'class': 'profile-desc' }, profile.description || _('No description'))
|
|
]),
|
|
profile.builtin ? E('span', { 'class': 'badge badge-info' }, _('Built-in')) : null
|
|
]),
|
|
E('div', { 'class': 'profile-stats' }, [
|
|
E('div', { 'class': 'stat' }, [
|
|
E('span', { 'class': 'stat-value' }, String(profile.priority)),
|
|
E('span', { 'class': 'stat-label' }, _('Priority'))
|
|
]),
|
|
E('div', { 'class': 'stat' }, [
|
|
E('span', { 'class': 'stat-value' }, profile.limit_down > 0 ? self.formatSpeed(profile.limit_down) : '∞'),
|
|
E('span', { 'class': 'stat-label' }, _('Down'))
|
|
]),
|
|
E('div', { 'class': 'stat' }, [
|
|
E('span', { 'class': 'stat-value' }, profile.limit_up > 0 ? self.formatSpeed(profile.limit_up) : '∞'),
|
|
E('span', { 'class': 'stat-label' }, _('Up'))
|
|
]),
|
|
E('div', { 'class': 'stat' }, [
|
|
E('span', { 'class': 'stat-value' }, String(assignedCount)),
|
|
E('span', { 'class': 'stat-label' }, _('Devices'))
|
|
])
|
|
]),
|
|
E('div', { 'class': 'profile-features' }, [
|
|
profile.latency_mode === 'ultra' ? E('span', { 'class': 'feature-badge' }, '⚡ ' + _('Ultra Low Latency')) : null,
|
|
profile.isolate ? E('span', { 'class': 'feature-badge warning' }, '🔒 ' + _('Isolated')) : null,
|
|
profile.content_filter ? E('span', { 'class': 'feature-badge' }, '🛡️ ' + _('Filtered')) : null
|
|
].filter(Boolean)),
|
|
E('div', { 'class': 'profile-actions-row' }, [
|
|
E('button', {
|
|
'class': 'bw-btn bw-btn-secondary btn-sm',
|
|
'click': function() { self.showProfileDetails(profile, assignments.filter(function(a) { return a.profile === profile.id; })); }
|
|
}, _('View')),
|
|
!profile.builtin ? E('button', {
|
|
'class': 'bw-btn bw-btn-secondary btn-sm',
|
|
'click': function() { self.showEditProfileModal(profile); }
|
|
}, _('Edit')) : null,
|
|
E('button', {
|
|
'class': 'bw-btn bw-btn-secondary btn-sm',
|
|
'click': function() { self.cloneProfile(profile); }
|
|
}, _('Clone')),
|
|
!profile.builtin ? E('button', {
|
|
'class': 'bw-btn bw-btn-secondary btn-sm btn-danger',
|
|
'click': function() { self.deleteProfile(profile); }
|
|
}, _('Delete')) : null
|
|
].filter(Boolean))
|
|
]);
|
|
|
|
profilesGrid.appendChild(profileCard);
|
|
});
|
|
|
|
v.appendChild(profilesGrid);
|
|
|
|
// Assigned Devices Section
|
|
if (assignments.length > 0) {
|
|
var assignmentsSection = E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, _('Device Assignments')),
|
|
E('div', { 'class': 'assignments-list' })
|
|
]);
|
|
|
|
var assignmentsList = assignmentsSection.querySelector('.assignments-list');
|
|
|
|
assignments.forEach(function(assignment) {
|
|
var profile = allProfiles.find(function(p) { return p.id === assignment.profile; });
|
|
var iconInfo = profile ? (PROFILE_ICONS[profile.icon] || PROFILE_ICONS['tag']) : PROFILE_ICONS['tag'];
|
|
|
|
assignmentsList.appendChild(E('div', { 'class': 'assignment-item' }, [
|
|
E('div', { 'class': 'device-info' }, [
|
|
E('div', { 'class': 'device-name' }, assignment.hostname || assignment.mac),
|
|
E('div', { 'class': 'device-mac' }, assignment.mac),
|
|
assignment.ip ? E('div', { 'class': 'device-ip' }, assignment.ip) : null
|
|
]),
|
|
E('div', { 'class': 'assignment-profile', 'style': '--profile-color: ' + (profile ? profile.color : '#6366f1') }, [
|
|
E('span', { 'class': 'profile-icon-sm' }, iconInfo.icon),
|
|
E('span', {}, assignment.profile_name || assignment.profile)
|
|
]),
|
|
E('button', {
|
|
'class': 'bw-btn bw-btn-secondary btn-sm',
|
|
'click': function() { self.removeAssignment(assignment.mac); }
|
|
}, '✕')
|
|
]));
|
|
});
|
|
|
|
v.appendChild(assignmentsSection);
|
|
}
|
|
|
|
return v;
|
|
},
|
|
|
|
showCreateProfileModal: function(clients, groups) {
|
|
var self = this;
|
|
|
|
var body = E('div', { 'class': 'profile-form' }, [
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', {}, _('Profile Name')),
|
|
E('input', { 'type': 'text', 'id': 'profile-name', 'class': 'cbi-input-text', 'placeholder': _('e.g., Gaming PC') })
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', {}, _('Description')),
|
|
E('input', { 'type': 'text', 'id': 'profile-desc', 'class': 'cbi-input-text', 'placeholder': _('Optional description') })
|
|
]),
|
|
E('div', { 'class': 'form-row' }, [
|
|
E('div', { 'class': 'form-group half' }, [
|
|
E('label', {}, _('Icon')),
|
|
E('select', { 'id': 'profile-icon', 'class': 'cbi-input-select' },
|
|
Object.keys(PROFILE_ICONS).map(function(key) {
|
|
return E('option', { 'value': key }, PROFILE_ICONS[key].icon + ' ' + key);
|
|
})
|
|
)
|
|
]),
|
|
E('div', { 'class': 'form-group half' }, [
|
|
E('label', {}, _('Color')),
|
|
E('input', { 'type': 'color', 'id': 'profile-color', 'value': '#8b5cf6' })
|
|
])
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', {}, _('Priority (1-8, lower = higher priority)')),
|
|
E('input', { 'type': 'number', 'id': 'profile-priority', 'class': 'cbi-input-text', 'min': '1', 'max': '8', 'value': '5' })
|
|
]),
|
|
E('div', { 'class': 'form-row' }, [
|
|
E('div', { 'class': 'form-group half' }, [
|
|
E('label', {}, _('Download Limit (kbit/s, 0 = unlimited)')),
|
|
E('input', { 'type': 'number', 'id': 'profile-limit-down', 'class': 'cbi-input-text', 'min': '0', 'value': '0' })
|
|
]),
|
|
E('div', { 'class': 'form-group half' }, [
|
|
E('label', {}, _('Upload Limit (kbit/s, 0 = unlimited)')),
|
|
E('input', { 'type': 'number', 'id': 'profile-limit-up', 'class': 'cbi-input-text', 'min': '0', 'value': '0' })
|
|
])
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', {}, _('Latency Mode')),
|
|
E('select', { 'id': 'profile-latency', 'class': 'cbi-input-select' }, [
|
|
E('option', { 'value': 'normal' }, _('Normal')),
|
|
E('option', { 'value': 'low' }, _('Low Latency')),
|
|
E('option', { 'value': 'ultra' }, _('Ultra Low Latency (Gaming)'))
|
|
])
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', {}, [
|
|
E('input', { 'type': 'checkbox', 'id': 'profile-isolate' }),
|
|
' ' + _('Network Isolation (block LAN communication)')
|
|
])
|
|
])
|
|
]);
|
|
|
|
ui.showModal(_('Create New Profile'), [
|
|
body,
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': function() { self.createProfile(); }
|
|
}, _('Create Profile'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
createProfile: function() {
|
|
var self = this;
|
|
var name = document.getElementById('profile-name').value;
|
|
var description = document.getElementById('profile-desc').value;
|
|
var icon = document.getElementById('profile-icon').value;
|
|
var color = document.getElementById('profile-color').value;
|
|
var priority = parseInt(document.getElementById('profile-priority').value) || 5;
|
|
var limit_down = parseInt(document.getElementById('profile-limit-down').value) || 0;
|
|
var limit_up = parseInt(document.getElementById('profile-limit-up').value) || 0;
|
|
var latency_mode = document.getElementById('profile-latency').value;
|
|
var isolate = document.getElementById('profile-isolate').checked ? '1' : '0';
|
|
|
|
if (!name) {
|
|
ui.addNotification(null, E('p', _('Profile name is required')), 'error');
|
|
return;
|
|
}
|
|
|
|
API.createProfile(name, description, icon, color, priority, limit_down, limit_up, latency_mode, '', isolate, '').then(function(result) {
|
|
if (result.success) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', _('Profile created successfully')), 'success');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', result.message || _('Failed to create profile')), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
showEditProfileModal: function(profile) {
|
|
var self = this;
|
|
|
|
var body = E('div', { 'class': 'profile-form' }, [
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', {}, _('Profile Name')),
|
|
E('input', { 'type': 'text', 'id': 'edit-profile-name', 'class': 'cbi-input-text', 'value': profile.name })
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', {}, _('Description')),
|
|
E('input', { 'type': 'text', 'id': 'edit-profile-desc', 'class': 'cbi-input-text', 'value': profile.description || '' })
|
|
]),
|
|
E('div', { 'class': 'form-row' }, [
|
|
E('div', { 'class': 'form-group half' }, [
|
|
E('label', {}, _('Icon')),
|
|
E('select', { 'id': 'edit-profile-icon', 'class': 'cbi-input-select' },
|
|
Object.keys(PROFILE_ICONS).map(function(key) {
|
|
return E('option', { 'value': key, 'selected': key === profile.icon }, PROFILE_ICONS[key].icon + ' ' + key);
|
|
})
|
|
)
|
|
]),
|
|
E('div', { 'class': 'form-group half' }, [
|
|
E('label', {}, _('Color')),
|
|
E('input', { 'type': 'color', 'id': 'edit-profile-color', 'value': profile.color || '#8b5cf6' })
|
|
])
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', {}, _('Priority (1-8)')),
|
|
E('input', { 'type': 'number', 'id': 'edit-profile-priority', 'class': 'cbi-input-text', 'min': '1', 'max': '8', 'value': String(profile.priority) })
|
|
]),
|
|
E('div', { 'class': 'form-row' }, [
|
|
E('div', { 'class': 'form-group half' }, [
|
|
E('label', {}, _('Download Limit (kbit/s)')),
|
|
E('input', { 'type': 'number', 'id': 'edit-profile-limit-down', 'class': 'cbi-input-text', 'min': '0', 'value': String(profile.limit_down) })
|
|
]),
|
|
E('div', { 'class': 'form-group half' }, [
|
|
E('label', {}, _('Upload Limit (kbit/s)')),
|
|
E('input', { 'type': 'number', 'id': 'edit-profile-limit-up', 'class': 'cbi-input-text', 'min': '0', 'value': String(profile.limit_up) })
|
|
])
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', {}, _('Latency Mode')),
|
|
E('select', { 'id': 'edit-profile-latency', 'class': 'cbi-input-select' }, [
|
|
E('option', { 'value': 'normal', 'selected': profile.latency_mode === 'normal' }, _('Normal')),
|
|
E('option', { 'value': 'low', 'selected': profile.latency_mode === 'low' }, _('Low Latency')),
|
|
E('option', { 'value': 'ultra', 'selected': profile.latency_mode === 'ultra' }, _('Ultra Low Latency'))
|
|
])
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', {}, [
|
|
E('input', { 'type': 'checkbox', 'id': 'edit-profile-isolate', 'checked': profile.isolate }),
|
|
' ' + _('Network Isolation')
|
|
])
|
|
])
|
|
]);
|
|
|
|
ui.showModal(_('Edit Profile: ') + profile.name, [
|
|
body,
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': function() { self.updateProfile(profile.id); }
|
|
}, _('Save Changes'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
updateProfile: function(profileId) {
|
|
var self = this;
|
|
var name = document.getElementById('edit-profile-name').value;
|
|
var description = document.getElementById('edit-profile-desc').value;
|
|
var icon = document.getElementById('edit-profile-icon').value;
|
|
var color = document.getElementById('edit-profile-color').value;
|
|
var priority = parseInt(document.getElementById('edit-profile-priority').value) || 5;
|
|
var limit_down = parseInt(document.getElementById('edit-profile-limit-down').value) || 0;
|
|
var limit_up = parseInt(document.getElementById('edit-profile-limit-up').value) || 0;
|
|
var latency_mode = document.getElementById('edit-profile-latency').value;
|
|
var isolate = document.getElementById('edit-profile-isolate').checked ? '1' : '0';
|
|
|
|
API.updateProfile(profileId, name, description, icon, color, priority, limit_down, limit_up, latency_mode, '', isolate, '', '1').then(function(result) {
|
|
if (result.success) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', _('Profile updated successfully')), 'success');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', result.message || _('Failed to update profile')), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
deleteProfile: function(profile) {
|
|
var self = this;
|
|
if (!confirm(_('Delete profile "%s"? This will also remove all device assignments.').format(profile.name))) {
|
|
return;
|
|
}
|
|
|
|
API.deleteProfile(profile.id).then(function(result) {
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', _('Profile deleted')), 'success');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', result.message || _('Failed to delete profile')), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
cloneProfile: function(profile) {
|
|
var self = this;
|
|
var newName = prompt(_('Enter name for cloned profile:'), profile.name + ' (Copy)');
|
|
if (!newName) return;
|
|
|
|
API.cloneProfile(profile.id, newName).then(function(result) {
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', _('Profile cloned successfully')), 'success');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', result.message || _('Failed to clone profile')), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
showAssignModal: function(profiles, clients) {
|
|
var self = this;
|
|
|
|
var body = E('div', { 'class': 'assign-form' }, [
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', {}, _('Select Device')),
|
|
E('select', { 'id': 'assign-device', 'class': 'cbi-input-select' },
|
|
clients.map(function(client) {
|
|
return E('option', { 'value': client.mac }, (client.hostname || 'Unknown') + ' (' + client.mac + ')');
|
|
})
|
|
)
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', {}, _('Select Profile')),
|
|
E('select', { 'id': 'assign-profile', 'class': 'cbi-input-select' },
|
|
profiles.map(function(profile) {
|
|
var iconInfo = PROFILE_ICONS[profile.icon] || PROFILE_ICONS['tag'];
|
|
return E('option', { 'value': profile.id }, iconInfo.icon + ' ' + profile.name);
|
|
})
|
|
)
|
|
])
|
|
]);
|
|
|
|
ui.showModal(_('Assign Profile to Device'), [
|
|
body,
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': function() { self.assignProfile(); }
|
|
}, _('Assign'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
assignProfile: function() {
|
|
var mac = document.getElementById('assign-device').value;
|
|
var profileId = document.getElementById('assign-profile').value;
|
|
|
|
API.assignProfileToDevice(mac, profileId, 0, 0).then(function(result) {
|
|
if (result.success) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', _('Profile assigned to device')), 'success');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', result.message || _('Failed to assign profile')), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
removeAssignment: function(mac) {
|
|
if (!confirm(_('Remove profile assignment for this device?'))) {
|
|
return;
|
|
}
|
|
|
|
API.removeProfileAssignment(mac).then(function(result) {
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', _('Assignment removed')), 'success');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', result.message || _('Failed to remove assignment')), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
showProfileDetails: function(profile, assignments) {
|
|
var iconInfo = PROFILE_ICONS[profile.icon] || PROFILE_ICONS['tag'];
|
|
|
|
var details = E('div', { 'class': 'profile-details' }, [
|
|
E('div', { 'class': 'detail-header', 'style': '--profile-color: ' + (profile.color || iconInfo.color) }, [
|
|
E('span', { 'class': 'profile-icon-lg' }, iconInfo.icon),
|
|
E('div', {}, [
|
|
E('h3', {}, profile.name),
|
|
E('p', {}, profile.description || _('No description'))
|
|
])
|
|
]),
|
|
E('table', { 'class': 'table' }, [
|
|
E('tr', {}, [E('td', {}, _('Priority')), E('td', {}, String(profile.priority))]),
|
|
E('tr', {}, [E('td', {}, _('Download Limit')), E('td', {}, profile.limit_down > 0 ? this.formatSpeed(profile.limit_down) : _('Unlimited'))]),
|
|
E('tr', {}, [E('td', {}, _('Upload Limit')), E('td', {}, profile.limit_up > 0 ? this.formatSpeed(profile.limit_up) : _('Unlimited'))]),
|
|
E('tr', {}, [E('td', {}, _('Latency Mode')), E('td', {}, profile.latency_mode)]),
|
|
E('tr', {}, [E('td', {}, _('Network Isolation')), E('td', {}, profile.isolate ? _('Yes') : _('No'))]),
|
|
E('tr', {}, [E('td', {}, _('Assigned Devices')), E('td', {}, String(assignments.length))])
|
|
])
|
|
]);
|
|
|
|
if (assignments.length > 0) {
|
|
details.appendChild(E('h4', {}, _('Assigned Devices:')));
|
|
var deviceList = E('ul', { 'class': 'device-list' });
|
|
assignments.forEach(function(a) {
|
|
deviceList.appendChild(E('li', {}, (a.hostname || 'Unknown') + ' - ' + a.mac));
|
|
});
|
|
details.appendChild(deviceList);
|
|
}
|
|
|
|
ui.showModal(_('Profile Details'), [
|
|
details,
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
formatSpeed: function(kbits) {
|
|
if (kbits >= 1000000) {
|
|
return (kbits / 1000000).toFixed(1) + ' Gbit/s';
|
|
} else if (kbits >= 1000) {
|
|
return (kbits / 1000).toFixed(1) + ' Mbit/s';
|
|
}
|
|
return kbits + ' kbit/s';
|
|
},
|
|
|
|
getCustomStyles: function() {
|
|
return `
|
|
.profile-actions { margin-bottom: 20px; display: flex; gap: 12px; }
|
|
.profiles-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; margin-bottom: 24px; }
|
|
.profile-card { background: var(--bw-light, #15151a); border: 1px solid var(--bw-border, #25252f); border-radius: 12px; padding: 20px; position: relative; overflow: hidden; transition: all 0.2s; }
|
|
.profile-card::before { content: ""; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: var(--profile-color, #8b5cf6); }
|
|
.profile-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(139, 92, 246, 0.2); }
|
|
.profile-card.builtin { border-style: dashed; }
|
|
.profile-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
|
.profile-icon { font-size: 32px; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; background: rgba(139, 92, 246, 0.1); border-radius: 12px; }
|
|
.profile-info { flex: 1; }
|
|
.profile-name { font-size: 18px; font-weight: 600; color: #fff; }
|
|
.profile-desc { font-size: 13px; color: #999; margin-top: 4px; }
|
|
.profile-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px; padding: 12px; background: var(--bw-dark, #0a0a0f); border-radius: 8px; }
|
|
.stat { text-align: center; }
|
|
.stat-value { display: block; font-size: 18px; font-weight: 700; background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
.stat-label { font-size: 11px; color: #666; text-transform: uppercase; }
|
|
.profile-features { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; min-height: 28px; }
|
|
.feature-badge { font-size: 11px; padding: 4px 8px; background: rgba(139, 92, 246, 0.2); color: #a78bfa; border-radius: 4px; }
|
|
.feature-badge.warning { background: rgba(245, 158, 11, 0.2); color: #fbbf24; }
|
|
.profile-actions-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
.btn-sm { padding: 6px 12px; font-size: 12px; }
|
|
.btn-danger { color: #ef4444; }
|
|
.btn-danger:hover { background: rgba(239, 68, 68, 0.2); }
|
|
.badge { padding: 4px 8px; border-radius: 4px; font-size: 10px; text-transform: uppercase; font-weight: 600; }
|
|
.badge-info { background: rgba(59, 130, 246, 0.2); color: #60a5fa; }
|
|
.assignments-list { display: grid; gap: 12px; }
|
|
.assignment-item { display: flex; align-items: center; justify-content: space-between; background: var(--bw-light, #15151a); border: 1px solid var(--bw-border, #25252f); border-radius: 8px; padding: 12px 16px; }
|
|
.device-info { flex: 1; }
|
|
.device-name { font-weight: 600; color: #fff; }
|
|
.device-mac { font-family: monospace; font-size: 12px; color: #666; }
|
|
.device-ip { font-size: 12px; color: #999; }
|
|
.assignment-profile { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: rgba(139, 92, 246, 0.1); border-radius: 6px; margin: 0 12px; }
|
|
.profile-icon-sm { font-size: 16px; }
|
|
.form-group { margin-bottom: 16px; }
|
|
.form-group label { display: block; margin-bottom: 6px; font-weight: 500; color: #ccc; }
|
|
.form-row { display: flex; gap: 16px; }
|
|
.form-group.half { flex: 1; }
|
|
.profile-details .detail-header { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid var(--bw-border, #25252f); }
|
|
.profile-icon-lg { font-size: 48px; }
|
|
.device-list { list-style: none; padding: 0; margin: 0; }
|
|
.device-list li { padding: 8px 12px; background: var(--bw-dark, #0a0a0f); border-radius: 4px; margin-bottom: 8px; font-family: monospace; font-size: 13px; }
|
|
`;
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|