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>
500 lines
20 KiB
JavaScript
500 lines
20 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require rpc';
|
|
'require ui';
|
|
'require form';
|
|
'require dom';
|
|
'require poll';
|
|
|
|
var callGetSchedules = rpc.declare({
|
|
object: 'luci.bandwidth-manager',
|
|
method: 'get_schedules',
|
|
expect: { schedules: [] }
|
|
});
|
|
|
|
var callCreateSchedule = rpc.declare({
|
|
object: 'luci.bandwidth-manager',
|
|
method: 'create_schedule',
|
|
params: ['name', 'enabled', 'days', 'start_time', 'end_time', 'limit_down', 'limit_up', 'priority', 'target', 'target_type'],
|
|
expect: { success: false }
|
|
});
|
|
|
|
var callUpdateSchedule = rpc.declare({
|
|
object: 'luci.bandwidth-manager',
|
|
method: 'update_schedule',
|
|
params: ['id', 'name', 'enabled', 'days', 'start_time', 'end_time', 'limit_down', 'limit_up', 'priority', 'target', 'target_type'],
|
|
expect: { success: false }
|
|
});
|
|
|
|
var callDeleteSchedule = rpc.declare({
|
|
object: 'luci.bandwidth-manager',
|
|
method: 'delete_schedule',
|
|
params: ['id'],
|
|
expect: { success: false }
|
|
});
|
|
|
|
var callToggleSchedule = rpc.declare({
|
|
object: 'luci.bandwidth-manager',
|
|
method: 'toggle_schedule',
|
|
params: ['id', 'enabled'],
|
|
expect: { success: false }
|
|
});
|
|
|
|
var callGetDevices = rpc.declare({
|
|
object: 'luci.bandwidth-manager',
|
|
method: 'get_clients',
|
|
expect: { clients: [] }
|
|
});
|
|
|
|
var callGetGroups = rpc.declare({
|
|
object: 'luci.bandwidth-manager',
|
|
method: 'get_groups',
|
|
expect: { groups: [] }
|
|
});
|
|
|
|
var DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
var DAY_VALUES = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
|
|
|
|
var CSS = '\
|
|
.schedules-container { background: linear-gradient(135deg, #0a0a0f 0%, #050508 100%); border-radius: 12px; padding: 24px; margin: 16px 0; }\
|
|
.schedules-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; padding-bottom: 16px; border-bottom: 2px solid #25252f; }\
|
|
.schedules-title { font-size: 24px; font-weight: 700; background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }\
|
|
.schedules-list { display: grid; gap: 12px; }\
|
|
.schedule-card { background: #15151a; border: 1px solid #25252f; border-radius: 8px; padding: 16px; display: flex; align-items: center; gap: 16px; transition: all 0.2s; }\
|
|
.schedule-card:hover { border-color: #8b5cf6; }\
|
|
.schedule-card.disabled { opacity: 0.5; }\
|
|
.schedule-icon { font-size: 32px; width: 56px; height: 56px; display: flex; align-items: center; justify-content: center; background: #0a0a0f; border-radius: 8px; }\
|
|
.schedule-info { flex: 1; }\
|
|
.schedule-name { font-size: 16px; font-weight: 600; color: #fff; margin-bottom: 4px; }\
|
|
.schedule-time { font-size: 14px; color: #06b6d4; font-family: monospace; margin-bottom: 4px; }\
|
|
.schedule-details { font-size: 12px; color: #666; }\
|
|
.schedule-days { display: flex; gap: 4px; margin-top: 8px; }\
|
|
.day-badge { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; background: #0a0a0f; color: #666; border: 1px solid #25252f; }\
|
|
.day-badge.active { background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%); color: white; border-color: transparent; }\
|
|
.schedule-limits { text-align: right; min-width: 100px; }\
|
|
.limit-value { font-size: 14px; font-weight: 600; color: #10b981; }\
|
|
.limit-label { font-size: 10px; color: #666; text-transform: uppercase; }\
|
|
.schedule-actions { display: flex; gap: 8px; align-items: center; }\
|
|
.toggle-switch { position: relative; width: 48px; height: 24px; }\
|
|
.toggle-switch input { opacity: 0; width: 0; height: 0; }\
|
|
.toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #25252f; border-radius: 24px; transition: 0.3s; }\
|
|
.toggle-slider:before { content: ""; position: absolute; height: 18px; width: 18px; left: 3px; bottom: 3px; background: #666; border-radius: 50%; transition: 0.3s; }\
|
|
.toggle-switch input:checked + .toggle-slider { background: #8b5cf6; }\
|
|
.toggle-switch input:checked + .toggle-slider:before { transform: translateX(24px); background: white; }\
|
|
.btn { padding: 8px 16px; border-radius: 6px; font-weight: 600; cursor: pointer; border: none; transition: all 0.2s; display: inline-flex; align-items: center; gap: 6px; }\
|
|
.btn-primary { background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%); color: white; }\
|
|
.btn-primary:hover { box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4); }\
|
|
.btn-secondary { background: #15151a; color: #ccc; border: 1px solid #25252f; }\
|
|
.btn-secondary:hover { background: #25252f; }\
|
|
.btn-danger { background: rgba(244, 63, 94, 0.2); color: #f43f5e; }\
|
|
.btn-danger:hover { background: rgba(244, 63, 94, 0.3); }\
|
|
.btn-icon { width: 32px; height: 32px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 14px; }\
|
|
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 1000; backdrop-filter: blur(4px); }\
|
|
.modal { background: #0a0a0f; border: 1px solid #25252f; border-radius: 12px; width: 90%; max-width: 500px; max-height: 90vh; overflow-y: auto; }\
|
|
.modal-header { padding: 20px; border-bottom: 1px solid #25252f; display: flex; justify-content: space-between; align-items: center; }\
|
|
.modal-title { font-size: 18px; font-weight: 600; color: #fff; }\
|
|
.modal-close { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: #15151a; color: #999; cursor: pointer; border: none; }\
|
|
.modal-body { padding: 20px; }\
|
|
.modal-footer { padding: 16px 20px; border-top: 1px solid #25252f; display: flex; justify-content: flex-end; gap: 12px; }\
|
|
.form-group { margin-bottom: 16px; }\
|
|
.form-label { display: block; font-size: 13px; font-weight: 600; color: #999; margin-bottom: 8px; text-transform: uppercase; }\
|
|
.form-input, .form-select { width: 100%; background: #15151a; border: 1px solid #25252f; border-radius: 6px; padding: 10px 14px; color: #fff; font-size: 14px; box-sizing: border-box; }\
|
|
.form-input:focus, .form-select:focus { border-color: #8b5cf6; outline: none; }\
|
|
.form-row { display: flex; gap: 16px; }\
|
|
.form-row .form-group { flex: 1; }\
|
|
.day-selector { display: flex; gap: 8px; flex-wrap: wrap; }\
|
|
.day-btn { width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; background: #15151a; color: #666; border: 2px solid #25252f; cursor: pointer; transition: all 0.2s; }\
|
|
.day-btn:hover { border-color: #8b5cf6; }\
|
|
.day-btn.selected { background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%); color: white; border-color: transparent; }\
|
|
.empty-state { text-align: center; padding: 48px; color: #666; }\
|
|
.empty-icon { font-size: 64px; margin-bottom: 16px; opacity: 0.5; }\
|
|
.quick-presets { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }\
|
|
.preset-btn { padding: 6px 12px; background: #15151a; border: 1px solid #25252f; border-radius: 16px; color: #999; font-size: 12px; cursor: pointer; transition: all 0.2s; }\
|
|
.preset-btn:hover { border-color: #8b5cf6; color: #fff; }\
|
|
';
|
|
|
|
return view.extend({
|
|
schedules: [],
|
|
devices: [],
|
|
groups: [],
|
|
|
|
load: function() {
|
|
return Promise.all([
|
|
callGetSchedules(),
|
|
callGetDevices(),
|
|
callGetGroups()
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var self = this;
|
|
this.schedules = data[0].schedules || [];
|
|
this.devices = data[1].clients || [];
|
|
this.groups = data[2].groups || [];
|
|
|
|
var container = E('div', { 'class': 'cbi-map' }, [
|
|
E('style', {}, CSS),
|
|
E('div', { 'class': 'schedules-container' }, [
|
|
E('div', { 'class': 'schedules-header' }, [
|
|
E('span', { 'class': 'schedules-title' }, 'Time-Based Schedules'),
|
|
E('button', {
|
|
'class': 'btn btn-primary',
|
|
'click': function() { self.showScheduleModal(); }
|
|
}, ['+ New Schedule'])
|
|
]),
|
|
E('div', { 'class': 'schedules-list', 'id': 'schedules-list' },
|
|
this.schedules.length > 0
|
|
? this.schedules.map(function(s) { return self.renderScheduleCard(s); })
|
|
: [this.renderEmptyState()]
|
|
)
|
|
])
|
|
]);
|
|
|
|
return container;
|
|
},
|
|
|
|
renderScheduleCard: function(schedule) {
|
|
var self = this;
|
|
var days = schedule.days || [];
|
|
var isEnabled = schedule.enabled === '1' || schedule.enabled === true;
|
|
|
|
return E('div', {
|
|
'class': 'schedule-card' + (isEnabled ? '' : ' disabled'),
|
|
'data-id': schedule['.name'] || schedule.id
|
|
}, [
|
|
E('div', { 'class': 'schedule-icon' }, this.getScheduleIcon(schedule)),
|
|
E('div', { 'class': 'schedule-info' }, [
|
|
E('div', { 'class': 'schedule-name' }, schedule.name || 'Unnamed Schedule'),
|
|
E('div', { 'class': 'schedule-time' }, (schedule.start_time || '00:00') + ' - ' + (schedule.end_time || '23:59')),
|
|
E('div', { 'class': 'schedule-details' },
|
|
schedule.target_type === 'group' ? 'Group: ' + (schedule.target || 'All') :
|
|
schedule.target_type === 'device' ? 'Device: ' + (schedule.target || 'All') : 'All devices'
|
|
),
|
|
E('div', { 'class': 'schedule-days' },
|
|
DAY_VALUES.map(function(d, i) {
|
|
return E('span', {
|
|
'class': 'day-badge' + (days.indexOf(d) >= 0 ? ' active' : '')
|
|
}, DAYS[i].charAt(0));
|
|
})
|
|
)
|
|
]),
|
|
E('div', { 'class': 'schedule-limits' }, [
|
|
E('div', { 'class': 'limit-value' }, this.formatSpeed(schedule.limit_down || 0)),
|
|
E('div', { 'class': 'limit-label' }, 'Download'),
|
|
E('div', { 'class': 'limit-value', 'style': 'margin-top:8px;color:#06b6d4' }, this.formatSpeed(schedule.limit_up || 0)),
|
|
E('div', { 'class': 'limit-label' }, 'Upload')
|
|
]),
|
|
E('div', { 'class': 'schedule-actions' }, [
|
|
E('label', { 'class': 'toggle-switch' }, [
|
|
E('input', {
|
|
'type': 'checkbox',
|
|
'checked': isEnabled,
|
|
'change': function(ev) {
|
|
self.toggleSchedule(schedule['.name'] || schedule.id, ev.target.checked);
|
|
}
|
|
}),
|
|
E('span', { 'class': 'toggle-slider' })
|
|
]),
|
|
E('button', {
|
|
'class': 'btn btn-icon btn-secondary',
|
|
'title': 'Edit',
|
|
'click': function() { self.showScheduleModal(schedule); }
|
|
}, '\u270E'),
|
|
E('button', {
|
|
'class': 'btn btn-icon btn-danger',
|
|
'title': 'Delete',
|
|
'click': function() { self.deleteSchedule(schedule['.name'] || schedule.id); }
|
|
}, '\u2715')
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderEmptyState: function() {
|
|
return E('div', { 'class': 'empty-state' }, [
|
|
E('div', { 'class': 'empty-icon' }, '\u23F0'),
|
|
E('div', { 'style': 'font-size:16px;margin-bottom:8px;color:#999' }, 'No schedules configured'),
|
|
E('div', { 'style': 'font-size:13px' }, 'Create a schedule to apply bandwidth limits at specific times')
|
|
]);
|
|
},
|
|
|
|
getScheduleIcon: function(schedule) {
|
|
var startHour = parseInt((schedule.start_time || '00:00').split(':')[0]);
|
|
if (startHour >= 6 && startHour < 12) return '\u2600\uFE0F'; // Morning
|
|
if (startHour >= 12 && startHour < 18) return '\u26C5'; // Afternoon
|
|
if (startHour >= 18 && startHour < 22) return '\uD83C\uDF19'; // Evening
|
|
return '\uD83C\uDF19'; // Night
|
|
},
|
|
|
|
formatSpeed: function(kbps) {
|
|
if (!kbps || kbps === 0) return 'Unlimited';
|
|
if (kbps >= 1000) return (kbps / 1000).toFixed(1) + ' Mbps';
|
|
return kbps + ' Kbps';
|
|
},
|
|
|
|
showScheduleModal: function(schedule) {
|
|
var self = this;
|
|
var isEdit = !!schedule;
|
|
var selectedDays = (schedule && schedule.days) ? schedule.days.slice() : [];
|
|
|
|
var overlay = E('div', { 'class': 'modal-overlay' });
|
|
var modal = E('div', { 'class': 'modal' }, [
|
|
E('div', { 'class': 'modal-header' }, [
|
|
E('span', { 'class': 'modal-title' }, isEdit ? 'Edit Schedule' : 'New Schedule'),
|
|
E('button', { 'class': 'modal-close', 'click': function() { overlay.remove(); } }, '\u2715')
|
|
]),
|
|
E('div', { 'class': 'modal-body' }, [
|
|
E('div', { 'class': 'quick-presets' }, [
|
|
E('button', { 'class': 'preset-btn', 'click': function() {
|
|
document.getElementById('sched-start').value = '22:00';
|
|
document.getElementById('sched-end').value = '06:00';
|
|
document.getElementById('sched-name').value = 'Night Hours';
|
|
}}, 'Night (22:00-06:00)'),
|
|
E('button', { 'class': 'preset-btn', 'click': function() {
|
|
document.getElementById('sched-start').value = '18:00';
|
|
document.getElementById('sched-end').value = '23:00';
|
|
document.getElementById('sched-name').value = 'Peak Hours';
|
|
}}, 'Peak (18:00-23:00)'),
|
|
E('button', { 'class': 'preset-btn', 'click': function() {
|
|
document.getElementById('sched-start').value = '08:00';
|
|
document.getElementById('sched-end').value = '16:00';
|
|
document.getElementById('sched-name').value = 'Work Hours';
|
|
}}, 'Work (08:00-16:00)')
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', { 'class': 'form-label' }, 'Schedule Name'),
|
|
E('input', {
|
|
'type': 'text',
|
|
'class': 'form-input',
|
|
'id': 'sched-name',
|
|
'value': schedule ? schedule.name : '',
|
|
'placeholder': 'e.g., Evening Limit'
|
|
})
|
|
]),
|
|
E('div', { 'class': 'form-row' }, [
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', { 'class': 'form-label' }, 'Start Time'),
|
|
E('input', {
|
|
'type': 'time',
|
|
'class': 'form-input',
|
|
'id': 'sched-start',
|
|
'value': schedule ? schedule.start_time : '00:00'
|
|
})
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', { 'class': 'form-label' }, 'End Time'),
|
|
E('input', {
|
|
'type': 'time',
|
|
'class': 'form-input',
|
|
'id': 'sched-end',
|
|
'value': schedule ? schedule.end_time : '23:59'
|
|
})
|
|
])
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', { 'class': 'form-label' }, 'Active Days'),
|
|
E('div', { 'class': 'day-selector', 'id': 'day-selector' },
|
|
DAY_VALUES.map(function(d, i) {
|
|
return E('button', {
|
|
'type': 'button',
|
|
'class': 'day-btn' + (selectedDays.indexOf(d) >= 0 ? ' selected' : ''),
|
|
'data-day': d,
|
|
'click': function(ev) {
|
|
ev.target.classList.toggle('selected');
|
|
}
|
|
}, DAYS[i]);
|
|
})
|
|
)
|
|
]),
|
|
E('div', { 'class': 'form-row' }, [
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', { 'class': 'form-label' }, 'Download Limit (Kbps)'),
|
|
E('input', {
|
|
'type': 'number',
|
|
'class': 'form-input',
|
|
'id': 'sched-down',
|
|
'value': schedule ? schedule.limit_down : '',
|
|
'placeholder': '0 = Unlimited'
|
|
})
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', { 'class': 'form-label' }, 'Upload Limit (Kbps)'),
|
|
E('input', {
|
|
'type': 'number',
|
|
'class': 'form-input',
|
|
'id': 'sched-up',
|
|
'value': schedule ? schedule.limit_up : '',
|
|
'placeholder': '0 = Unlimited'
|
|
})
|
|
])
|
|
]),
|
|
E('div', { 'class': 'form-row' }, [
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', { 'class': 'form-label' }, 'Apply To'),
|
|
E('select', {
|
|
'class': 'form-select',
|
|
'id': 'sched-target-type',
|
|
'change': function(ev) {
|
|
var targetSelect = document.getElementById('sched-target');
|
|
targetSelect.innerHTML = '';
|
|
if (ev.target.value === 'all') {
|
|
targetSelect.style.display = 'none';
|
|
} else if (ev.target.value === 'device') {
|
|
targetSelect.style.display = '';
|
|
self.devices.forEach(function(d) {
|
|
targetSelect.appendChild(E('option', { 'value': d.mac }, d.hostname || d.mac));
|
|
});
|
|
} else if (ev.target.value === 'group') {
|
|
targetSelect.style.display = '';
|
|
self.groups.forEach(function(g) {
|
|
targetSelect.appendChild(E('option', { 'value': g['.name'] || g.name }, g.name));
|
|
});
|
|
}
|
|
}
|
|
}, [
|
|
E('option', { 'value': 'all', 'selected': !schedule || !schedule.target_type || schedule.target_type === 'all' }, 'All Devices'),
|
|
E('option', { 'value': 'device', 'selected': schedule && schedule.target_type === 'device' }, 'Specific Device'),
|
|
E('option', { 'value': 'group', 'selected': schedule && schedule.target_type === 'group' }, 'Device Group')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', { 'class': 'form-label' }, 'Target'),
|
|
E('select', {
|
|
'class': 'form-select',
|
|
'id': 'sched-target',
|
|
'style': (!schedule || !schedule.target_type || schedule.target_type === 'all') ? 'display:none' : ''
|
|
})
|
|
])
|
|
]),
|
|
E('div', { 'class': 'form-group' }, [
|
|
E('label', { 'class': 'form-label' }, 'Priority'),
|
|
E('select', { 'class': 'form-select', 'id': 'sched-priority' }, [
|
|
E('option', { 'value': '1', 'selected': schedule && schedule.priority === '1' }, 'High'),
|
|
E('option', { 'value': '2', 'selected': !schedule || schedule.priority === '2' || !schedule.priority }, 'Normal'),
|
|
E('option', { 'value': '3', 'selected': schedule && schedule.priority === '3' }, 'Low'),
|
|
E('option', { 'value': '4', 'selected': schedule && schedule.priority === '4' }, 'Bulk')
|
|
])
|
|
])
|
|
]),
|
|
E('div', { 'class': 'modal-footer' }, [
|
|
E('button', { 'class': 'btn btn-secondary', 'click': function() { overlay.remove(); } }, 'Cancel'),
|
|
E('button', { 'class': 'btn btn-primary', 'click': function() { self.saveSchedule(schedule, overlay); } },
|
|
isEdit ? 'Update' : 'Create')
|
|
])
|
|
]);
|
|
|
|
overlay.appendChild(modal);
|
|
document.body.appendChild(overlay);
|
|
|
|
// Trigger target type change to populate target dropdown
|
|
if (schedule && schedule.target_type && schedule.target_type !== 'all') {
|
|
var event = new Event('change');
|
|
document.getElementById('sched-target-type').dispatchEvent(event);
|
|
if (schedule.target) {
|
|
setTimeout(function() {
|
|
document.getElementById('sched-target').value = schedule.target;
|
|
}, 50);
|
|
}
|
|
}
|
|
},
|
|
|
|
saveSchedule: function(existingSchedule, overlay) {
|
|
var self = this;
|
|
var name = document.getElementById('sched-name').value;
|
|
var startTime = document.getElementById('sched-start').value;
|
|
var endTime = document.getElementById('sched-end').value;
|
|
var limitDown = document.getElementById('sched-down').value || '0';
|
|
var limitUp = document.getElementById('sched-up').value || '0';
|
|
var targetType = document.getElementById('sched-target-type').value;
|
|
var target = targetType !== 'all' ? document.getElementById('sched-target').value : '';
|
|
var priority = document.getElementById('sched-priority').value;
|
|
|
|
var days = [];
|
|
document.querySelectorAll('#day-selector .day-btn.selected').forEach(function(btn) {
|
|
days.push(btn.dataset.day);
|
|
});
|
|
|
|
if (!name) {
|
|
ui.addNotification(null, E('p', 'Please enter a schedule name'), 'warning');
|
|
return;
|
|
}
|
|
|
|
if (days.length === 0) {
|
|
ui.addNotification(null, E('p', 'Please select at least one day'), 'warning');
|
|
return;
|
|
}
|
|
|
|
var promise;
|
|
if (existingSchedule) {
|
|
promise = callUpdateSchedule(
|
|
existingSchedule['.name'] || existingSchedule.id,
|
|
name, '1', days, startTime, endTime,
|
|
limitDown, limitUp, priority, target, targetType
|
|
);
|
|
} else {
|
|
promise = callCreateSchedule(
|
|
name, '1', days, startTime, endTime,
|
|
limitDown, limitUp, priority, target, targetType
|
|
);
|
|
}
|
|
|
|
promise.then(function(res) {
|
|
if (res.success) {
|
|
overlay.remove();
|
|
self.refreshSchedules();
|
|
ui.addNotification(null, E('p', existingSchedule ? 'Schedule updated' : 'Schedule created'), 'success');
|
|
} else {
|
|
ui.addNotification(null, E('p', res.error || 'Failed to save schedule'), 'error');
|
|
}
|
|
}).catch(function(err) {
|
|
ui.addNotification(null, E('p', 'Error: ' + err.message), 'error');
|
|
});
|
|
},
|
|
|
|
toggleSchedule: function(id, enabled) {
|
|
var self = this;
|
|
callToggleSchedule(id, enabled ? '1' : '0').then(function(res) {
|
|
if (res.success) {
|
|
self.refreshSchedules();
|
|
} else {
|
|
ui.addNotification(null, E('p', 'Failed to toggle schedule'), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
deleteSchedule: function(id) {
|
|
var self = this;
|
|
if (!confirm('Delete this schedule?')) return;
|
|
|
|
callDeleteSchedule(id).then(function(res) {
|
|
if (res.success) {
|
|
self.refreshSchedules();
|
|
ui.addNotification(null, E('p', 'Schedule deleted'), 'success');
|
|
} else {
|
|
ui.addNotification(null, E('p', 'Failed to delete schedule'), 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
refreshSchedules: function() {
|
|
var self = this;
|
|
callGetSchedules().then(function(data) {
|
|
self.schedules = data.schedules || [];
|
|
var list = document.getElementById('schedules-list');
|
|
if (list) {
|
|
list.innerHTML = '';
|
|
if (self.schedules.length > 0) {
|
|
self.schedules.forEach(function(s) {
|
|
list.appendChild(self.renderScheduleCard(s));
|
|
});
|
|
} else {
|
|
list.appendChild(self.renderEmptyState());
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|