feat: Add scheduled backups, live logs, and component detection (v0.6.0-r30)
System Hub enhancements: - Add cron-based scheduled backup configuration (daily/weekly/monthly) - Add backup schedule RPCD methods (get_backup_schedule, set_backup_schedule) - Add live streaming logs with LIVE badge, play/pause, 2s refresh - Add real component installation detection from secubox state field - Add service running status detection for components - Add category-based icons for components (security, network, monitoring) - Fix status emoji display (✅ ⚠️ ❓) for Quick Status Indicators UI improvements: - New Scheduled Backups card in backup page with enable/disable toggle - Time picker for backup schedule (hour/minute selectors) - Day of week/month selectors for weekly/monthly backups - Live indicator badge with pulse animation for logs - Play/Pause button for log streaming control - New log highlighting with fade-in animation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
327cc5b285
commit
8255cc6f39
@ -63,6 +63,19 @@ var callRestoreConfig = rpc.declare({
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callGetBackupSchedule = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'get_backup_schedule',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callSetBackupSchedule = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'set_backup_schedule',
|
||||
params: ['enabled', 'frequency', 'hour', 'minute', 'day_of_week', 'day_of_month'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callReboot = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'reboot',
|
||||
@ -214,6 +227,10 @@ return baseclass.extend({
|
||||
data: data
|
||||
});
|
||||
},
|
||||
getBackupSchedule: callGetBackupSchedule,
|
||||
setBackupSchedule: function(data) {
|
||||
return callSetBackupSchedule(data);
|
||||
},
|
||||
reboot: callReboot,
|
||||
getStorage: callGetStorage,
|
||||
getSettings: callGetSettings,
|
||||
|
||||
@ -98,3 +98,86 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Schedule form styles */
|
||||
.sh-schedule-form {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.sh-toggle-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sh-toggle-main input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
accent-color: var(--sh-accent, #6366f1);
|
||||
}
|
||||
|
||||
.sh-schedule-options {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.sh-form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sh-form-row > label {
|
||||
min-width: 100px;
|
||||
font-size: 14px;
|
||||
color: var(--sh-text-secondary);
|
||||
}
|
||||
|
||||
.sh-select {
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--sh-border);
|
||||
background: rgba(15,23,42,0.6);
|
||||
color: var(--sh-text-primary);
|
||||
font-size: 14px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.sh-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--sh-accent, #6366f1);
|
||||
}
|
||||
|
||||
.sh-select-time {
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.sh-time-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sh-time-picker span {
|
||||
font-size: 18px;
|
||||
color: var(--sh-text-secondary);
|
||||
}
|
||||
|
||||
.sh-badge-success {
|
||||
background: linear-gradient(135deg, rgba(34,197,94,0.2), rgba(22,163,74,0.2)) !important;
|
||||
color: #22c55e !important;
|
||||
border: 1px solid rgba(34,197,94,0.3) !important;
|
||||
}
|
||||
|
||||
.sh-badge-muted {
|
||||
background: rgba(100,116,139,0.2) !important;
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
|
||||
.sh-last-backup {
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@ -132,6 +132,31 @@
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.sh-btn-success {
|
||||
background: linear-gradient(135deg,#22c55e,#16a34a);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.sh-btn-danger {
|
||||
background: linear-gradient(135deg,#ef4444,#dc2626);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* Live indicator animations */
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; background: rgba(34, 197, 94, 0.3); }
|
||||
to { opacity: 1; background: transparent; }
|
||||
}
|
||||
|
||||
.sh-log-new {
|
||||
border-left: 3px solid #22c55e;
|
||||
}
|
||||
|
||||
.sh-logs-body {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr;
|
||||
|
||||
@ -13,11 +13,16 @@ Theme.init({ language: shLang });
|
||||
|
||||
return view.extend({
|
||||
statusData: {},
|
||||
scheduleData: {},
|
||||
|
||||
load: function() {
|
||||
return API.getSystemInfo().then(L.bind(function(info) {
|
||||
this.statusData = info || {};
|
||||
return info;
|
||||
return Promise.all([
|
||||
API.getSystemInfo(),
|
||||
API.getBackupSchedule()
|
||||
]).then(L.bind(function(results) {
|
||||
this.statusData = results[0] || {};
|
||||
this.scheduleData = results[1] || {};
|
||||
return results;
|
||||
}, this));
|
||||
},
|
||||
|
||||
@ -32,6 +37,7 @@ return view.extend({
|
||||
this.renderHero(),
|
||||
E('div', { 'class': 'sh-backup-grid' }, [
|
||||
this.renderBackupCard(),
|
||||
this.renderScheduleCard(),
|
||||
this.renderRestoreCard(),
|
||||
this.renderMaintenanceCard()
|
||||
])
|
||||
@ -97,6 +103,174 @@ return view.extend({
|
||||
]);
|
||||
},
|
||||
|
||||
renderScheduleCard: function() {
|
||||
var self = this;
|
||||
var schedule = this.scheduleData || {};
|
||||
var enabled = schedule.enabled || false;
|
||||
var frequency = schedule.frequency || 'weekly';
|
||||
var hour = schedule.hour || '03';
|
||||
var minute = schedule.minute || '00';
|
||||
var dayOfWeek = schedule.day_of_week || '0';
|
||||
var dayOfMonth = schedule.day_of_month || '1';
|
||||
|
||||
var dayNames = [
|
||||
_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'),
|
||||
_('Thursday'), _('Friday'), _('Saturday')
|
||||
];
|
||||
|
||||
var frequencySelect = E('select', {
|
||||
'id': 'schedule-frequency',
|
||||
'class': 'sh-select',
|
||||
'change': function() { self.updateScheduleVisibility(); }
|
||||
}, [
|
||||
E('option', { 'value': 'daily', 'selected': frequency === 'daily' ? 'selected' : null }, _('Daily')),
|
||||
E('option', { 'value': 'weekly', 'selected': frequency === 'weekly' ? 'selected' : null }, _('Weekly')),
|
||||
E('option', { 'value': 'monthly', 'selected': frequency === 'monthly' ? 'selected' : null }, _('Monthly'))
|
||||
]);
|
||||
|
||||
var hourSelect = E('select', { 'id': 'schedule-hour', 'class': 'sh-select sh-select-time' });
|
||||
for (var h = 0; h < 24; h++) {
|
||||
var hStr = (h < 10 ? '0' : '') + h;
|
||||
hourSelect.appendChild(E('option', { 'value': hStr, 'selected': hStr === hour ? 'selected' : null }, hStr));
|
||||
}
|
||||
|
||||
var minuteSelect = E('select', { 'id': 'schedule-minute', 'class': 'sh-select sh-select-time' });
|
||||
for (var m = 0; m < 60; m += 15) {
|
||||
var mStr = (m < 10 ? '0' : '') + m;
|
||||
minuteSelect.appendChild(E('option', { 'value': mStr, 'selected': mStr === minute ? 'selected' : null }, mStr));
|
||||
}
|
||||
|
||||
var dowSelect = E('select', { 'id': 'schedule-dow', 'class': 'sh-select' });
|
||||
for (var d = 0; d < 7; d++) {
|
||||
dowSelect.appendChild(E('option', { 'value': String(d), 'selected': String(d) === dayOfWeek ? 'selected' : null }, dayNames[d]));
|
||||
}
|
||||
|
||||
var domSelect = E('select', { 'id': 'schedule-dom', 'class': 'sh-select' });
|
||||
for (var day = 1; day <= 28; day++) {
|
||||
domSelect.appendChild(E('option', { 'value': String(day), 'selected': String(day) === dayOfMonth ? 'selected' : null }, String(day)));
|
||||
}
|
||||
|
||||
var statusText = enabled
|
||||
? (schedule.next_backup ? _('Next: ') + schedule.next_backup : _('Enabled'))
|
||||
: _('Disabled');
|
||||
|
||||
return E('section', { 'class': 'sh-card' }, [
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
E('div', { 'class': 'sh-card-title' }, [
|
||||
E('span', { 'class': 'sh-card-title-icon' }, '📅'),
|
||||
_('Scheduled Backups')
|
||||
]),
|
||||
E('span', {
|
||||
'class': 'sh-card-badge ' + (enabled ? 'sh-badge-success' : 'sh-badge-muted'),
|
||||
'id': 'schedule-status-badge'
|
||||
}, statusText)
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-body' }, [
|
||||
E('p', { 'class': 'sh-text-muted' }, _('Automatically create backups on a schedule. Backups are saved to /root/backups with auto-cleanup after 30 days.')),
|
||||
E('div', { 'class': 'sh-schedule-form' }, [
|
||||
E('label', { 'class': 'sh-toggle sh-toggle-main' }, [
|
||||
E('input', {
|
||||
'type': 'checkbox',
|
||||
'id': 'schedule-enabled',
|
||||
'checked': enabled ? 'checked' : null,
|
||||
'change': function() { self.updateScheduleVisibility(); }
|
||||
}),
|
||||
E('span', {}, _('Enable scheduled backups'))
|
||||
]),
|
||||
E('div', { 'class': 'sh-schedule-options', 'id': 'schedule-options', 'style': enabled ? '' : 'opacity: 0.5; pointer-events: none;' }, [
|
||||
E('div', { 'class': 'sh-form-row' }, [
|
||||
E('label', {}, _('Frequency')),
|
||||
frequencySelect
|
||||
]),
|
||||
E('div', { 'class': 'sh-form-row' }, [
|
||||
E('label', {}, _('Time')),
|
||||
E('div', { 'class': 'sh-time-picker' }, [
|
||||
hourSelect,
|
||||
E('span', {}, ':'),
|
||||
minuteSelect
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sh-form-row', 'id': 'dow-row', 'style': frequency === 'weekly' ? '' : 'display: none;' }, [
|
||||
E('label', {}, _('Day of week')),
|
||||
dowSelect
|
||||
]),
|
||||
E('div', { 'class': 'sh-form-row', 'id': 'dom-row', 'style': frequency === 'monthly' ? '' : 'display: none;' }, [
|
||||
E('label', {}, _('Day of month')),
|
||||
domSelect
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sh-action-row', 'style': 'margin-top: 16px;' }, [
|
||||
E('button', {
|
||||
'class': 'sh-btn sh-btn-primary',
|
||||
'type': 'button',
|
||||
'click': ui.createHandlerFn(this, 'saveSchedule')
|
||||
}, '💾 ' + _('Save Schedule'))
|
||||
]),
|
||||
schedule.last_backup ? E('p', { 'class': 'sh-text-muted sh-last-backup', 'style': 'margin-top: 12px; font-size: 13px;' },
|
||||
_('Last backup: ') + schedule.last_backup) : ''
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
updateScheduleVisibility: function() {
|
||||
var enabled = document.getElementById('schedule-enabled');
|
||||
var options = document.getElementById('schedule-options');
|
||||
var frequency = document.getElementById('schedule-frequency');
|
||||
var dowRow = document.getElementById('dow-row');
|
||||
var domRow = document.getElementById('dom-row');
|
||||
|
||||
if (enabled && options) {
|
||||
options.style.opacity = enabled.checked ? '1' : '0.5';
|
||||
options.style.pointerEvents = enabled.checked ? 'auto' : 'none';
|
||||
}
|
||||
|
||||
if (frequency && dowRow && domRow) {
|
||||
var freq = frequency.value;
|
||||
dowRow.style.display = freq === 'weekly' ? '' : 'none';
|
||||
domRow.style.display = freq === 'monthly' ? '' : 'none';
|
||||
}
|
||||
},
|
||||
|
||||
saveSchedule: function() {
|
||||
var enabled = document.getElementById('schedule-enabled');
|
||||
var frequency = document.getElementById('schedule-frequency');
|
||||
var hour = document.getElementById('schedule-hour');
|
||||
var minute = document.getElementById('schedule-minute');
|
||||
var dow = document.getElementById('schedule-dow');
|
||||
var dom = document.getElementById('schedule-dom');
|
||||
|
||||
var data = {
|
||||
enabled: enabled && enabled.checked ? 1 : 0,
|
||||
frequency: frequency ? frequency.value : 'weekly',
|
||||
hour: hour ? hour.value : '03',
|
||||
minute: minute ? minute.value : '00',
|
||||
day_of_week: dow ? dow.value : '0',
|
||||
day_of_month: dom ? dom.value : '1'
|
||||
};
|
||||
|
||||
ui.showModal(_('Saving schedule...'), [
|
||||
E('p', { 'class': 'spinning' }, _('Updating cron configuration...'))
|
||||
]);
|
||||
|
||||
return API.setBackupSchedule(data).then(function(result) {
|
||||
ui.hideModal();
|
||||
if (result && result.success) {
|
||||
ui.addNotification(null, E('p', {}, _('Backup schedule saved successfully')), 'info');
|
||||
var badge = document.getElementById('schedule-status-badge');
|
||||
if (badge) {
|
||||
badge.className = 'sh-card-badge ' + (data.enabled ? 'sh-badge-success' : 'sh-badge-muted');
|
||||
badge.textContent = data.enabled ? _('Enabled') : _('Disabled');
|
||||
}
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, (result && result.message) || _('Failed to save schedule')), 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
renderRestoreCard: function() {
|
||||
return E('section', { 'class': 'sh-card' }, [
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
|
||||
@ -197,6 +197,7 @@ return view.extend({
|
||||
renderComponentActions: function(component) {
|
||||
var self = this;
|
||||
var actions = [];
|
||||
var serviceName = component.service || component.id;
|
||||
|
||||
if (component.installed) {
|
||||
if (component.running) {
|
||||
@ -204,7 +205,7 @@ return view.extend({
|
||||
actions.push(
|
||||
E('button', {
|
||||
'class': 'sh-action-btn sh-btn-danger',
|
||||
'click': function() { self.handleComponentAction(component.id, 'stop'); }
|
||||
'click': function() { self.handleComponentAction(component, 'stop'); }
|
||||
}, [
|
||||
E('span', {}, '⏹️'),
|
||||
' Stop'
|
||||
@ -215,16 +216,16 @@ return view.extend({
|
||||
actions.push(
|
||||
E('button', {
|
||||
'class': 'sh-action-btn sh-btn-warning',
|
||||
'click': function() { self.handleComponentAction(component.id, 'restart'); }
|
||||
'click': function() { self.handleComponentAction(component, 'restart'); }
|
||||
}, [
|
||||
E('span', {}, '🔄'),
|
||||
' Restart'
|
||||
])
|
||||
);
|
||||
|
||||
// Dashboard button (if has dashboard)
|
||||
if (component.package && component.package.includes('dashboard')) {
|
||||
var dashboardUrl = '/cgi-bin/luci/admin/secubox/' + component.category + '/' + component.id;
|
||||
// Dashboard button for security/monitoring components
|
||||
if (component.category === 'security' || component.category === 'monitoring') {
|
||||
var dashboardUrl = L.url('admin/secubox/' + component.category + '/' + component.id);
|
||||
actions.push(
|
||||
E('a', {
|
||||
'class': 'sh-action-btn sh-btn-primary',
|
||||
@ -240,7 +241,7 @@ return view.extend({
|
||||
actions.push(
|
||||
E('button', {
|
||||
'class': 'sh-action-btn sh-btn-success',
|
||||
'click': function() { self.handleComponentAction(component.id, 'start'); }
|
||||
'click': function() { self.handleComponentAction(component, 'start'); }
|
||||
}, [
|
||||
E('span', {}, '▶️'),
|
||||
' Start'
|
||||
@ -248,12 +249,12 @@ return view.extend({
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Install button
|
||||
// Not installed - show package info
|
||||
actions.push(
|
||||
E('button', {
|
||||
'class': 'sh-action-btn sh-btn-secondary',
|
||||
'disabled': 'disabled',
|
||||
'title': 'Manual installation required'
|
||||
'title': 'Install via: opkg install ' + component.package
|
||||
}, [
|
||||
E('span', {}, '📥'),
|
||||
' Not Installed'
|
||||
@ -264,36 +265,43 @@ return view.extend({
|
||||
return actions;
|
||||
},
|
||||
|
||||
handleComponentAction: function(componentId, action) {
|
||||
handleComponentAction: function(component, action) {
|
||||
var self = this;
|
||||
var serviceName = component.service || component.id;
|
||||
var displayName = component.name || component.id;
|
||||
|
||||
ui.showModal(_('Component Action'), [
|
||||
E('p', {}, 'Performing ' + action + ' on ' + componentId + '...'),
|
||||
E('p', {}, _('Performing ') + action + _(' on ') + displayName + '...'),
|
||||
E('div', { 'class': 'spinning' })
|
||||
]);
|
||||
|
||||
// Call service action via system-hub API
|
||||
API.serviceAction(componentId, action).then(function(result) {
|
||||
// Call service action via system-hub API using service name
|
||||
API.serviceAction(serviceName, action).then(function(result) {
|
||||
ui.hideModal();
|
||||
|
||||
if (result && result.success) {
|
||||
ui.addNotification(null,
|
||||
E('p', {}, '✅ ' + componentId + ' ' + action + ' successful'),
|
||||
E('p', {}, '✅ ' + displayName + ' ' + action + ' ' + _('successful')),
|
||||
'success');
|
||||
|
||||
// Refresh components
|
||||
// Refresh components after a short delay
|
||||
setTimeout(function() {
|
||||
self.updateComponentsGrid();
|
||||
}, 2000);
|
||||
API.getComponents().then(function(data) {
|
||||
if (data && data.modules) {
|
||||
self.componentsData = data.modules;
|
||||
self.updateComponentsGrid();
|
||||
}
|
||||
});
|
||||
}, 1500);
|
||||
} else {
|
||||
ui.addNotification(null,
|
||||
E('p', {}, '❌ Failed to ' + action + ' ' + componentId),
|
||||
E('p', {}, '❌ ' + _('Failed to ') + action + ' ' + displayName + (result && result.message ? ': ' + result.message : '')),
|
||||
'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null,
|
||||
E('p', {}, '❌ Error: ' + (err.message || err)),
|
||||
E('p', {}, '❌ ' + _('Error: ') + (err.message || err)),
|
||||
'error');
|
||||
});
|
||||
},
|
||||
|
||||
@ -20,13 +20,16 @@ return view.extend({
|
||||
autoScroll: true,
|
||||
searchQuery: '',
|
||||
severityFilter: 'all',
|
||||
lastLogCount: 0,
|
||||
pollInterval: 2,
|
||||
|
||||
load: function() {
|
||||
return API.getLogs(this.lineCount, '');
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
this.logs = (data && data.logs) || [];
|
||||
this.logs = Array.isArray(data) ? data : (data && data.logs) || [];
|
||||
this.lastLogCount = this.logs.length;
|
||||
|
||||
var container = E('div', { 'class': 'sh-logs-view' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||||
@ -45,12 +48,17 @@ return view.extend({
|
||||
var self = this;
|
||||
poll.add(function() {
|
||||
if (!self.autoRefresh) return;
|
||||
self.updateLiveIndicator(true);
|
||||
return API.getLogs(self.lineCount, '').then(function(result) {
|
||||
self.logs = (result && result.logs) || [];
|
||||
var newLogs = Array.isArray(result) ? result : (result && result.logs) || [];
|
||||
var hasNewLogs = newLogs.length !== self.lastLogCount;
|
||||
self.logs = newLogs;
|
||||
self.lastLogCount = newLogs.length;
|
||||
self.updateStats();
|
||||
self.updateLogStream();
|
||||
self.updateLogStream(hasNewLogs);
|
||||
self.updateLiveIndicator(false);
|
||||
});
|
||||
}, 5);
|
||||
}, this.pollInterval);
|
||||
|
||||
return container;
|
||||
},
|
||||
@ -58,8 +66,18 @@ return view.extend({
|
||||
renderHero: function() {
|
||||
return E('section', { 'class': 'sh-logs-hero' }, [
|
||||
E('div', {}, [
|
||||
E('h1', {}, _('System Logs Live Stream')),
|
||||
E('p', {}, _('Follow kernel, service, and security events in real time'))
|
||||
E('h1', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
|
||||
_('System Logs'),
|
||||
E('span', {
|
||||
'id': 'sh-live-indicator',
|
||||
'class': 'sh-live-badge',
|
||||
'style': 'display: inline-flex; align-items: center; gap: 0.3em; font-size: 0.5em; padding: 0.3em 0.6em; background: #22c55e; color: #fff; border-radius: 4px; animation: pulse 2s infinite;'
|
||||
}, [
|
||||
E('span', { 'class': 'sh-live-dot', 'style': 'width: 8px; height: 8px; background: #fff; border-radius: 50%;' }),
|
||||
'LIVE'
|
||||
])
|
||||
]),
|
||||
E('p', {}, _('Real-time kernel, service, and security events'))
|
||||
]),
|
||||
E('div', { 'class': 'sh-log-stats', 'id': 'sh-log-stats' }, [
|
||||
this.createStat('sh-log-total', _('Lines'), this.logs.length),
|
||||
@ -92,6 +110,17 @@ return view.extend({
|
||||
})
|
||||
]),
|
||||
E('div', { 'class': 'sh-log-selectors' }, [
|
||||
E('button', {
|
||||
'id': 'sh-play-pause',
|
||||
'class': 'sh-btn ' + (this.autoRefresh ? 'sh-btn-danger' : 'sh-btn-success'),
|
||||
'type': 'button',
|
||||
'style': 'min-width: 100px; font-size: 1.1em;',
|
||||
'click': function(ev) {
|
||||
self.autoRefresh = !self.autoRefresh;
|
||||
self.updatePlayPauseButton();
|
||||
self.updateLiveBadge();
|
||||
}
|
||||
}, this.autoRefresh ? '⏸ ' + _('Pause') : '▶ ' + _('Play')),
|
||||
E('select', {
|
||||
'change': function(ev) {
|
||||
self.lineCount = parseInt(ev.target.value, 10);
|
||||
@ -103,13 +132,15 @@ return view.extend({
|
||||
E('option', { 'value': '500' }, '500 lines'),
|
||||
E('option', { 'value': '1000' }, '1000 lines')
|
||||
]),
|
||||
E('div', { 'class': 'sh-toggle-group' }, [
|
||||
this.renderToggle(_('Auto Refresh'), this.autoRefresh, function(enabled) {
|
||||
self.autoRefresh = enabled;
|
||||
E('label', { 'class': 'sh-toggle', 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
|
||||
E('input', {
|
||||
'type': 'checkbox',
|
||||
'checked': this.autoScroll ? 'checked' : null,
|
||||
'change': function(ev) {
|
||||
self.autoScroll = ev.target.checked;
|
||||
}
|
||||
}),
|
||||
this.renderToggle(_('Auto Scroll'), this.autoScroll, function(enabled) {
|
||||
self.autoScroll = enabled;
|
||||
})
|
||||
E('span', {}, _('Auto Scroll'))
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'sh-btn sh-btn-primary',
|
||||
@ -120,6 +151,40 @@ return view.extend({
|
||||
]);
|
||||
},
|
||||
|
||||
updatePlayPauseButton: function() {
|
||||
var btn = document.getElementById('sh-play-pause');
|
||||
if (btn) {
|
||||
btn.textContent = this.autoRefresh ? '⏸ ' + _('Pause') : '▶ ' + _('Play');
|
||||
btn.className = 'sh-btn ' + (this.autoRefresh ? 'sh-btn-danger' : 'sh-btn-success');
|
||||
}
|
||||
},
|
||||
|
||||
updateLiveBadge: function() {
|
||||
var badge = document.getElementById('sh-live-indicator');
|
||||
if (badge) {
|
||||
if (this.autoRefresh) {
|
||||
badge.style.background = '#22c55e';
|
||||
badge.style.animation = 'pulse 2s infinite';
|
||||
badge.innerHTML = '<span class="sh-live-dot" style="width: 8px; height: 8px; background: #fff; border-radius: 50%;"></span>LIVE';
|
||||
} else {
|
||||
badge.style.background = '#6b7280';
|
||||
badge.style.animation = 'none';
|
||||
badge.innerHTML = '⏸ PAUSED';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateLiveIndicator: function(fetching) {
|
||||
var badge = document.getElementById('sh-live-indicator');
|
||||
if (badge && this.autoRefresh) {
|
||||
if (fetching) {
|
||||
badge.style.background = '#f59e0b';
|
||||
} else {
|
||||
badge.style.background = '#22c55e';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
renderToggle: function(label, state, handler) {
|
||||
return E('label', { 'class': 'sh-toggle' }, [
|
||||
E('input', {
|
||||
@ -194,13 +259,18 @@ return view.extend({
|
||||
}, this);
|
||||
},
|
||||
|
||||
updateLogStream: function() {
|
||||
updateLogStream: function(hasNewLogs) {
|
||||
var container = document.getElementById('sh-log-stream');
|
||||
if (!container) return;
|
||||
var filtered = this.getFilteredLogs();
|
||||
var totalLines = filtered.length;
|
||||
var frag = filtered.map(function(line, idx) {
|
||||
var severity = this.detectSeverity(line);
|
||||
return E('div', { 'class': 'sh-log-line ' + severity }, [
|
||||
var isNew = hasNewLogs && idx >= totalLines - 5;
|
||||
return E('div', {
|
||||
'class': 'sh-log-line ' + severity + (isNew ? ' sh-log-new' : ''),
|
||||
'style': isNew ? 'animation: fadeIn 0.5s ease;' : ''
|
||||
}, [
|
||||
E('span', { 'class': 'sh-log-index' }, idx + 1),
|
||||
E('span', { 'class': 'sh-log-message' }, line)
|
||||
]);
|
||||
@ -252,7 +322,7 @@ return view.extend({
|
||||
]);
|
||||
return API.getLogs(this.lineCount, '').then(function(result) {
|
||||
ui.hideModal();
|
||||
self.logs = (result && result.logs) || [];
|
||||
self.logs = Array.isArray(result) ? result : (result && result.logs) || [];
|
||||
self.updateStats();
|
||||
self.updateLogStream();
|
||||
}).catch(function(err) {
|
||||
|
||||
@ -292,8 +292,8 @@ return view.extend({
|
||||
},
|
||||
|
||||
getStatusLabel: function(state) {
|
||||
if (state === undefined || state === null) return _('Unknown');
|
||||
return state ? _('Healthy') : _('Attention');
|
||||
if (state === undefined || state === null) return '❓';
|
||||
return state ? '✅' : '⚠️';
|
||||
},
|
||||
|
||||
getScoreLabel: function(score) {
|
||||
|
||||
@ -117,6 +117,60 @@ get_system_info() {
|
||||
local localtime=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")
|
||||
json_add_string "local_time" "$localtime"
|
||||
|
||||
# Load averages
|
||||
local load=$(cat /proc/loadavg 2>/dev/null || echo "0 0 0")
|
||||
local load1=$(echo $load | awk '{print $1}')
|
||||
local load5=$(echo $load | awk '{print $2}')
|
||||
local load15=$(echo $load | awk '{print $3}')
|
||||
json_add_array "load"
|
||||
json_add_string "" "$load1"
|
||||
json_add_string "" "$load5"
|
||||
json_add_string "" "$load15"
|
||||
json_close_array
|
||||
|
||||
# Quick Status Indicators
|
||||
json_add_object "status"
|
||||
|
||||
# Internet connectivity (ping 8.8.8.8)
|
||||
local internet_ok=0
|
||||
if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then
|
||||
internet_ok=1
|
||||
fi
|
||||
json_add_boolean "internet" "$internet_ok"
|
||||
|
||||
# DNS resolution (resolve cloudflare.com)
|
||||
local dns_ok=0
|
||||
if nslookup cloudflare.com >/dev/null 2>&1 || host cloudflare.com >/dev/null 2>&1; then
|
||||
dns_ok=1
|
||||
fi
|
||||
json_add_boolean "dns" "$dns_ok"
|
||||
|
||||
# NTP sync status
|
||||
local ntp_ok=0
|
||||
if [ -f /var/state/ntpd ] || pgrep -x ntpd >/dev/null 2>&1 || pgrep -x chronyd >/dev/null 2>&1; then
|
||||
# Check if time seems reasonable (after year 2020)
|
||||
local year=$(date +%Y)
|
||||
[ "$year" -ge 2020 ] && ntp_ok=1
|
||||
fi
|
||||
json_add_boolean "ntp" "$ntp_ok"
|
||||
|
||||
# Firewall status (check if nftables or iptables rules exist)
|
||||
local firewall_ok=0
|
||||
local firewall_rules=0
|
||||
# Count nftables rules (chains with rules)
|
||||
firewall_rules=$(nft list ruleset 2>/dev/null | grep -cE "^\s+(type|counter|accept|drop|reject|jump|goto)" || echo 0)
|
||||
if [ "$firewall_rules" -gt 0 ]; then
|
||||
firewall_ok=1
|
||||
else
|
||||
# Fallback to iptables
|
||||
firewall_rules=$(iptables -L -n 2>/dev/null | grep -cE "^(ACCEPT|DROP|REJECT)" || echo 0)
|
||||
[ "$firewall_rules" -gt 0 ] && firewall_ok=1
|
||||
fi
|
||||
json_add_boolean "firewall" "$firewall_ok"
|
||||
json_add_int "firewall_rules" "$firewall_rules"
|
||||
|
||||
json_close_object
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
@ -577,6 +631,147 @@ reboot_system() {
|
||||
( sleep 3 && reboot ) &
|
||||
}
|
||||
|
||||
# Get backup schedule from crontab
|
||||
get_backup_schedule() {
|
||||
json_init
|
||||
|
||||
local cron_line=""
|
||||
local enabled=0
|
||||
local frequency="weekly"
|
||||
local hour="03"
|
||||
local minute="00"
|
||||
local day_of_week="0"
|
||||
local day_of_month="1"
|
||||
local last_backup=""
|
||||
local next_backup=""
|
||||
|
||||
# Check for existing backup cron job
|
||||
if [ -f /etc/crontabs/root ]; then
|
||||
cron_line=$(grep "sysupgrade -b" /etc/crontabs/root 2>/dev/null | head -n 1)
|
||||
fi
|
||||
|
||||
if [ -n "$cron_line" ]; then
|
||||
enabled=1
|
||||
# Parse cron schedule: minute hour day_of_month month day_of_week command
|
||||
minute=$(echo "$cron_line" | awk '{print $1}')
|
||||
hour=$(echo "$cron_line" | awk '{print $2}')
|
||||
local dom=$(echo "$cron_line" | awk '{print $3}')
|
||||
local dow=$(echo "$cron_line" | awk '{print $5}')
|
||||
|
||||
# Determine frequency from cron pattern
|
||||
if [ "$dom" != "*" ] && [ "$dow" = "*" ]; then
|
||||
frequency="monthly"
|
||||
day_of_month="$dom"
|
||||
elif [ "$dow" != "*" ]; then
|
||||
frequency="weekly"
|
||||
day_of_week="$dow"
|
||||
else
|
||||
frequency="daily"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Find last backup file
|
||||
local last_file=$(ls -t /tmp/backup-*.tar.gz 2>/dev/null | head -n 1)
|
||||
if [ -n "$last_file" ] && [ -f "$last_file" ]; then
|
||||
last_backup=$(date -r "$last_file" '+%Y-%m-%d %H:%M' 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
# Calculate next backup time (simplified)
|
||||
if [ "$enabled" = "1" ]; then
|
||||
local now_hour=$(date +%H)
|
||||
local now_min=$(date +%M)
|
||||
local target_time="${hour}:${minute}"
|
||||
case "$frequency" in
|
||||
daily)
|
||||
if [ "$now_hour$now_min" -lt "${hour}${minute}" ]; then
|
||||
next_backup="Today at $target_time"
|
||||
else
|
||||
next_backup="Tomorrow at $target_time"
|
||||
fi
|
||||
;;
|
||||
weekly)
|
||||
local dow_names="Sun Mon Tue Wed Thu Fri Sat"
|
||||
local dow_name=$(echo "$dow_names" | cut -d' ' -f$((day_of_week + 1)))
|
||||
next_backup="$dow_name at $target_time"
|
||||
;;
|
||||
monthly)
|
||||
next_backup="Day $day_of_month at $target_time"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_add_string "frequency" "$frequency"
|
||||
json_add_string "hour" "$hour"
|
||||
json_add_string "minute" "$minute"
|
||||
json_add_string "day_of_week" "$day_of_week"
|
||||
json_add_string "day_of_month" "$day_of_month"
|
||||
json_add_string "last_backup" "$last_backup"
|
||||
json_add_string "next_backup" "$next_backup"
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Set backup schedule in crontab
|
||||
set_backup_schedule() {
|
||||
read -r input
|
||||
json_load "$input"
|
||||
|
||||
local enabled frequency hour minute day_of_week day_of_month
|
||||
json_get_var enabled enabled "0"
|
||||
json_get_var frequency frequency "weekly"
|
||||
json_get_var hour hour "03"
|
||||
json_get_var minute minute "00"
|
||||
json_get_var day_of_week day_of_week "0"
|
||||
json_get_var day_of_month day_of_month "1"
|
||||
json_cleanup
|
||||
|
||||
# Validate inputs
|
||||
hour=$(printf "%02d" "$((${hour:-3} % 24))")
|
||||
minute=$(printf "%02d" "$((${minute:-0} % 60))")
|
||||
day_of_week=$(printf "%d" "$((${day_of_week:-0} % 7))")
|
||||
day_of_month=$(printf "%d" "$((${day_of_month:-1}))")
|
||||
[ "$day_of_month" -lt 1 ] && day_of_month=1
|
||||
[ "$day_of_month" -gt 28 ] && day_of_month=28
|
||||
|
||||
# Backup destination
|
||||
local backup_dir="/root/backups"
|
||||
local backup_cmd="mkdir -p $backup_dir && sysupgrade -b $backup_dir/backup-\$(date +%Y%m%d-%H%M%S).tar.gz && find $backup_dir -name 'backup-*.tar.gz' -mtime +30 -delete"
|
||||
|
||||
# Remove existing backup cron entries
|
||||
if [ -f /etc/crontabs/root ]; then
|
||||
grep -v "sysupgrade -b" /etc/crontabs/root > /tmp/crontab.tmp 2>/dev/null || touch /tmp/crontab.tmp
|
||||
mv /tmp/crontab.tmp /etc/crontabs/root
|
||||
else
|
||||
touch /etc/crontabs/root
|
||||
fi
|
||||
|
||||
# Add new cron entry if enabled
|
||||
if [ "$enabled" = "1" ]; then
|
||||
local cron_schedule=""
|
||||
case "$frequency" in
|
||||
daily)
|
||||
cron_schedule="$minute $hour * * *"
|
||||
;;
|
||||
weekly)
|
||||
cron_schedule="$minute $hour * * $day_of_week"
|
||||
;;
|
||||
monthly)
|
||||
cron_schedule="$minute $hour $day_of_month * *"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$cron_schedule $backup_cmd" >> /etc/crontabs/root
|
||||
fi
|
||||
|
||||
# Reload cron
|
||||
/etc/init.d/cron restart >/dev/null 2>&1 || true
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Backup schedule updated"
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Get storage details
|
||||
get_storage() {
|
||||
json_init
|
||||
@ -1404,38 +1599,217 @@ save_settings() {
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Get components (leverages secubox module detection)
|
||||
get_components() {
|
||||
# Call secubox backend to get apps list
|
||||
local apps_result=$(ubus call luci.secubox list_apps 2>/dev/null)
|
||||
# Check if a package is installed (supports wildcards)
|
||||
is_package_installed() {
|
||||
local pkg="$1"
|
||||
[ -z "$pkg" ] && return 1
|
||||
# Check exact match first
|
||||
opkg list-installed 2>/dev/null | grep -q "^${pkg} " && return 0
|
||||
# Check if it's a LuCI app package
|
||||
opkg list-installed 2>/dev/null | grep -q "^luci-app-${pkg} " && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
if [ -n "$apps_result" ]; then
|
||||
# Transform apps to components format
|
||||
echo "$apps_result" | jq '{
|
||||
modules: [
|
||||
.apps[] | {
|
||||
id: .id,
|
||||
name: .name,
|
||||
version: (.pkg_version // .version // "1.0.0"),
|
||||
category: (.category // "system"),
|
||||
description: (.description // "No description"),
|
||||
icon: (.icon // "📦"),
|
||||
package: (.packages.required[0] // ""),
|
||||
installed: false,
|
||||
running: false,
|
||||
color: (
|
||||
if .category == "security" then "#ef4444"
|
||||
elif .category == "monitoring" then "#10b981"
|
||||
elif .category == "network" then "#3b82f6"
|
||||
else "#64748b"
|
||||
end
|
||||
)
|
||||
}
|
||||
]
|
||||
}'
|
||||
# Get installed package info (name and version) for a component
|
||||
get_installed_package_info() {
|
||||
local pkg="$1"
|
||||
[ -z "$pkg" ] && return
|
||||
# Try exact match
|
||||
local info=$(opkg list-installed "$pkg" 2>/dev/null | head -n1)
|
||||
if [ -z "$info" ]; then
|
||||
# Try luci-app- prefix
|
||||
info=$(opkg list-installed "luci-app-$pkg" 2>/dev/null | head -n1)
|
||||
fi
|
||||
echo "$info"
|
||||
}
|
||||
|
||||
# Check if a service is running
|
||||
is_service_running() {
|
||||
local svc="$1"
|
||||
[ -z "$svc" ] && return 1
|
||||
|
||||
# Check init script
|
||||
if [ -x "/etc/init.d/$svc" ]; then
|
||||
/etc/init.d/"$svc" running >/dev/null 2>&1 && return 0
|
||||
fi
|
||||
|
||||
# Fallback: check process
|
||||
pgrep -x "$svc" >/dev/null 2>&1 && return 0
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Get package version
|
||||
get_package_version() {
|
||||
local pkg="$1"
|
||||
[ -z "$pkg" ] && echo "" && return
|
||||
local ver=$(opkg list-installed "$pkg" 2>/dev/null | awk '{print $3}' | head -n1)
|
||||
echo "${ver:-}"
|
||||
}
|
||||
|
||||
# Add a single component to the JSON output
|
||||
add_component() {
|
||||
local id="$1" name="$2" category="$3" icon="$4" package="$5" service="$6" description="$7" color="$8"
|
||||
|
||||
local installed=0
|
||||
local running=0
|
||||
local version=""
|
||||
|
||||
if is_package_installed "$package"; then
|
||||
installed=1
|
||||
version=$(get_package_version "$package")
|
||||
fi
|
||||
|
||||
if [ "$installed" = "1" ] && is_service_running "$service"; then
|
||||
running=1
|
||||
fi
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "id" "$id"
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "category" "$category"
|
||||
json_add_string "icon" "$icon"
|
||||
json_add_string "package" "$package"
|
||||
json_add_string "service" "$service"
|
||||
json_add_string "description" "$description"
|
||||
json_add_string "color" "$color"
|
||||
json_add_string "version" "$version"
|
||||
json_add_boolean "installed" "$installed"
|
||||
json_add_boolean "running" "$running"
|
||||
json_close_object
|
||||
}
|
||||
|
||||
# Get components with real installation/running status
|
||||
get_components() {
|
||||
# First try to get apps from secubox backend
|
||||
local apps_json=$(ubus call luci.secubox list_apps 2>/dev/null)
|
||||
|
||||
if [ -n "$apps_json" ]; then
|
||||
json_init
|
||||
json_add_array "modules"
|
||||
|
||||
# Parse using jsonfilter - get all app IDs first
|
||||
local app_ids=$(echo "$apps_json" | jsonfilter -e '@.apps[*].id' 2>/dev/null)
|
||||
local i=0
|
||||
|
||||
for id in $app_ids; do
|
||||
[ -z "$id" ] && continue
|
||||
|
||||
local name=$(echo "$apps_json" | jsonfilter -e "@.apps[$i].name" 2>/dev/null)
|
||||
local category=$(echo "$apps_json" | jsonfilter -e "@.apps[$i].category" 2>/dev/null)
|
||||
local state=$(echo "$apps_json" | jsonfilter -e "@.apps[$i].state" 2>/dev/null)
|
||||
local version=$(echo "$apps_json" | jsonfilter -e "@.apps[$i].version" 2>/dev/null)
|
||||
local description=$(echo "$apps_json" | jsonfilter -e "@.apps[$i].description" 2>/dev/null)
|
||||
|
||||
[ -z "$name" ] && name="$id"
|
||||
[ -z "$category" ] && category="system"
|
||||
[ -z "$state" ] && state="missing"
|
||||
[ -z "$description" ] && description="No description"
|
||||
|
||||
local installed=0
|
||||
local running=0
|
||||
local color="#64748b"
|
||||
local icon="📦"
|
||||
local service="$id"
|
||||
|
||||
# Determine if installed based on state from secubox
|
||||
case "$state" in
|
||||
installed|partial) installed=1 ;;
|
||||
esac
|
||||
|
||||
# Determine color and icon based on category
|
||||
case "$category" in
|
||||
security) color="#ef4444"; icon="🛡️" ;;
|
||||
monitoring) color="#10b981"; icon="📊" ;;
|
||||
network) color="#3b82f6"; icon="🌐" ;;
|
||||
system) color="#64748b"; icon="⚙️" ;;
|
||||
esac
|
||||
|
||||
# Override icon based on app ID
|
||||
case "$id" in
|
||||
crowdsec*) icon="🛡️" ;;
|
||||
auth-guardian) icon="🔐" ;;
|
||||
client-guardian) icon="👥" ;;
|
||||
key-storage*|ksm*) icon="🔑" ;;
|
||||
vaultwarden) icon="🔒" ;;
|
||||
wireguard*) icon="🔒" ;;
|
||||
bandwidth*|nlbwmon) icon="📊" ;;
|
||||
netdata*) icon="📉" ;;
|
||||
esac
|
||||
|
||||
# Check if service is running
|
||||
if [ "$installed" = "1" ]; then
|
||||
# Try direct service name
|
||||
if [ -x "/etc/init.d/$id" ]; then
|
||||
/etc/init.d/"$id" running >/dev/null 2>&1 && running=1
|
||||
fi
|
||||
# Try base service name (e.g., crowdsec-dashboard -> crowdsec)
|
||||
if [ "$running" = "0" ]; then
|
||||
local base_svc=$(echo "$id" | sed 's/-dashboard$//' | sed 's/-guardian$//' | sed 's/-manager$//')
|
||||
if [ -x "/etc/init.d/$base_svc" ]; then
|
||||
/etc/init.d/"$base_svc" running >/dev/null 2>&1 && running=1
|
||||
service="$base_svc"
|
||||
fi
|
||||
fi
|
||||
# Fallback: check process
|
||||
if [ "$running" = "0" ]; then
|
||||
pgrep -f "$id" >/dev/null 2>&1 && running=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get version from opkg if not provided
|
||||
if [ -z "$version" ] && [ "$installed" = "1" ]; then
|
||||
local luci_pkg="luci-app-${id}"
|
||||
version=$(opkg list-installed "$luci_pkg" 2>/dev/null | awk '{print $3}' | head -n1)
|
||||
fi
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "id" "$id"
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "category" "$category"
|
||||
json_add_string "icon" "$icon"
|
||||
json_add_string "package" "luci-app-$id"
|
||||
json_add_string "service" "$service"
|
||||
json_add_string "description" "$description"
|
||||
json_add_string "color" "$color"
|
||||
json_add_string "version" "$version"
|
||||
json_add_string "state" "$state"
|
||||
json_add_boolean "installed" "$installed"
|
||||
json_add_boolean "running" "$running"
|
||||
json_close_object
|
||||
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
# Also add core system components
|
||||
add_component "firewall4" "Firewall" "system" "🧱" "firewall4" "firewall" "nftables-based firewall" "#64748b"
|
||||
add_component "dnsmasq" "DNSMasq" "system" "🔍" "dnsmasq-full" "dnsmasq" "DNS and DHCP server" "#64748b"
|
||||
add_component "dropbear" "SSH Server" "system" "🔑" "dropbear" "dropbear" "Lightweight SSH server" "#64748b"
|
||||
add_component "uhttpd" "uHTTPd" "system" "🌐" "uhttpd" "uhttpd" "Lightweight HTTP server" "#64748b"
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
else
|
||||
# Fallback if secubox is not available
|
||||
echo '{"modules":[]}'
|
||||
# Fallback: use hardcoded list with real status checks
|
||||
json_init
|
||||
json_add_array "modules"
|
||||
|
||||
add_component "crowdsec" "CrowdSec" "security" "🛡️" "crowdsec" "crowdsec" "Collaborative security engine" "#ef4444"
|
||||
add_component "crowdsec-firewall-bouncer" "CrowdSec Bouncer" "security" "🔥" "crowdsec-firewall-bouncer-nftables" "crowdsec-firewall-bouncer" "Firewall bouncer" "#ef4444"
|
||||
add_component "adguardhome" "AdGuard Home" "security" "🚫" "adguardhome" "AdGuardHome" "Ad and tracker blocking" "#22c55e"
|
||||
add_component "banip" "BanIP" "security" "🚷" "banip" "banip" "IP blocking service" "#ef4444"
|
||||
add_component "wireguard" "WireGuard" "network" "🔒" "wireguard-tools" "wg-quick@wg0" "Modern VPN tunnel" "#3b82f6"
|
||||
add_component "sqm" "SQM QoS" "network" "⚡" "sqm-scripts" "sqm" "Smart Queue Management" "#3b82f6"
|
||||
add_component "mwan3" "Multi-WAN" "network" "🔀" "mwan3" "mwan3" "Multi-WAN failover" "#3b82f6"
|
||||
add_component "nlbwmon" "Bandwidth Monitor" "monitoring" "📊" "nlbwmon" "nlbwmon" "Bandwidth monitoring" "#10b981"
|
||||
add_component "vnstat2" "Traffic Stats" "monitoring" "📈" "vnstat2" "vnstatd" "Traffic statistics" "#10b981"
|
||||
add_component "firewall4" "Firewall" "system" "🧱" "firewall4" "firewall" "nftables firewall" "#64748b"
|
||||
add_component "dnsmasq" "DNSMasq" "system" "🔍" "dnsmasq-full" "dnsmasq" "DNS/DHCP server" "#64748b"
|
||||
add_component "dropbear" "SSH Server" "system" "🔑" "dropbear" "dropbear" "SSH server" "#64748b"
|
||||
add_component "uhttpd" "uHTTPd" "system" "🌐" "uhttpd" "uhttpd" "HTTP server" "#64748b"
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
fi
|
||||
}
|
||||
|
||||
@ -1478,6 +1852,15 @@ case "$1" in
|
||||
"get_logs": { "lines": 100, "filter": "" },
|
||||
"backup_config": {},
|
||||
"restore_config": { "file_name": "string", "data": "string" },
|
||||
"get_backup_schedule": {},
|
||||
"set_backup_schedule": {
|
||||
"enabled": 1,
|
||||
"frequency": "weekly",
|
||||
"hour": "03",
|
||||
"minute": "00",
|
||||
"day_of_week": "0",
|
||||
"day_of_month": "1"
|
||||
},
|
||||
"reboot": {},
|
||||
"get_storage": {},
|
||||
"get_settings": {},
|
||||
@ -1539,6 +1922,8 @@ EOF
|
||||
get_logs) get_logs ;;
|
||||
backup_config) backup_config ;;
|
||||
restore_config) restore_config ;;
|
||||
get_backup_schedule) get_backup_schedule ;;
|
||||
set_backup_schedule) set_backup_schedule ;;
|
||||
reboot) reboot_system ;;
|
||||
get_storage) get_storage ;;
|
||||
get_settings) get_settings ;;
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"get_logs",
|
||||
"get_storage",
|
||||
"get_settings",
|
||||
"get_backup_schedule",
|
||||
"list_diagnostics",
|
||||
"list_diagnostic_profiles",
|
||||
"get_diagnostic_profile",
|
||||
@ -38,6 +39,7 @@
|
||||
"service_action",
|
||||
"backup_config",
|
||||
"restore_config",
|
||||
"set_backup_schedule",
|
||||
"reboot",
|
||||
"save_settings",
|
||||
"collect_diagnostics",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user