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: {}
|
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({
|
var callReboot = rpc.declare({
|
||||||
object: 'luci.system-hub',
|
object: 'luci.system-hub',
|
||||||
method: 'reboot',
|
method: 'reboot',
|
||||||
@ -214,6 +227,10 @@ return baseclass.extend({
|
|||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getBackupSchedule: callGetBackupSchedule,
|
||||||
|
setBackupSchedule: function(data) {
|
||||||
|
return callSetBackupSchedule(data);
|
||||||
|
},
|
||||||
reboot: callReboot,
|
reboot: callReboot,
|
||||||
getStorage: callGetStorage,
|
getStorage: callGetStorage,
|
||||||
getSettings: callGetSettings,
|
getSettings: callGetSettings,
|
||||||
|
|||||||
@ -98,3 +98,86 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
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;
|
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 {
|
.sh-logs-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 3fr 1fr;
|
grid-template-columns: 3fr 1fr;
|
||||||
|
|||||||
@ -13,11 +13,16 @@ Theme.init({ language: shLang });
|
|||||||
|
|
||||||
return view.extend({
|
return view.extend({
|
||||||
statusData: {},
|
statusData: {},
|
||||||
|
scheduleData: {},
|
||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
return API.getSystemInfo().then(L.bind(function(info) {
|
return Promise.all([
|
||||||
this.statusData = info || {};
|
API.getSystemInfo(),
|
||||||
return info;
|
API.getBackupSchedule()
|
||||||
|
]).then(L.bind(function(results) {
|
||||||
|
this.statusData = results[0] || {};
|
||||||
|
this.scheduleData = results[1] || {};
|
||||||
|
return results;
|
||||||
}, this));
|
}, this));
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -32,6 +37,7 @@ return view.extend({
|
|||||||
this.renderHero(),
|
this.renderHero(),
|
||||||
E('div', { 'class': 'sh-backup-grid' }, [
|
E('div', { 'class': 'sh-backup-grid' }, [
|
||||||
this.renderBackupCard(),
|
this.renderBackupCard(),
|
||||||
|
this.renderScheduleCard(),
|
||||||
this.renderRestoreCard(),
|
this.renderRestoreCard(),
|
||||||
this.renderMaintenanceCard()
|
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() {
|
renderRestoreCard: function() {
|
||||||
return E('section', { 'class': 'sh-card' }, [
|
return E('section', { 'class': 'sh-card' }, [
|
||||||
E('div', { 'class': 'sh-card-header' }, [
|
E('div', { 'class': 'sh-card-header' }, [
|
||||||
|
|||||||
@ -197,6 +197,7 @@ return view.extend({
|
|||||||
renderComponentActions: function(component) {
|
renderComponentActions: function(component) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var actions = [];
|
var actions = [];
|
||||||
|
var serviceName = component.service || component.id;
|
||||||
|
|
||||||
if (component.installed) {
|
if (component.installed) {
|
||||||
if (component.running) {
|
if (component.running) {
|
||||||
@ -204,7 +205,7 @@ return view.extend({
|
|||||||
actions.push(
|
actions.push(
|
||||||
E('button', {
|
E('button', {
|
||||||
'class': 'sh-action-btn sh-btn-danger',
|
'class': 'sh-action-btn sh-btn-danger',
|
||||||
'click': function() { self.handleComponentAction(component.id, 'stop'); }
|
'click': function() { self.handleComponentAction(component, 'stop'); }
|
||||||
}, [
|
}, [
|
||||||
E('span', {}, '⏹️'),
|
E('span', {}, '⏹️'),
|
||||||
' Stop'
|
' Stop'
|
||||||
@ -215,16 +216,16 @@ return view.extend({
|
|||||||
actions.push(
|
actions.push(
|
||||||
E('button', {
|
E('button', {
|
||||||
'class': 'sh-action-btn sh-btn-warning',
|
'class': 'sh-action-btn sh-btn-warning',
|
||||||
'click': function() { self.handleComponentAction(component.id, 'restart'); }
|
'click': function() { self.handleComponentAction(component, 'restart'); }
|
||||||
}, [
|
}, [
|
||||||
E('span', {}, '🔄'),
|
E('span', {}, '🔄'),
|
||||||
' Restart'
|
' Restart'
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
// Dashboard button (if has dashboard)
|
// Dashboard button for security/monitoring components
|
||||||
if (component.package && component.package.includes('dashboard')) {
|
if (component.category === 'security' || component.category === 'monitoring') {
|
||||||
var dashboardUrl = '/cgi-bin/luci/admin/secubox/' + component.category + '/' + component.id;
|
var dashboardUrl = L.url('admin/secubox/' + component.category + '/' + component.id);
|
||||||
actions.push(
|
actions.push(
|
||||||
E('a', {
|
E('a', {
|
||||||
'class': 'sh-action-btn sh-btn-primary',
|
'class': 'sh-action-btn sh-btn-primary',
|
||||||
@ -240,7 +241,7 @@ return view.extend({
|
|||||||
actions.push(
|
actions.push(
|
||||||
E('button', {
|
E('button', {
|
||||||
'class': 'sh-action-btn sh-btn-success',
|
'class': 'sh-action-btn sh-btn-success',
|
||||||
'click': function() { self.handleComponentAction(component.id, 'start'); }
|
'click': function() { self.handleComponentAction(component, 'start'); }
|
||||||
}, [
|
}, [
|
||||||
E('span', {}, '▶️'),
|
E('span', {}, '▶️'),
|
||||||
' Start'
|
' Start'
|
||||||
@ -248,12 +249,12 @@ return view.extend({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Install button
|
// Not installed - show package info
|
||||||
actions.push(
|
actions.push(
|
||||||
E('button', {
|
E('button', {
|
||||||
'class': 'sh-action-btn sh-btn-secondary',
|
'class': 'sh-action-btn sh-btn-secondary',
|
||||||
'disabled': 'disabled',
|
'disabled': 'disabled',
|
||||||
'title': 'Manual installation required'
|
'title': 'Install via: opkg install ' + component.package
|
||||||
}, [
|
}, [
|
||||||
E('span', {}, '📥'),
|
E('span', {}, '📥'),
|
||||||
' Not Installed'
|
' Not Installed'
|
||||||
@ -264,36 +265,43 @@ return view.extend({
|
|||||||
return actions;
|
return actions;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleComponentAction: function(componentId, action) {
|
handleComponentAction: function(component, action) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
var serviceName = component.service || component.id;
|
||||||
|
var displayName = component.name || component.id;
|
||||||
|
|
||||||
ui.showModal(_('Component Action'), [
|
ui.showModal(_('Component Action'), [
|
||||||
E('p', {}, 'Performing ' + action + ' on ' + componentId + '...'),
|
E('p', {}, _('Performing ') + action + _(' on ') + displayName + '...'),
|
||||||
E('div', { 'class': 'spinning' })
|
E('div', { 'class': 'spinning' })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Call service action via system-hub API
|
// Call service action via system-hub API using service name
|
||||||
API.serviceAction(componentId, action).then(function(result) {
|
API.serviceAction(serviceName, action).then(function(result) {
|
||||||
ui.hideModal();
|
ui.hideModal();
|
||||||
|
|
||||||
if (result && result.success) {
|
if (result && result.success) {
|
||||||
ui.addNotification(null,
|
ui.addNotification(null,
|
||||||
E('p', {}, '✅ ' + componentId + ' ' + action + ' successful'),
|
E('p', {}, '✅ ' + displayName + ' ' + action + ' ' + _('successful')),
|
||||||
'success');
|
'success');
|
||||||
|
|
||||||
// Refresh components
|
// Refresh components after a short delay
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
self.updateComponentsGrid();
|
API.getComponents().then(function(data) {
|
||||||
}, 2000);
|
if (data && data.modules) {
|
||||||
|
self.componentsData = data.modules;
|
||||||
|
self.updateComponentsGrid();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 1500);
|
||||||
} else {
|
} else {
|
||||||
ui.addNotification(null,
|
ui.addNotification(null,
|
||||||
E('p', {}, '❌ Failed to ' + action + ' ' + componentId),
|
E('p', {}, '❌ ' + _('Failed to ') + action + ' ' + displayName + (result && result.message ? ': ' + result.message : '')),
|
||||||
'error');
|
'error');
|
||||||
}
|
}
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
ui.hideModal();
|
ui.hideModal();
|
||||||
ui.addNotification(null,
|
ui.addNotification(null,
|
||||||
E('p', {}, '❌ Error: ' + (err.message || err)),
|
E('p', {}, '❌ ' + _('Error: ') + (err.message || err)),
|
||||||
'error');
|
'error');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,13 +20,16 @@ return view.extend({
|
|||||||
autoScroll: true,
|
autoScroll: true,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
severityFilter: 'all',
|
severityFilter: 'all',
|
||||||
|
lastLogCount: 0,
|
||||||
|
pollInterval: 2,
|
||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
return API.getLogs(this.lineCount, '');
|
return API.getLogs(this.lineCount, '');
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
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' }, [
|
var container = E('div', { 'class': 'sh-logs-view' }, [
|
||||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||||||
@ -45,12 +48,17 @@ return view.extend({
|
|||||||
var self = this;
|
var self = this;
|
||||||
poll.add(function() {
|
poll.add(function() {
|
||||||
if (!self.autoRefresh) return;
|
if (!self.autoRefresh) return;
|
||||||
|
self.updateLiveIndicator(true);
|
||||||
return API.getLogs(self.lineCount, '').then(function(result) {
|
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.updateStats();
|
||||||
self.updateLogStream();
|
self.updateLogStream(hasNewLogs);
|
||||||
|
self.updateLiveIndicator(false);
|
||||||
});
|
});
|
||||||
}, 5);
|
}, this.pollInterval);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
},
|
},
|
||||||
@ -58,8 +66,18 @@ return view.extend({
|
|||||||
renderHero: function() {
|
renderHero: function() {
|
||||||
return E('section', { 'class': 'sh-logs-hero' }, [
|
return E('section', { 'class': 'sh-logs-hero' }, [
|
||||||
E('div', {}, [
|
E('div', {}, [
|
||||||
E('h1', {}, _('System Logs Live Stream')),
|
E('h1', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
|
||||||
E('p', {}, _('Follow kernel, service, and security events in real time'))
|
_('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' }, [
|
E('div', { 'class': 'sh-log-stats', 'id': 'sh-log-stats' }, [
|
||||||
this.createStat('sh-log-total', _('Lines'), this.logs.length),
|
this.createStat('sh-log-total', _('Lines'), this.logs.length),
|
||||||
@ -92,6 +110,17 @@ return view.extend({
|
|||||||
})
|
})
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'sh-log-selectors' }, [
|
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', {
|
E('select', {
|
||||||
'change': function(ev) {
|
'change': function(ev) {
|
||||||
self.lineCount = parseInt(ev.target.value, 10);
|
self.lineCount = parseInt(ev.target.value, 10);
|
||||||
@ -103,13 +132,15 @@ return view.extend({
|
|||||||
E('option', { 'value': '500' }, '500 lines'),
|
E('option', { 'value': '500' }, '500 lines'),
|
||||||
E('option', { 'value': '1000' }, '1000 lines')
|
E('option', { 'value': '1000' }, '1000 lines')
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'sh-toggle-group' }, [
|
E('label', { 'class': 'sh-toggle', 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
|
||||||
this.renderToggle(_('Auto Refresh'), this.autoRefresh, function(enabled) {
|
E('input', {
|
||||||
self.autoRefresh = enabled;
|
'type': 'checkbox',
|
||||||
|
'checked': this.autoScroll ? 'checked' : null,
|
||||||
|
'change': function(ev) {
|
||||||
|
self.autoScroll = ev.target.checked;
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
this.renderToggle(_('Auto Scroll'), this.autoScroll, function(enabled) {
|
E('span', {}, _('Auto Scroll'))
|
||||||
self.autoScroll = enabled;
|
|
||||||
})
|
|
||||||
]),
|
]),
|
||||||
E('button', {
|
E('button', {
|
||||||
'class': 'sh-btn sh-btn-primary',
|
'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) {
|
renderToggle: function(label, state, handler) {
|
||||||
return E('label', { 'class': 'sh-toggle' }, [
|
return E('label', { 'class': 'sh-toggle' }, [
|
||||||
E('input', {
|
E('input', {
|
||||||
@ -194,13 +259,18 @@ return view.extend({
|
|||||||
}, this);
|
}, this);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateLogStream: function() {
|
updateLogStream: function(hasNewLogs) {
|
||||||
var container = document.getElementById('sh-log-stream');
|
var container = document.getElementById('sh-log-stream');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
var filtered = this.getFilteredLogs();
|
var filtered = this.getFilteredLogs();
|
||||||
|
var totalLines = filtered.length;
|
||||||
var frag = filtered.map(function(line, idx) {
|
var frag = filtered.map(function(line, idx) {
|
||||||
var severity = this.detectSeverity(line);
|
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-index' }, idx + 1),
|
||||||
E('span', { 'class': 'sh-log-message' }, line)
|
E('span', { 'class': 'sh-log-message' }, line)
|
||||||
]);
|
]);
|
||||||
@ -252,7 +322,7 @@ return view.extend({
|
|||||||
]);
|
]);
|
||||||
return API.getLogs(this.lineCount, '').then(function(result) {
|
return API.getLogs(this.lineCount, '').then(function(result) {
|
||||||
ui.hideModal();
|
ui.hideModal();
|
||||||
self.logs = (result && result.logs) || [];
|
self.logs = Array.isArray(result) ? result : (result && result.logs) || [];
|
||||||
self.updateStats();
|
self.updateStats();
|
||||||
self.updateLogStream();
|
self.updateLogStream();
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
|
|||||||
@ -292,8 +292,8 @@ return view.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
getStatusLabel: function(state) {
|
getStatusLabel: function(state) {
|
||||||
if (state === undefined || state === null) return _('Unknown');
|
if (state === undefined || state === null) return '❓';
|
||||||
return state ? _('Healthy') : _('Attention');
|
return state ? '✅' : '⚠️';
|
||||||
},
|
},
|
||||||
|
|
||||||
getScoreLabel: function(score) {
|
getScoreLabel: function(score) {
|
||||||
|
|||||||
@ -117,6 +117,60 @@ get_system_info() {
|
|||||||
local localtime=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")
|
local localtime=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")
|
||||||
json_add_string "local_time" "$localtime"
|
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
|
json_dump
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -577,6 +631,147 @@ reboot_system() {
|
|||||||
( sleep 3 && reboot ) &
|
( 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 details
|
||||||
get_storage() {
|
get_storage() {
|
||||||
json_init
|
json_init
|
||||||
@ -1404,38 +1599,217 @@ save_settings() {
|
|||||||
json_dump
|
json_dump
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get components (leverages secubox module detection)
|
# Check if a package is installed (supports wildcards)
|
||||||
get_components() {
|
is_package_installed() {
|
||||||
# Call secubox backend to get apps list
|
local pkg="$1"
|
||||||
local apps_result=$(ubus call luci.secubox list_apps 2>/dev/null)
|
[ -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
|
# Get installed package info (name and version) for a component
|
||||||
# Transform apps to components format
|
get_installed_package_info() {
|
||||||
echo "$apps_result" | jq '{
|
local pkg="$1"
|
||||||
modules: [
|
[ -z "$pkg" ] && return
|
||||||
.apps[] | {
|
# Try exact match
|
||||||
id: .id,
|
local info=$(opkg list-installed "$pkg" 2>/dev/null | head -n1)
|
||||||
name: .name,
|
if [ -z "$info" ]; then
|
||||||
version: (.pkg_version // .version // "1.0.0"),
|
# Try luci-app- prefix
|
||||||
category: (.category // "system"),
|
info=$(opkg list-installed "luci-app-$pkg" 2>/dev/null | head -n1)
|
||||||
description: (.description // "No description"),
|
fi
|
||||||
icon: (.icon // "📦"),
|
echo "$info"
|
||||||
package: (.packages.required[0] // ""),
|
}
|
||||||
installed: false,
|
|
||||||
running: false,
|
# Check if a service is running
|
||||||
color: (
|
is_service_running() {
|
||||||
if .category == "security" then "#ef4444"
|
local svc="$1"
|
||||||
elif .category == "monitoring" then "#10b981"
|
[ -z "$svc" ] && return 1
|
||||||
elif .category == "network" then "#3b82f6"
|
|
||||||
else "#64748b"
|
# Check init script
|
||||||
end
|
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
|
else
|
||||||
# Fallback if secubox is not available
|
# Fallback: use hardcoded list with real status checks
|
||||||
echo '{"modules":[]}'
|
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
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1478,6 +1852,15 @@ case "$1" in
|
|||||||
"get_logs": { "lines": 100, "filter": "" },
|
"get_logs": { "lines": 100, "filter": "" },
|
||||||
"backup_config": {},
|
"backup_config": {},
|
||||||
"restore_config": { "file_name": "string", "data": "string" },
|
"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": {},
|
"reboot": {},
|
||||||
"get_storage": {},
|
"get_storage": {},
|
||||||
"get_settings": {},
|
"get_settings": {},
|
||||||
@ -1539,6 +1922,8 @@ EOF
|
|||||||
get_logs) get_logs ;;
|
get_logs) get_logs ;;
|
||||||
backup_config) backup_config ;;
|
backup_config) backup_config ;;
|
||||||
restore_config) restore_config ;;
|
restore_config) restore_config ;;
|
||||||
|
get_backup_schedule) get_backup_schedule ;;
|
||||||
|
set_backup_schedule) set_backup_schedule ;;
|
||||||
reboot) reboot_system ;;
|
reboot) reboot_system ;;
|
||||||
get_storage) get_storage ;;
|
get_storage) get_storage ;;
|
||||||
get_settings) get_settings ;;
|
get_settings) get_settings ;;
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
"get_logs",
|
"get_logs",
|
||||||
"get_storage",
|
"get_storage",
|
||||||
"get_settings",
|
"get_settings",
|
||||||
|
"get_backup_schedule",
|
||||||
"list_diagnostics",
|
"list_diagnostics",
|
||||||
"list_diagnostic_profiles",
|
"list_diagnostic_profiles",
|
||||||
"get_diagnostic_profile",
|
"get_diagnostic_profile",
|
||||||
@ -38,6 +39,7 @@
|
|||||||
"service_action",
|
"service_action",
|
||||||
"backup_config",
|
"backup_config",
|
||||||
"restore_config",
|
"restore_config",
|
||||||
|
"set_backup_schedule",
|
||||||
"reboot",
|
"reboot",
|
||||||
"save_settings",
|
"save_settings",
|
||||||
"collect_diagnostics",
|
"collect_diagnostics",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user