feat: theme-aligned monitoring and seccubox logs

This commit is contained in:
CyberMind-FR 2025-12-29 15:41:13 +01:00
parent ee5c001572
commit d566a84dda
16 changed files with 796 additions and 160 deletions

View File

@ -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) || []
};
});
}
});

View File

@ -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%;

View File

@ -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,32 +433,78 @@ 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,
handleReset: null

View File

@ -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"}'
;;

View File

@ -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

View File

@ -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,

View File

@ -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;

View File

@ -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

View File

@ -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"}'
;;

View File

@ -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

View File

@ -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;

View File

@ -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,7 +190,9 @@ return view.extend({
)
])
])
])
]),
this.renderLogCard(logEntries)
]);
// Include 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

View File

@ -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"}'
;;

View File

@ -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

View File

@ -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

91
secubox-tools/secubox-log.sh Executable file
View File

@ -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