From 8255cc6f39cf29c2ee5e8fe6bd9531c0d9a47c80 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Thu, 8 Jan 2026 15:37:50 +0100 Subject: [PATCH] feat: Add scheduled backups, live logs, and component detection (v0.6.0-r30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../luci-static/resources/system-hub/api.js | 17 + .../resources/system-hub/backup.css | 83 ++++ .../luci-static/resources/system-hub/logs.css | 25 + .../resources/view/system-hub/backup.js | 180 ++++++- .../resources/view/system-hub/components.js | 44 +- .../resources/view/system-hub/logs.js | 100 +++- .../resources/view/system-hub/overview.js | 4 +- .../root/usr/libexec/rpcd/luci.system-hub | 445 ++++++++++++++++-- .../share/rpcd/acl.d/luci-app-system-hub.json | 2 + 9 files changed, 832 insertions(+), 68 deletions(-) diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js index a2bbe319..48a9d88c 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js @@ -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, diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/backup.css b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/backup.css index a009e111..7ed8080d 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/backup.css +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/backup.css @@ -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; +} diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/logs.css b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/logs.css index 6b3dd196..5f270009 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/logs.css +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/logs.css @@ -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; diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js index 0e603e35..374fa91d 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js @@ -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' }, [ diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/components.js b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/components.js index 4eac97a9..12e93eaf 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/components.js +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/components.js @@ -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'); }); }, diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js index 2923b2a6..9e53ba45 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js @@ -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 = '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) { diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js index db6c3272..8311c222 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js @@ -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) { diff --git a/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub b/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub index 22b7b47f..b92540dd 100755 --- a/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub +++ b/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub @@ -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 ;; diff --git a/package/secubox/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json b/package/secubox/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json index 43825b77..bd86cafb 100644 --- a/package/secubox/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json +++ b/package/secubox/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json @@ -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",