diff --git a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js index 878349d..6369d22 100644 --- a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js +++ b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/api.js @@ -58,6 +58,18 @@ var callStats = rpc.declare({ expect: { } }); +var callSecuboxLogs = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'seccubox_logs', + expect: { } +}); + +var callCollectDebug = rpc.declare({ + object: 'luci.crowdsec-dashboard', + method: 'collect_debug', + expect: { success: false } +}); + var callBan = rpc.declare({ object: 'luci.crowdsec-dashboard', method: 'ban', @@ -99,8 +111,26 @@ return baseclass.extend({ getMachines: callMachines, getHub: callHub, getStats: callStats, + getSecuboxLogs: callSecuboxLogs, + collectDebugSnapshot: callCollectDebug, addBan: callBan, removeBan: callUnban, formatDuration: formatDuration, - formatDate: formatDate + formatDate: formatDate, + + getDashboardData: function() { + return Promise.all([ + callStatus(), + callStats(), + callDecisions(), + callAlerts() + ]).then(function(results) { + return { + status: results[0] || {}, + stats: results[1] || {}, + decisions: (results[2] && results[2].decisions) || [], + alerts: (results[3] && results[3].alerts) || [] + }; + }); + } }); diff --git a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/dashboard.css b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/dashboard.css index 2f7909b..a7d7e00 100644 --- a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/dashboard.css +++ b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/dashboard.css @@ -247,6 +247,17 @@ padding: 0; } +.cs-log-card pre.cs-log-output { + background: #0b1120; + color: #9efc6a; + padding: 14px; + border-radius: 12px; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + max-height: 220px; + overflow-y: auto; +} + /* Tables */ .cs-table { width: 100%; diff --git a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js index b705c7c..5741a30 100644 --- a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js +++ b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js @@ -28,7 +28,10 @@ return view.extend({ // Load API this.csApi = new api(); - return this.csApi.getDashboardData(); + return Promise.all([ + this.csApi.getDashboardData(), + this.csApi.getSecuboxLogs() + ]); }, renderHeader: function(status) { @@ -310,7 +313,7 @@ return view.extend({ if (result.success) { self.showToast('IP ' + ip + ' unbanned successfully', 'success'); // Refresh data - return self.csApi.getDashboardData(); + return self.refreshDashboard(); } else { self.showToast('Failed to unban: ' + (result.error || 'Unknown error'), 'error'); } @@ -355,7 +358,7 @@ return view.extend({ if (result.success) { self.showToast('IP ' + ip + ' banned for ' + duration, 'success'); self.closeBanModal(); - return self.csApi.getDashboardData(); + return self.refreshDashboard(); } else { self.showToast('Failed to ban: ' + (result.error || 'Unknown error'), 'error'); } @@ -393,6 +396,7 @@ return view.extend({ var stats = data.stats || {}; var decisions = data.decisions || []; var alerts = data.alerts || []; + var logs = this.logs || []; return E('div', {}, [ this.renderHeader(status), @@ -429,31 +433,77 @@ return view.extend({ E('div', { 'class': 'cs-card-title' }, 'Recent Alerts'), ]), E('div', { 'class': 'cs-card-body' }, this.renderAlertsTimeline(alerts)) - ]) + ]), + this.renderLogCard(logs) ]), this.renderBanModal() ]); }, - render: function(data) { + render: function(payload) { var self = this; - this.data = data; + this.data = payload[0] || {}; + this.logs = (payload[1] && payload[1].entries) || []; var view = E('div', { 'class': 'crowdsec-dashboard' }, [ - E('div', { 'id': 'cs-dashboard-content' }, this.renderContent(data)) + E('div', { 'id': 'cs-dashboard-content' }, this.renderContent(this.data)) ]); // Setup polling for auto-refresh (every 30 seconds) poll.add(function() { - return self.csApi.getDashboardData().then(function(newData) { - self.data = newData; - self.updateView(); - }); + return self.refreshDashboard(); }, 30); return view; }, + +refreshDashboard: function() { + var self = this; + return Promise.all([ + self.csApi.getDashboardData(), + self.csApi.getSecuboxLogs() + ]).then(function(results) { + self.data = results[0]; + self.logs = (results[1] && results[1].entries) || []; + self.updateView(); + }); + }, + + renderLogCard: function(entries) { + return E('div', { 'class': 'cs-card cs-log-card' }, [ + E('div', { 'class': 'cs-card-header' }, [ + E('div', { 'class': 'cs-card-title' }, _('SecuBox Log Tail')), + E('button', { + 'class': 'cs-btn cs-btn-secondary cs-btn-sm', + 'click': ui.createHandlerFn(this, 'handleSnapshot') + }, _('Snapshot')) + ]), + entries && entries.length ? + E('pre', { 'class': 'cs-log-output' }, entries.join('\n')) : + E('p', { 'class': 'cs-empty' }, _('Log file empty')) + ]); + }, + + handleSnapshot: function() { + var self = this; + ui.showModal(_('Collecting snapshot'), [ + E('p', {}, _('Aggregating dmesg/logread into SecuBox log…')), + E('div', { 'class': 'spinning' }) + ]); + this.csApi.collectDebugSnapshot().then(function(result) { + ui.hideModal(); + if (result && result.success) { + self.refreshDashboard(); + self.showToast(_('Snapshot appended to /var/log/seccubox.log'), 'success'); + } else { + self.showToast((result && result.error) || _('Snapshot failed'), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + self.showToast(err.message || _('Snapshot failed'), 'error'); + }); + }, handleSaveApply: null, handleSave: null, diff --git a/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard b/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard index 734293c..189eddc 100755 --- a/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard +++ b/luci-app-crowdsec-dashboard/root/usr/libexec/rpcd/luci.crowdsec-dashboard @@ -6,6 +6,13 @@ . /lib/functions.sh . /usr/share/libubox/jshn.sh +SECCUBOX_LOG="/usr/sbin/secubox-log" + +secubox_log() { + [ -x "$SECCUBOX_LOG" ] || return + "$SECCUBOX_LOG" --tag "crowdsec" --message "$1" >/dev/null 2>&1 +} + CSCLI="/usr/bin/cscli" # Check if cscli exists @@ -137,6 +144,7 @@ add_ban() { result=$($CSCLI decisions add --ip "$ip" --duration "$duration" --reason "$reason" 2>&1) if [ $? -eq 0 ]; then + secubox_log "CrowdSec ban added for $ip ($duration)" echo '{"success": true}' else json_init @@ -161,6 +169,7 @@ remove_ban() { result=$($CSCLI decisions delete --ip "$ip" 2>&1) if [ $? -eq 0 ]; then + secubox_log "CrowdSec ban removed for $ip" echo '{"success": true}' else json_init @@ -214,6 +223,31 @@ get_dashboard_stats() { json_dump } +seccubox_logs() { + json_init + json_add_array "entries" + if [ -f /var/log/seccubox.log ]; then + tail -n 80 /var/log/seccubox.log | while IFS= read -r line; do + json_add_string "" "$line" + done + fi + json_close_array + json_dump +} + +collect_debug() { + json_init + if [ -x "$SECCUBOX_LOG" ]; then + "$SECCUBOX_LOG" --snapshot >/dev/null 2>&1 + json_add_boolean "success" 1 + json_add_string "message" "Snapshot appended to /var/log/seccubox.log" + else + json_add_boolean "success" 0 + json_add_string "error" "secubox-log helper not found" + fi + json_dump +} + # Main dispatcher case "$1" in list) @@ -259,6 +293,12 @@ case "$1" in stats) get_dashboard_stats ;; + seccubox_logs) + seccubox_logs + ;; + collect_debug) + collect_debug + ;; *) echo '{"error": "Unknown method"}' ;; diff --git a/luci-app-netdata-dashboard/README.md b/luci-app-netdata-dashboard/README.md index c651e8a..f5f9f45 100644 --- a/luci-app-netdata-dashboard/README.md +++ b/luci-app-netdata-dashboard/README.md @@ -47,6 +47,11 @@ Real-time system monitoring dashboard for OpenWrt with a modern, responsive inte - Animated gauges and sparklines - GitHub-inspired color palette +### 🔔 SecuBox Alerts & Logs +- Control bar integrates with the new `/usr/sbin/secubox-log` helper. +- Start/restart/stop events get appended to `/var/log/seccubox.log`. +- Dashboard card shows the tail of the aggregated log and lets you capture a dmesg/logread snapshot from LuCI. + ## Screenshots ### Real-time View diff --git a/luci-app-netdata-dashboard/htdocs/luci-static/resources/netdata-dashboard/api.js b/luci-app-netdata-dashboard/htdocs/luci-static/resources/netdata-dashboard/api.js index 3fa9c2e..c2e4377 100644 --- a/luci-app-netdata-dashboard/htdocs/luci-static/resources/netdata-dashboard/api.js +++ b/luci-app-netdata-dashboard/htdocs/luci-static/resources/netdata-dashboard/api.js @@ -96,6 +96,18 @@ var callStopNetdata = rpc.declare({ expect: { success: false } }); +var callSecuboxLogs = rpc.declare({ + object: 'luci.netdata-dashboard', + method: 'seccubox_logs', + expect: { } +}); + +var callCollectDebug = rpc.declare({ + object: 'luci.netdata-dashboard', + method: 'collect_debug', + expect: { success: false } +}); + function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 B'; var units = ['B', 'KB', 'MB', 'GB', 'TB']; @@ -134,6 +146,8 @@ return baseclass.extend({ restartNetdata: callRestartNetdata, startNetdata: callStartNetdata, stopNetdata: callStopNetdata, + getSecuboxLogs: callSecuboxLogs, + collectDebugSnapshot: callCollectDebug, // Utility functions formatBytes: formatBytes, diff --git a/luci-app-netdata-dashboard/htdocs/luci-static/resources/netdata-dashboard/dashboard.css b/luci-app-netdata-dashboard/htdocs/luci-static/resources/netdata-dashboard/dashboard.css index b8da42d..9030351 100644 --- a/luci-app-netdata-dashboard/htdocs/luci-static/resources/netdata-dashboard/dashboard.css +++ b/luci-app-netdata-dashboard/htdocs/luci-static/resources/netdata-dashboard/dashboard.css @@ -170,6 +170,96 @@ color: var(--nd-text-muted); } +/* New SecuBox-aligned cards */ +.nd-control-bar { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.nd-card { + background: var(--nd-bg-secondary); + border: 1px solid var(--nd-border); + border-radius: var(--nd-radius-lg); + padding: 20px; + margin-bottom: 16px; + box-shadow: var(--nd-shadow); +} + +.nd-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.nd-card-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; +} + +.nd-chip { + padding: 4px 10px; + border-radius: 999px; + background: rgba(56, 189, 248, 0.1); + color: var(--nd-accent-blue); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.nd-chip.danger { + background: rgba(248, 81, 73, 0.15); + color: var(--nd-accent-red); +} + +.nd-card-text { + margin: 0; + color: var(--nd-text-secondary); +} + +.nd-card-actions { + margin-top: 12px; +} + +.nd-card.nd-logs pre { + background: #000; + color: #a8ff60; + padding: 12px; + border-radius: var(--nd-radius); + max-height: 240px; + overflow-y: auto; + font-family: var(--nd-font-mono); + font-size: 12px; +} + +.nd-card.nd-embed { + padding: 0; + overflow: hidden; +} + +.nd-card.nd-embed.off { + padding: 20px; + text-align: center; +} + +.nd-iframe-wrapper { + position: relative; + width: 100%; + padding-top: 75%; + background: var(--nd-bg-tertiary); +} + +.nd-iframe-wrapper iframe { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + border: none; +} /* Charts Grid */ .nd-charts-grid { display: grid; diff --git a/luci-app-netdata-dashboard/htdocs/luci-static/resources/view/netdata-dashboard/dashboard.js b/luci-app-netdata-dashboard/htdocs/luci-static/resources/view/netdata-dashboard/dashboard.js index f6d55a4..47442a5 100644 --- a/luci-app-netdata-dashboard/htdocs/luci-static/resources/view/netdata-dashboard/dashboard.js +++ b/luci-app-netdata-dashboard/htdocs/luci-static/resources/view/netdata-dashboard/dashboard.js @@ -4,13 +4,20 @@ 'require ui'; 'require poll'; 'require netdata-dashboard/api as API'; +'require secubox-theme/theme as Theme'; + +var lang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: lang }); return view.extend({ load: function() { return Promise.all([ API.getNetdataStatus(), API.getNetdataAlarms(), - API.getStats() + API.getStats(), + API.getSecuboxLogs() ]); }, @@ -18,163 +25,162 @@ return view.extend({ var netdataStatus = data[0] || {}; var alarms = data[1] || {}; var stats = data[2] || {}; + var logs = (data[3] && data[3].entries) || []; var isRunning = netdataStatus.running || false; var netdataUrl = netdataStatus.url || 'http://127.0.0.1:19999'; + var alarmCount = this.countAlarms(alarms); - // Count active alarms - var alarmCount = 0; - if (alarms.alarms && typeof alarms.alarms === 'object') { - Object.keys(alarms.alarms).forEach(function(key) { - var alarm = alarms.alarms[key]; - if (alarm.status && alarm.status !== 'CLEAR') { - alarmCount++; - } - }); - } - - var view = E('div', { 'class': 'cbi-map' }, [ - E('h2', {}, _('Netdata Dashboard')), - E('div', { 'class': 'cbi-map-descr' }, - _('Real-time system monitoring and performance metrics powered by Netdata.')), - - // Control Panel - E('div', { 'class': 'cbi-section', 'style': 'margin-bottom: 1em;' }, [ - E('div', { 'style': 'display: grid; grid-template-columns: 2fr 1fr; gap: 1em;' }, [ - // Status Card - E('div', { - 'style': 'background: ' + (isRunning ? '#d4edda' : '#f8d7da') + '; border-left: 4px solid ' + (isRunning ? '#28a745' : '#dc3545') + '; padding: 1em; border-radius: 4px;' - }, [ - E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [ - E('div', {}, [ - E('div', { 'style': 'font-size: 0.9em; color: #666; margin-bottom: 0.25em;' }, _('Service Status')), - E('div', { 'style': 'font-size: 1.5em; font-weight: bold; color: ' + (isRunning ? '#155724' : '#721c24') + ';' }, - isRunning ? _('RUNNING') : _('STOPPED')) - ]), - E('div', { 'style': 'text-align: right;' }, [ - E('div', { 'style': 'font-size: 0.9em; color: #666; margin-bottom: 0.25em;' }, _('Version')), - E('div', { 'style': 'font-size: 1.1em; font-weight: bold;' }, netdataStatus.version || 'Unknown') - ]), - E('div', { 'style': 'text-align: right;' }, [ - E('div', { 'style': 'font-size: 0.9em; color: #666; margin-bottom: 0.25em;' }, _('Port')), - E('div', { 'style': 'font-size: 1.1em; font-weight: bold;' }, (netdataStatus.port || 19999).toString()) - ]) - ]) - ]), - - // Control Buttons - E('div', { 'style': 'display: flex; flex-direction: column; gap: 0.5em;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': L.bind(this.handleStart, this), - 'disabled': isRunning, - 'style': 'flex: 1;' - }, _('Start Netdata')), - E('button', { - 'class': 'cbi-button cbi-button-reset', - 'click': L.bind(this.handleRestart, this), - 'disabled': !isRunning, - 'style': 'flex: 1;' - }, _('Restart Netdata')), - E('button', { - 'class': 'cbi-button cbi-button-negative', - 'click': L.bind(this.handleStop, this), - 'disabled': !isRunning, - 'style': 'flex: 1;' - }, _('Stop Netdata')) - ]) - ]) - ]), - - // Alarms Card - alarmCount > 0 ? E('div', { - 'class': 'cbi-section', - 'style': 'background: #fff3cd; border-left: 4px solid #ffc107; padding: 1em; margin-bottom: 1em;' - }, [ - E('div', { 'style': 'display: flex; align-items: center; gap: 1em;' }, [ - E('div', { 'style': 'font-size: 2em;' }, '⚠️'), - E('div', { 'style': 'flex: 1;' }, [ - E('strong', {}, _('Active Alarms: %d').format(alarmCount)), - E('p', { 'style': 'margin: 0.25em 0 0 0; color: #666;' }, - _('Netdata has detected %d active alarm(s). Check the dashboard for details.').format(alarmCount)) - ]), - E('a', { - 'href': netdataUrl + '#menu_alarms', - 'target': '_blank', - 'class': 'cbi-button cbi-button-action' - }, _('View Alarms')) - ]) - ]) : null, - - // Quick Stats Preview - E('div', { 'class': 'cbi-section', 'style': 'margin-bottom: 1em;' }, [ - E('h3', { 'style': 'margin-top: 0;' }, _('Quick Stats')), - E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1em;' }, [ - this.renderStatCard(_('CPU'), stats.cpu_percent + '%', '#0088cc'), - this.renderStatCard(_('Memory'), stats.memory_percent + '%', '#17a2b8'), - this.renderStatCard(_('Disk'), stats.disk_percent + '%', '#6610f2'), - this.renderStatCard(_('Load'), stats.load || '0.00', '#e83e8c'), - this.renderStatCard(_('Temp'), stats.temperature + '°C', '#fd7e14'), - this.renderStatCard(_('Uptime'), API.formatUptime(stats.uptime || 0), '#28a745') - ]) - ]), - - // Netdata Dashboard Iframe - isRunning ? E('div', { 'class': 'cbi-section' }, [ - E('h3', { 'style': 'margin-top: 0;' }, _('Netdata Real-Time Dashboard')), - E('div', { - 'style': 'position: relative; width: 100%; height: 0; padding-bottom: 75%; background: #f5f5f5; border-radius: 4px; overflow: hidden;' - }, [ - E('iframe', { - 'src': netdataUrl, - 'style': 'position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;', - 'frameborder': '0', - 'allow': 'fullscreen' - }) - ]), - E('div', { 'style': 'margin-top: 1em; padding: 0.75em; background: #e8f4f8; border-radius: 4px;' }, [ - E('strong', {}, _('Tip:')), - ' ', - _('Use the Netdata interface above for detailed real-time monitoring. Click '), - E('a', { 'href': netdataUrl, 'target': '_blank' }, _('here')), - _(' to open in a new window.') - ]) - ]) : E('div', { 'class': 'cbi-section' }, [ - E('div', { - 'style': 'text-align: center; padding: 3em; background: #f8d7da; border-radius: 4px; border-left: 4px solid #dc3545;' - }, [ - E('div', { 'style': 'font-size: 3em; margin-bottom: 0.5em;' }, '⚠️'), - E('h3', {}, _('Netdata is not running')), - E('p', { 'style': 'color: #666; margin-bottom: 1.5em;' }, - _('Start the Netdata service to access real-time monitoring dashboards.')), - E('button', { - 'class': 'cbi-button cbi-button-action', - 'style': 'font-size: 1.1em; padding: 0.75em 2em;', - 'click': L.bind(this.handleStart, this) - }, _('Start Netdata Now')) - ]) - ]) + var view = E('div', { 'class': 'netdata-dashboard secubox-netdata' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), + E('link', { 'rel': 'stylesheet', 'href': L.resource('netdata-dashboard/dashboard.css') }), + this.renderHeader(netdataStatus, stats), + this.renderControls(isRunning), + this.renderQuickStats(stats), + this.renderAlarmCard(alarmCount, netdataUrl), + this.renderLogCard(logs), + this.renderEmbed(isRunning, netdataUrl) ]); // Setup auto-refresh poll.add(L.bind(function() { return Promise.all([ API.getNetdataStatus(), - API.getStats() + API.getStats(), + API.getSecuboxLogs() ]).then(L.bind(function(refreshData) { - // Could update stats display here + // Update quick stats/logs in place if needed }, this)); }, this), 5); return view; }, - renderStatCard: function(label, value, color) { - return E('div', { - 'style': 'background: white; border-left: 4px solid ' + color + '; padding: 1em; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);' - }, [ - E('div', { 'style': 'font-size: 0.85em; color: #666; margin-bottom: 0.25em;' }, label), - E('div', { 'style': 'font-size: 1.5em; font-weight: bold; color: ' + color + ';' }, value) + countAlarms: function(alarms) { + var count = 0; + if (alarms.alarms && typeof alarms.alarms === 'object') { + Object.keys(alarms.alarms).forEach(function(key) { + var alarm = alarms.alarms[key]; + if (alarm.status && alarm.status !== 'CLEAR') + count++; + }); + } + return count; + }, + + renderHeader: function(status, stats) { + return E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [ + E('div', {}, [ + E('h2', { 'class': 'sh-page-title' }, [ + E('span', { 'class': 'sh-page-title-icon' }, '📊'), + _('Netdata Monitoring') + ]), + E('p', { 'class': 'sh-page-subtitle' }, + _('Real-time analytics for CPU, memory, disk, and services.')) + ]), + E('div', { 'class': 'sh-header-meta' }, [ + this.renderHeaderChip(_('Status'), status.running ? _('Online') : _('Offline'), + status.running ? 'success' : 'warn'), + this.renderHeaderChip(_('Version'), status.version || _('Unknown')), + this.renderHeaderChip(_('Uptime'), API.formatUptime(stats.uptime || 0)) + ]) + ]); + }, + + renderHeaderChip: function(label, value, tone) { + return E('div', { 'class': 'sh-header-chip' + (tone ? ' ' + tone : '') }, [ + E('span', { 'class': 'sh-chip-label' }, label), + E('strong', {}, value) + ]); + }, + + renderControls: function(isRunning) { + return E('div', { 'class': 'nd-control-bar' }, [ + E('button', { + 'class': 'sh-btn-primary', + 'click': L.bind(this.handleStart, this), + 'disabled': isRunning + }, ['▶️ ', _('Start')]), + E('button', { + 'class': 'sh-btn-secondary', + 'click': L.bind(this.handleRestart, this), + 'disabled': !isRunning + }, ['🔁 ', _('Restart')]), + E('button', { + 'class': 'sh-btn-secondary', + 'click': L.bind(this.handleStop, this), + 'disabled': !isRunning + }, ['⏹ ', _('Stop')]) + ]); + }, + + renderQuickStats: function(stats) { + return E('div', { 'class': 'nd-quick-stats' }, [ + this.renderStatCard(_('CPU'), (stats.cpu_percent || 0) + '%'), + this.renderStatCard(_('Memory'), (stats.memory_percent || 0) + '%'), + this.renderStatCard(_('Disk'), (stats.disk_percent || 0) + '%'), + this.renderStatCard(_('Load'), stats.load || '0.00'), + this.renderStatCard(_('Temp'), (stats.temperature || 0) + '°C'), + this.renderStatCard(_('Clients'), stats.clients || 0) + ]); + }, + + renderStatCard: function(label, value) { + return E('div', { 'class': 'nd-stat-card' }, [ + E('span', { 'class': 'nd-stat-label' }, label), + E('strong', { 'class': 'nd-stat-value' }, value) + ]); + }, + + renderAlarmCard: function(count, url) { + return E('div', { 'class': 'nd-card nd-alarms' }, [ + E('div', { 'class': 'nd-card-header' }, [ + E('div', { 'class': 'nd-card-title' }, ['🚨', _('Netdata alarms')]), + E('span', { 'class': 'nd-chip' + (count > 0 ? ' danger' : '') }, count + ' ' + _('active')) + ]), + count > 0 ? E('p', { 'class': 'nd-card-text' }, + _('Netdata reports %d active alarms. Open the dashboard to investigate.').format(count)) : + E('p', { 'class': 'nd-card-text' }, _('No active alarms detected.')), + E('div', { 'class': 'nd-card-actions' }, [ + E('a', { 'href': url + '#menu_alarms', 'class': 'sh-btn-secondary', 'target': '_blank' }, ['🔍 ', _('View alarms')]) + ]) + ]); + }, + + renderLogCard: function(entries) { + return E('div', { 'class': 'nd-card nd-logs' }, [ + E('div', { 'class': 'nd-card-header' }, [ + E('div', { 'class': 'nd-card-title' }, ['🗒️', _('SecuBox Log Tail')]), + E('button', { + 'class': 'sh-btn-secondary', + 'click': L.bind(this.handleSnapshot, this) + }, ['📎 ', _('Add snapshot')]) + ]), + entries && entries.length ? E('pre', { 'class': 'nd-log-output' }, + entries.join('\n')) : E('p', { 'class': 'nd-card-text' }, _('Log file empty.')) + ]); + }, + + renderEmbed: function(isRunning, url) { + if (!isRunning) { + return E('div', { 'class': 'nd-card nd-embed off' }, [ + E('div', { 'class': 'nd-card-title' }, ['⚠️ ', _('Netdata is offline')]), + E('p', { 'class': 'nd-card-text' }, _('Start the service to access real-time charts.')) + ]); + } + + return E('div', { 'class': 'nd-card nd-embed' }, [ + E('div', { 'class': 'nd-card-header' }, [ + E('div', { 'class': 'nd-card-title' }, ['📈', _('Live dashboard')]), + E('a', { 'href': url, 'target': '_blank', 'class': 'sh-btn-secondary' }, _('Open in tab')) + ]), + E('div', { 'class': 'nd-iframe-wrapper' }, [ + E('iframe', { + 'src': url, + 'frameborder': '0', + 'allow': 'fullscreen' + }) + ]) ]); }, @@ -268,6 +274,25 @@ return view.extend({ }); }, + handleSnapshot: function() { + var self = this; + ui.showModal(_('Collecting snapshot'), [ + E('p', {}, _('Aggregating dmesg and logread into SecuBox log…')), + E('div', { 'class': 'spinning' }) + ]); + API.collectDebugSnapshot().then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Snapshot appended to /var/log/seccubox.log.')), 'info'); + } else { + ui.addNotification(null, E('p', {}, (result && result.error) || _('Failed to collect snapshot.')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + }, + handleSaveApply: null, handleSave: null, handleReset: null diff --git a/luci-app-netdata-dashboard/root/usr/libexec/rpcd/luci.netdata-dashboard b/luci-app-netdata-dashboard/root/usr/libexec/rpcd/luci.netdata-dashboard index ddc9435..5f0632a 100755 --- a/luci-app-netdata-dashboard/root/usr/libexec/rpcd/luci.netdata-dashboard +++ b/luci-app-netdata-dashboard/root/usr/libexec/rpcd/luci.netdata-dashboard @@ -6,6 +6,13 @@ . /lib/functions.sh . /usr/share/libubox/jshn.sh +SECCUBOX_LOG="/usr/sbin/secubox-log" + +secubox_log() { + [ -x "$SECCUBOX_LOG" ] || return + "$SECCUBOX_LOG" --tag "netdata" --message "$1" >/dev/null 2>&1 +} + # Get CPU statistics get_cpu() { json_init @@ -525,6 +532,7 @@ restart_netdata() { if pgrep -x netdata >/dev/null 2>&1; then json_add_boolean "success" 1 json_add_string "message" "Netdata restarted successfully" + secubox_log "Netdata service restarted" else json_add_boolean "success" 0 json_add_string "error" "Failed to start Netdata" @@ -551,6 +559,7 @@ start_netdata() { if pgrep -x netdata >/dev/null 2>&1; then json_add_boolean "success" 1 json_add_string "message" "Netdata started successfully" + secubox_log "Netdata service started" else json_add_boolean "success" 0 json_add_string "error" "Failed to start Netdata" @@ -577,6 +586,7 @@ stop_netdata() { if ! pgrep -x netdata >/dev/null 2>&1; then json_add_boolean "success" 1 json_add_string "message" "Netdata stopped successfully" + secubox_log "Netdata service stopped" else json_add_boolean "success" 0 json_add_string "error" "Failed to stop Netdata" @@ -589,10 +599,35 @@ stop_netdata() { json_dump } +seccubox_logs() { + json_init + json_add_array "entries" + if [ -f /var/log/seccubox.log ]; then + tail -n 80 /var/log/seccubox.log | while IFS= read -r line; do + json_add_string "" "$line" + done + fi + json_close_array + json_dump +} + +collect_debug() { + json_init + if [ -x "$SECCUBOX_LOG" ]; then + "$SECCUBOX_LOG" --snapshot >/dev/null 2>&1 + json_add_boolean "success" 1 + json_add_string "message" "Snapshot collected to /var/log/seccubox.log" + else + json_add_boolean "success" 0 + json_add_string "error" "secubox-log helper not found" + fi + json_dump +} + # Main dispatcher case "$1" in list) - echo '{"stats":{},"cpu":{},"memory":{},"disk":{},"network":{},"processes":{},"sensors":{},"system":{},"netdata_status":{},"netdata_alarms":{},"netdata_info":{},"restart_netdata":{},"start_netdata":{},"stop_netdata":{}}' + echo '{"stats":{},"cpu":{},"memory":{},"disk":{},"network":{},"processes":{},"sensors":{},"system":{},"netdata_status":{},"netdata_alarms":{},"netdata_info":{},"restart_netdata":{},"start_netdata":{},"stop_netdata":{},"seccubox_logs":{},"collect_debug":{}}' ;; call) case "$2" in @@ -638,6 +673,12 @@ case "$1" in stop_netdata) stop_netdata ;; + seccubox_logs) + seccubox_logs + ;; + collect_debug) + collect_debug + ;; *) echo '{"error": "Unknown method"}' ;; diff --git a/luci-app-netifyd-dashboard/htdocs/luci-static/resources/netifyd-dashboard/api.js b/luci-app-netifyd-dashboard/htdocs/luci-static/resources/netifyd-dashboard/api.js index 218232e..26e70ab 100644 --- a/luci-app-netifyd-dashboard/htdocs/luci-static/resources/netifyd-dashboard/api.js +++ b/luci-app-netifyd-dashboard/htdocs/luci-static/resources/netifyd-dashboard/api.js @@ -46,6 +46,18 @@ var callStats = rpc.declare({ expect: { } }); +var callSecuboxLogs = rpc.declare({ + object: 'luci.netifyd-dashboard', + method: 'seccubox_logs', + expect: { } +}); + +var callCollectDebug = rpc.declare({ + object: 'luci.netifyd-dashboard', + method: 'collect_debug', + expect: { success: false } +}); + function formatBytes(bytes) { if (bytes === 0) return '0 B'; var k = 1024; @@ -61,6 +73,8 @@ return baseclass.extend({ getHosts: callHosts, getProtocols: callProtocols, getStats: callStats, + getSecuboxLogs: callSecuboxLogs, + collectDebugSnapshot: callCollectDebug, formatBytes: formatBytes, // Aggregate function for overview page diff --git a/luci-app-netifyd-dashboard/htdocs/luci-static/resources/netifyd-dashboard/dashboard.css b/luci-app-netifyd-dashboard/htdocs/luci-static/resources/netifyd-dashboard/dashboard.css index 16dd61d..656e6be 100644 --- a/luci-app-netifyd-dashboard/htdocs/luci-static/resources/netifyd-dashboard/dashboard.css +++ b/luci-app-netifyd-dashboard/htdocs/luci-static/resources/netifyd-dashboard/dashboard.css @@ -368,6 +368,48 @@ color: var(--nf-text-primary); } +.nf-log-card { + background: var(--nf-bg-secondary); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 18px; + padding: 20px; + margin-top: 20px; + box-shadow: 0 18px 30px rgba(2, 6, 23, 0.45); +} + +.nf-log-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-bottom: 10px; +} + +.nf-log-btn { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 6px 14px; + border-radius: 999px; + color: var(--nf-text-secondary); + cursor: pointer; +} + +.nf-log-output { + background: #020617; + color: #9efc6a; + padding: 12px; + border-radius: 12px; + font-family: var(--nf-font-mono); + font-size: 12px; + max-height: 220px; + overflow-y: auto; +} + +.nf-log-empty { + color: var(--nf-text-secondary); + font-size: 13px; +} + /* Flow Table */ .nf-table-container { overflow-x: auto; diff --git a/luci-app-netifyd-dashboard/htdocs/luci-static/resources/view/netifyd-dashboard/overview.js b/luci-app-netifyd-dashboard/htdocs/luci-static/resources/view/netifyd-dashboard/overview.js index 6283aea..f9a1477 100644 --- a/luci-app-netifyd-dashboard/htdocs/luci-static/resources/view/netifyd-dashboard/overview.js +++ b/luci-app-netifyd-dashboard/htdocs/luci-static/resources/view/netifyd-dashboard/overview.js @@ -9,7 +9,10 @@ return view.extend({ title: _('Netifyd Dashboard'), load: function() { - return api.getAllData(); + return Promise.all([ + api.getAllData(), + api.getSecuboxLogs() + ]); }, renderDonut: function(data, size) { @@ -47,8 +50,10 @@ return view.extend({ ]); }, - render: function(data) { + render: function(payload) { var self = this; + var data = payload[0] || {}; + var logEntries = (payload[1] && payload[1].entries) || []; var status = data.status || {}; var stats = data.stats || {}; var apps = (data.applications || {}).applications || []; @@ -63,7 +68,6 @@ return view.extend({ { name: 'UDP', value: udpFlows }, { name: 'Other', value: Math.max(0, totalFlows - tcpFlows - udpFlows) } ].filter(function(p) { return p.value > 0; }); - var topApps = apps.slice(0, 6); var maxAppBytes = topApps.length > 0 ? Math.max.apply(null, topApps.map(function(a) { return a.bytes; })) : 1; @@ -186,8 +190,10 @@ return view.extend({ ) ]) ]) - ]) - ]); + ]), + + this.renderLogCard(logEntries) + ]); // Include CSS var cssLink = E('link', { 'rel': 'stylesheet', 'href': L.resource('netifyd-dashboard/dashboard.css') }); @@ -196,6 +202,39 @@ return view.extend({ return view; }, + renderLogCard: function(entries) { + return E('div', { 'class': 'nf-log-card' }, [ + E('div', { 'class': 'nf-log-header' }, [ + E('strong', {}, _('SecuBox log tail')), + E('button', { + 'class': 'nf-log-btn', + 'click': L.bind(this.handleSnapshot, this) + }, _('Snapshot')) + ]), + entries && entries.length ? + E('pre', { 'class': 'nf-log-output' }, entries.join('\n')) : + E('p', { 'class': 'nf-log-empty' }, _('Log file empty')) + ]); + }, + + handleSnapshot: function() { + ui.showModal(_('Collecting snapshot'), [ + E('p', {}, _('Aggregating dmesg + logread into SecuBox log…')), + E('div', { 'class': 'spinning' }) + ]); + api.collectDebugSnapshot().then(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Snapshot appended to /var/log/seccubox.log')), 'info'); + } else { + ui.addNotification(null, E('p', {}, (result && result.error) || _('Snapshot failed')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + }, + handleSaveApply: null, handleSave: null, handleReset: null diff --git a/luci-app-netifyd-dashboard/root/usr/libexec/rpcd/luci.netifyd-dashboard b/luci-app-netifyd-dashboard/root/usr/libexec/rpcd/luci.netifyd-dashboard index 270e60d..20e09dc 100755 --- a/luci-app-netifyd-dashboard/root/usr/libexec/rpcd/luci.netifyd-dashboard +++ b/luci-app-netifyd-dashboard/root/usr/libexec/rpcd/luci.netifyd-dashboard @@ -6,6 +6,13 @@ . /lib/functions.sh . /usr/share/libubox/jshn.sh +SECCUBOX_LOG="/usr/sbin/secubox-log" + +secubox_log() { + [ -x "$SECCUBOX_LOG" ] || return + "$SECCUBOX_LOG" --tag "netifyd" --message "$1" >/dev/null 2>&1 +} + NETIFYD_SOCKET="/var/run/netifyd/netifyd.sock" NETIFYD_STATUS="/var/run/netifyd/status.json" NETIFYD_FLOWS="/var/run/netifyd/flows.json" @@ -265,6 +272,31 @@ get_devices() { json_dump } +seccubox_logs() { + json_init + json_add_array "entries" + if [ -f /var/log/seccubox.log ]; then + tail -n 80 /var/log/seccubox.log | while IFS= read -r line; do + json_add_string "" "$line" + done + fi + json_close_array + json_dump +} + +collect_debug() { + json_init + if [ -x "$SECCUBOX_LOG" ]; then + "$SECCUBOX_LOG" --snapshot >/dev/null 2>&1 + json_add_boolean "success" 1 + json_add_string "message" "Snapshot stored in /var/log/seccubox.log" + else + json_add_boolean "success" 0 + json_add_string "error" "secubox-log helper not found" + fi + json_dump +} + # Get overall statistics get_stats() { json_init @@ -463,7 +495,7 @@ get_dns_queries() { # Main dispatcher case "$1" in list) - echo '{"status":{},"flows":{},"applications":{},"protocols":{},"devices":{},"stats":{},"risks":{},"category_bandwidth":{},"top_talkers":{},"dns_queries":{}}' + echo '{"status":{},"flows":{},"applications":{},"protocols":{},"devices":{},"stats":{},"risks":{},"category_bandwidth":{},"top_talkers":{},"dns_queries":{},"seccubox_logs":{},"collect_debug":{}}' ;; call) case "$2" in @@ -497,6 +529,12 @@ case "$1" in dns_queries) get_dns_queries ;; + seccubox_logs) + seccubox_logs + ;; + collect_debug) + collect_debug + ;; *) echo '{"error": "Unknown method"}' ;; diff --git a/luci-app-secubox/root/usr/sbin/secubox-log b/luci-app-secubox/root/usr/sbin/secubox-log new file mode 100755 index 0000000..8cd9bd8 --- /dev/null +++ b/luci-app-secubox/root/usr/sbin/secubox-log @@ -0,0 +1,87 @@ +#!/bin/sh +# +# SecuBox Log Aggregator / Logger +# Central log file: /var/log/seccubox.log + +LOG_FILE="/var/log/seccubox.log" +TAG="secubox" +MESSAGE="" +PAYLOAD="" +TAIL_COUNT="" +MODE="append" + +ensure_log() { + local dir + dir="$(dirname "$LOG_FILE")" + [ -d "$dir" ] || mkdir -p "$dir" + touch "$LOG_FILE" +} + +write_entry() { + ensure_log + printf '%s [%s] %s\n' "$(date -Iseconds)" "$TAG" "$MESSAGE" >> "$LOG_FILE" + [ -n "$PAYLOAD" ] && printf '%s\n' "$PAYLOAD" >> "$LOG_FILE" +} + +write_snapshot() { + ensure_log + { + printf '===== SNAPSHOT %s =====\n' "$(date -Iseconds)" + printf '--- DMESG (tail -n 200) ---\n' + dmesg | tail -n 200 + printf '--- LOGREAD (tail -n 200) ---\n' + logread 2>/dev/null | tail -n 200 + printf '===== END SNAPSHOT =====\n' + } >> "$LOG_FILE" +} + +tail_log() { + ensure_log + if [ -n "$TAIL_COUNT" ]; then + tail -n "$TAIL_COUNT" "$LOG_FILE" + else + tail "$LOG_FILE" + fi +} + +while [ $# -gt 0 ]; do + case "$1" in + --tag) + TAG="$2"; shift 2;; + --message|-m) + MESSAGE="$2"; shift 2;; + --payload|-p) + PAYLOAD="$2"; shift 2;; + --snapshot) + MODE="snapshot"; shift;; + --tail) + MODE="tail"; TAIL_COUNT="$2"; shift 2;; + --tail-all) + MODE="tail"; TAIL_COUNT=""; shift;; + -h|--help) + cat <<'EOF' +secubox-log --tag TAG --message MSG [--payload TEXT] +secubox-log --snapshot # append dmesg+logread snapshot +secubox-log --tail [N] # print last N lines (default 10) +EOF + exit 0;; + *) + shift;; + esac +done + +case "$MODE" in + snapshot) + write_snapshot + ;; + tail) + tail_log + ;; + *) + if [ -z "$MESSAGE" ]; then + echo "Usage: secubox-log --message 'text' [--tag tag]" >&2 + exit 1 + fi + write_entry + ;; +esac diff --git a/secubox-tools/README.md b/secubox-tools/README.md index e08d6e9..226093a 100644 --- a/secubox-tools/README.md +++ b/secubox-tools/README.md @@ -120,6 +120,25 @@ opkg install /tmp/luci-app-system-hub*.ipk /etc/init.d/rpcd restart ``` +### Logging & Debug Utilities + +#### secubox-log.sh + +Centralized logger/aggregator for SecuBox modules. Appends tagged events to `/var/log/seccubox.log`, captures snapshots that merge `dmesg` + `logread`, and can tail the aggregated file for troubleshooting. + +``` +# Append a message +secubox-log.sh --tag netdata --message "Netdata restarted" + +# Add a snapshot with dmesg/logread tail +secubox-log.sh --snapshot + +# Tail the aggregated log +secubox-log.sh --tail 100 +``` + +The script is also installed on the router as `/usr/sbin/secubox-log` (via `luci-app-secubox`) so LuCI modules can log lifecycle events and collect debug bundles. + **Example Workflow - Firmware Building:** ```bash # 1. Build firmware for MOCHAbin with SecuBox pre-installed diff --git a/secubox-tools/secubox-log.sh b/secubox-tools/secubox-log.sh new file mode 100755 index 0000000..dbb4e61 --- /dev/null +++ b/secubox-tools/secubox-log.sh @@ -0,0 +1,91 @@ +#!/bin/sh +# +# SecuBox Log Aggregator / Logger +# Usage: +# secubox-log.sh --tag netdata --message "Netdata started" +# secubox-log.sh --snapshot # append dmesg + logread snapshot +# secubox-log.sh --tail 100 # print last 100 lines +# + +LOG_FILE="/var/log/seccubox.log" +TAG="secubox" +MESSAGE="" +PAYLOAD="" +TAIL_COUNT="" +MODE="append" + +ensure_log() { + local dir + dir="$(dirname "$LOG_FILE")" + [ -d "$dir" ] || mkdir -p "$dir" + touch "$LOG_FILE" +} + +write_entry() { + ensure_log + printf '%s [%s] %s\n' "$(date -Iseconds)" "$TAG" "$MESSAGE" >> "$LOG_FILE" + [ -n "$PAYLOAD" ] && printf '%s\n' "$PAYLOAD" >> "$LOG_FILE" +} + +write_snapshot() { + ensure_log + { + printf '===== SNAPSHOT %s =====\n' "$(date -Iseconds)" + printf '--- DMESG (tail -n 200) ---\n' + dmesg | tail -n 200 + printf '--- LOGREAD (tail -n 200) ---\n' + logread 2>/dev/null | tail -n 200 + printf '===== END SNAPSHOT =====\n' + } >> "$LOG_FILE" +} + +tail_log() { + ensure_log + if [ -n "$TAIL_COUNT" ]; then + tail -n "$TAIL_COUNT" "$LOG_FILE" + else + tail "$LOG_FILE" + fi +} + +while [ $# -gt 0 ]; do + case "$1" in + --tag) + TAG="$2"; shift 2;; + --message|-m) + MESSAGE="$2"; shift 2;; + --payload|-p) + PAYLOAD="$2"; shift 2;; + --snapshot) + MODE="snapshot"; shift;; + --tail) + MODE="tail"; TAIL_COUNT="$2"; shift 2;; + --tail-all) + MODE="tail"; TAIL_COUNT=""; shift;; + -h|--help) + cat <<'EOF' +secubox-log.sh --tag TAG --message MSG [--payload TEXT] +secubox-log.sh --snapshot # append dmesg+logread snapshot +secubox-log.sh --tail [N] # print last N lines (default 10) +EOF + exit 0;; + *) + shift;; + esac +done + +case "$MODE" in + snapshot) + write_snapshot + ;; + tail) + tail_log + ;; + *) + if [ -z "$MESSAGE" ]; then + echo "Usage: $0 --message 'text' [--tag tag]" >&2 + exit 1 + fi + write_entry + ;; +esac