feat: theme-aligned monitoring and seccubox logs
This commit is contained in:
parent
ee5c001572
commit
d566a84dda
@ -58,6 +58,18 @@ var callStats = rpc.declare({
|
|||||||
expect: { }
|
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({
|
var callBan = rpc.declare({
|
||||||
object: 'luci.crowdsec-dashboard',
|
object: 'luci.crowdsec-dashboard',
|
||||||
method: 'ban',
|
method: 'ban',
|
||||||
@ -99,8 +111,26 @@ return baseclass.extend({
|
|||||||
getMachines: callMachines,
|
getMachines: callMachines,
|
||||||
getHub: callHub,
|
getHub: callHub,
|
||||||
getStats: callStats,
|
getStats: callStats,
|
||||||
|
getSecuboxLogs: callSecuboxLogs,
|
||||||
|
collectDebugSnapshot: callCollectDebug,
|
||||||
addBan: callBan,
|
addBan: callBan,
|
||||||
removeBan: callUnban,
|
removeBan: callUnban,
|
||||||
formatDuration: formatDuration,
|
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) || []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -247,6 +247,17 @@
|
|||||||
padding: 0;
|
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 */
|
/* Tables */
|
||||||
.cs-table {
|
.cs-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -28,7 +28,10 @@ return view.extend({
|
|||||||
// Load API
|
// Load API
|
||||||
this.csApi = new api();
|
this.csApi = new api();
|
||||||
|
|
||||||
return this.csApi.getDashboardData();
|
return Promise.all([
|
||||||
|
this.csApi.getDashboardData(),
|
||||||
|
this.csApi.getSecuboxLogs()
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHeader: function(status) {
|
renderHeader: function(status) {
|
||||||
@ -310,7 +313,7 @@ return view.extend({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
self.showToast('IP ' + ip + ' unbanned successfully', 'success');
|
self.showToast('IP ' + ip + ' unbanned successfully', 'success');
|
||||||
// Refresh data
|
// Refresh data
|
||||||
return self.csApi.getDashboardData();
|
return self.refreshDashboard();
|
||||||
} else {
|
} else {
|
||||||
self.showToast('Failed to unban: ' + (result.error || 'Unknown error'), 'error');
|
self.showToast('Failed to unban: ' + (result.error || 'Unknown error'), 'error');
|
||||||
}
|
}
|
||||||
@ -355,7 +358,7 @@ return view.extend({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
self.showToast('IP ' + ip + ' banned for ' + duration, 'success');
|
self.showToast('IP ' + ip + ' banned for ' + duration, 'success');
|
||||||
self.closeBanModal();
|
self.closeBanModal();
|
||||||
return self.csApi.getDashboardData();
|
return self.refreshDashboard();
|
||||||
} else {
|
} else {
|
||||||
self.showToast('Failed to ban: ' + (result.error || 'Unknown error'), 'error');
|
self.showToast('Failed to ban: ' + (result.error || 'Unknown error'), 'error');
|
||||||
}
|
}
|
||||||
@ -393,6 +396,7 @@ return view.extend({
|
|||||||
var stats = data.stats || {};
|
var stats = data.stats || {};
|
||||||
var decisions = data.decisions || [];
|
var decisions = data.decisions || [];
|
||||||
var alerts = data.alerts || [];
|
var alerts = data.alerts || [];
|
||||||
|
var logs = this.logs || [];
|
||||||
|
|
||||||
return E('div', {}, [
|
return E('div', {}, [
|
||||||
this.renderHeader(status),
|
this.renderHeader(status),
|
||||||
@ -429,32 +433,78 @@ return view.extend({
|
|||||||
E('div', { 'class': 'cs-card-title' }, 'Recent Alerts'),
|
E('div', { 'class': 'cs-card-title' }, 'Recent Alerts'),
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'cs-card-body' }, this.renderAlertsTimeline(alerts))
|
E('div', { 'class': 'cs-card-body' }, this.renderAlertsTimeline(alerts))
|
||||||
])
|
]),
|
||||||
|
this.renderLogCard(logs)
|
||||||
]),
|
]),
|
||||||
|
|
||||||
this.renderBanModal()
|
this.renderBanModal()
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
render: function(payload) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.data = data;
|
this.data = payload[0] || {};
|
||||||
|
this.logs = (payload[1] && payload[1].entries) || [];
|
||||||
|
|
||||||
var view = E('div', { 'class': 'crowdsec-dashboard' }, [
|
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)
|
// Setup polling for auto-refresh (every 30 seconds)
|
||||||
poll.add(function() {
|
poll.add(function() {
|
||||||
return self.csApi.getDashboardData().then(function(newData) {
|
return self.refreshDashboard();
|
||||||
self.data = newData;
|
|
||||||
self.updateView();
|
|
||||||
});
|
|
||||||
}, 30);
|
}, 30);
|
||||||
|
|
||||||
return view;
|
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,
|
handleSaveApply: null,
|
||||||
handleSave: null,
|
handleSave: null,
|
||||||
handleReset: null
|
handleReset: null
|
||||||
|
|||||||
@ -6,6 +6,13 @@
|
|||||||
. /lib/functions.sh
|
. /lib/functions.sh
|
||||||
. /usr/share/libubox/jshn.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"
|
CSCLI="/usr/bin/cscli"
|
||||||
|
|
||||||
# Check if cscli exists
|
# Check if cscli exists
|
||||||
@ -137,6 +144,7 @@ add_ban() {
|
|||||||
result=$($CSCLI decisions add --ip "$ip" --duration "$duration" --reason "$reason" 2>&1)
|
result=$($CSCLI decisions add --ip "$ip" --duration "$duration" --reason "$reason" 2>&1)
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
|
secubox_log "CrowdSec ban added for $ip ($duration)"
|
||||||
echo '{"success": true}'
|
echo '{"success": true}'
|
||||||
else
|
else
|
||||||
json_init
|
json_init
|
||||||
@ -161,6 +169,7 @@ remove_ban() {
|
|||||||
result=$($CSCLI decisions delete --ip "$ip" 2>&1)
|
result=$($CSCLI decisions delete --ip "$ip" 2>&1)
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
|
secubox_log "CrowdSec ban removed for $ip"
|
||||||
echo '{"success": true}'
|
echo '{"success": true}'
|
||||||
else
|
else
|
||||||
json_init
|
json_init
|
||||||
@ -214,6 +223,31 @@ get_dashboard_stats() {
|
|||||||
json_dump
|
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
|
# Main dispatcher
|
||||||
case "$1" in
|
case "$1" in
|
||||||
list)
|
list)
|
||||||
@ -259,6 +293,12 @@ case "$1" in
|
|||||||
stats)
|
stats)
|
||||||
get_dashboard_stats
|
get_dashboard_stats
|
||||||
;;
|
;;
|
||||||
|
seccubox_logs)
|
||||||
|
seccubox_logs
|
||||||
|
;;
|
||||||
|
collect_debug)
|
||||||
|
collect_debug
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo '{"error": "Unknown method"}'
|
echo '{"error": "Unknown method"}'
|
||||||
;;
|
;;
|
||||||
|
|||||||
@ -47,6 +47,11 @@ Real-time system monitoring dashboard for OpenWrt with a modern, responsive inte
|
|||||||
- Animated gauges and sparklines
|
- Animated gauges and sparklines
|
||||||
- GitHub-inspired color palette
|
- 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
|
## Screenshots
|
||||||
|
|
||||||
### Real-time View
|
### Real-time View
|
||||||
|
|||||||
@ -96,6 +96,18 @@ var callStopNetdata = rpc.declare({
|
|||||||
expect: { success: false }
|
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) {
|
function formatBytes(bytes) {
|
||||||
if (!bytes || bytes === 0) return '0 B';
|
if (!bytes || bytes === 0) return '0 B';
|
||||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
@ -134,6 +146,8 @@ return baseclass.extend({
|
|||||||
restartNetdata: callRestartNetdata,
|
restartNetdata: callRestartNetdata,
|
||||||
startNetdata: callStartNetdata,
|
startNetdata: callStartNetdata,
|
||||||
stopNetdata: callStopNetdata,
|
stopNetdata: callStopNetdata,
|
||||||
|
getSecuboxLogs: callSecuboxLogs,
|
||||||
|
collectDebugSnapshot: callCollectDebug,
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
formatBytes: formatBytes,
|
formatBytes: formatBytes,
|
||||||
|
|||||||
@ -170,6 +170,96 @@
|
|||||||
color: var(--nd-text-muted);
|
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 */
|
/* Charts Grid */
|
||||||
.nd-charts-grid {
|
.nd-charts-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@ -4,13 +4,20 @@
|
|||||||
'require ui';
|
'require ui';
|
||||||
'require poll';
|
'require poll';
|
||||||
'require netdata-dashboard/api as API';
|
'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({
|
return view.extend({
|
||||||
load: function() {
|
load: function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
API.getNetdataStatus(),
|
API.getNetdataStatus(),
|
||||||
API.getNetdataAlarms(),
|
API.getNetdataAlarms(),
|
||||||
API.getStats()
|
API.getStats(),
|
||||||
|
API.getSecuboxLogs()
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -18,163 +25,162 @@ return view.extend({
|
|||||||
var netdataStatus = data[0] || {};
|
var netdataStatus = data[0] || {};
|
||||||
var alarms = data[1] || {};
|
var alarms = data[1] || {};
|
||||||
var stats = data[2] || {};
|
var stats = data[2] || {};
|
||||||
|
var logs = (data[3] && data[3].entries) || [];
|
||||||
|
|
||||||
var isRunning = netdataStatus.running || false;
|
var isRunning = netdataStatus.running || false;
|
||||||
var netdataUrl = netdataStatus.url || 'http://127.0.0.1:19999';
|
var netdataUrl = netdataStatus.url || 'http://127.0.0.1:19999';
|
||||||
|
var alarmCount = this.countAlarms(alarms);
|
||||||
|
|
||||||
// Count active alarms
|
var view = E('div', { 'class': 'netdata-dashboard secubox-netdata' }, [
|
||||||
var alarmCount = 0;
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||||||
if (alarms.alarms && typeof alarms.alarms === 'object') {
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('netdata-dashboard/dashboard.css') }),
|
||||||
Object.keys(alarms.alarms).forEach(function(key) {
|
this.renderHeader(netdataStatus, stats),
|
||||||
var alarm = alarms.alarms[key];
|
this.renderControls(isRunning),
|
||||||
if (alarm.status && alarm.status !== 'CLEAR') {
|
this.renderQuickStats(stats),
|
||||||
alarmCount++;
|
this.renderAlarmCard(alarmCount, netdataUrl),
|
||||||
}
|
this.renderLogCard(logs),
|
||||||
});
|
this.renderEmbed(isRunning, netdataUrl)
|
||||||
}
|
|
||||||
|
|
||||||
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'))
|
|
||||||
])
|
|
||||||
])
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Setup auto-refresh
|
// Setup auto-refresh
|
||||||
poll.add(L.bind(function() {
|
poll.add(L.bind(function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
API.getNetdataStatus(),
|
API.getNetdataStatus(),
|
||||||
API.getStats()
|
API.getStats(),
|
||||||
|
API.getSecuboxLogs()
|
||||||
]).then(L.bind(function(refreshData) {
|
]).then(L.bind(function(refreshData) {
|
||||||
// Could update stats display here
|
// Update quick stats/logs in place if needed
|
||||||
}, this));
|
}, this));
|
||||||
}, this), 5);
|
}, this), 5);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderStatCard: function(label, value, color) {
|
countAlarms: function(alarms) {
|
||||||
return E('div', {
|
var count = 0;
|
||||||
'style': 'background: white; border-left: 4px solid ' + color + '; padding: 1em; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);'
|
if (alarms.alarms && typeof alarms.alarms === 'object') {
|
||||||
}, [
|
Object.keys(alarms.alarms).forEach(function(key) {
|
||||||
E('div', { 'style': 'font-size: 0.85em; color: #666; margin-bottom: 0.25em;' }, label),
|
var alarm = alarms.alarms[key];
|
||||||
E('div', { 'style': 'font-size: 1.5em; font-weight: bold; color: ' + color + ';' }, value)
|
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,
|
handleSaveApply: null,
|
||||||
handleSave: null,
|
handleSave: null,
|
||||||
handleReset: null
|
handleReset: null
|
||||||
|
|||||||
@ -6,6 +6,13 @@
|
|||||||
. /lib/functions.sh
|
. /lib/functions.sh
|
||||||
. /usr/share/libubox/jshn.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 statistics
|
||||||
get_cpu() {
|
get_cpu() {
|
||||||
json_init
|
json_init
|
||||||
@ -525,6 +532,7 @@ restart_netdata() {
|
|||||||
if pgrep -x netdata >/dev/null 2>&1; then
|
if pgrep -x netdata >/dev/null 2>&1; then
|
||||||
json_add_boolean "success" 1
|
json_add_boolean "success" 1
|
||||||
json_add_string "message" "Netdata restarted successfully"
|
json_add_string "message" "Netdata restarted successfully"
|
||||||
|
secubox_log "Netdata service restarted"
|
||||||
else
|
else
|
||||||
json_add_boolean "success" 0
|
json_add_boolean "success" 0
|
||||||
json_add_string "error" "Failed to start Netdata"
|
json_add_string "error" "Failed to start Netdata"
|
||||||
@ -551,6 +559,7 @@ start_netdata() {
|
|||||||
if pgrep -x netdata >/dev/null 2>&1; then
|
if pgrep -x netdata >/dev/null 2>&1; then
|
||||||
json_add_boolean "success" 1
|
json_add_boolean "success" 1
|
||||||
json_add_string "message" "Netdata started successfully"
|
json_add_string "message" "Netdata started successfully"
|
||||||
|
secubox_log "Netdata service started"
|
||||||
else
|
else
|
||||||
json_add_boolean "success" 0
|
json_add_boolean "success" 0
|
||||||
json_add_string "error" "Failed to start Netdata"
|
json_add_string "error" "Failed to start Netdata"
|
||||||
@ -577,6 +586,7 @@ stop_netdata() {
|
|||||||
if ! pgrep -x netdata >/dev/null 2>&1; then
|
if ! pgrep -x netdata >/dev/null 2>&1; then
|
||||||
json_add_boolean "success" 1
|
json_add_boolean "success" 1
|
||||||
json_add_string "message" "Netdata stopped successfully"
|
json_add_string "message" "Netdata stopped successfully"
|
||||||
|
secubox_log "Netdata service stopped"
|
||||||
else
|
else
|
||||||
json_add_boolean "success" 0
|
json_add_boolean "success" 0
|
||||||
json_add_string "error" "Failed to stop Netdata"
|
json_add_string "error" "Failed to stop Netdata"
|
||||||
@ -589,10 +599,35 @@ stop_netdata() {
|
|||||||
json_dump
|
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
|
# Main dispatcher
|
||||||
case "$1" in
|
case "$1" in
|
||||||
list)
|
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)
|
call)
|
||||||
case "$2" in
|
case "$2" in
|
||||||
@ -638,6 +673,12 @@ case "$1" in
|
|||||||
stop_netdata)
|
stop_netdata)
|
||||||
stop_netdata
|
stop_netdata
|
||||||
;;
|
;;
|
||||||
|
seccubox_logs)
|
||||||
|
seccubox_logs
|
||||||
|
;;
|
||||||
|
collect_debug)
|
||||||
|
collect_debug
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo '{"error": "Unknown method"}'
|
echo '{"error": "Unknown method"}'
|
||||||
;;
|
;;
|
||||||
|
|||||||
@ -46,6 +46,18 @@ var callStats = rpc.declare({
|
|||||||
expect: { }
|
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) {
|
function formatBytes(bytes) {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
var k = 1024;
|
var k = 1024;
|
||||||
@ -61,6 +73,8 @@ return baseclass.extend({
|
|||||||
getHosts: callHosts,
|
getHosts: callHosts,
|
||||||
getProtocols: callProtocols,
|
getProtocols: callProtocols,
|
||||||
getStats: callStats,
|
getStats: callStats,
|
||||||
|
getSecuboxLogs: callSecuboxLogs,
|
||||||
|
collectDebugSnapshot: callCollectDebug,
|
||||||
formatBytes: formatBytes,
|
formatBytes: formatBytes,
|
||||||
|
|
||||||
// Aggregate function for overview page
|
// Aggregate function for overview page
|
||||||
|
|||||||
@ -368,6 +368,48 @@
|
|||||||
color: var(--nf-text-primary);
|
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 */
|
/* Flow Table */
|
||||||
.nf-table-container {
|
.nf-table-container {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|||||||
@ -9,7 +9,10 @@ return view.extend({
|
|||||||
title: _('Netifyd Dashboard'),
|
title: _('Netifyd Dashboard'),
|
||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
return api.getAllData();
|
return Promise.all([
|
||||||
|
api.getAllData(),
|
||||||
|
api.getSecuboxLogs()
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderDonut: function(data, size) {
|
renderDonut: function(data, size) {
|
||||||
@ -47,8 +50,10 @@ return view.extend({
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
render: function(payload) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
var data = payload[0] || {};
|
||||||
|
var logEntries = (payload[1] && payload[1].entries) || [];
|
||||||
var status = data.status || {};
|
var status = data.status || {};
|
||||||
var stats = data.stats || {};
|
var stats = data.stats || {};
|
||||||
var apps = (data.applications || {}).applications || [];
|
var apps = (data.applications || {}).applications || [];
|
||||||
@ -63,7 +68,6 @@ return view.extend({
|
|||||||
{ name: 'UDP', value: udpFlows },
|
{ name: 'UDP', value: udpFlows },
|
||||||
{ name: 'Other', value: Math.max(0, totalFlows - tcpFlows - udpFlows) }
|
{ name: 'Other', value: Math.max(0, totalFlows - tcpFlows - udpFlows) }
|
||||||
].filter(function(p) { return p.value > 0; });
|
].filter(function(p) { return p.value > 0; });
|
||||||
|
|
||||||
var topApps = apps.slice(0, 6);
|
var topApps = apps.slice(0, 6);
|
||||||
var maxAppBytes = topApps.length > 0 ? Math.max.apply(null, topApps.map(function(a) { return a.bytes; })) : 1;
|
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
|
// Include CSS
|
||||||
@ -196,6 +202,39 @@ return view.extend({
|
|||||||
return view;
|
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,
|
handleSaveApply: null,
|
||||||
handleSave: null,
|
handleSave: null,
|
||||||
handleReset: null
|
handleReset: null
|
||||||
|
|||||||
@ -6,6 +6,13 @@
|
|||||||
. /lib/functions.sh
|
. /lib/functions.sh
|
||||||
. /usr/share/libubox/jshn.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_SOCKET="/var/run/netifyd/netifyd.sock"
|
||||||
NETIFYD_STATUS="/var/run/netifyd/status.json"
|
NETIFYD_STATUS="/var/run/netifyd/status.json"
|
||||||
NETIFYD_FLOWS="/var/run/netifyd/flows.json"
|
NETIFYD_FLOWS="/var/run/netifyd/flows.json"
|
||||||
@ -265,6 +272,31 @@ get_devices() {
|
|||||||
json_dump
|
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 overall statistics
|
||||||
get_stats() {
|
get_stats() {
|
||||||
json_init
|
json_init
|
||||||
@ -463,7 +495,7 @@ get_dns_queries() {
|
|||||||
# Main dispatcher
|
# Main dispatcher
|
||||||
case "$1" in
|
case "$1" in
|
||||||
list)
|
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)
|
call)
|
||||||
case "$2" in
|
case "$2" in
|
||||||
@ -497,6 +529,12 @@ case "$1" in
|
|||||||
dns_queries)
|
dns_queries)
|
||||||
get_dns_queries
|
get_dns_queries
|
||||||
;;
|
;;
|
||||||
|
seccubox_logs)
|
||||||
|
seccubox_logs
|
||||||
|
;;
|
||||||
|
collect_debug)
|
||||||
|
collect_debug
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo '{"error": "Unknown method"}'
|
echo '{"error": "Unknown method"}'
|
||||||
;;
|
;;
|
||||||
|
|||||||
87
luci-app-secubox/root/usr/sbin/secubox-log
Executable file
87
luci-app-secubox/root/usr/sbin/secubox-log
Executable 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
|
||||||
@ -120,6 +120,25 @@ opkg install /tmp/luci-app-system-hub*.ipk
|
|||||||
/etc/init.d/rpcd restart
|
/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:**
|
**Example Workflow - Firmware Building:**
|
||||||
```bash
|
```bash
|
||||||
# 1. Build firmware for MOCHAbin with SecuBox pre-installed
|
# 1. Build firmware for MOCHAbin with SecuBox pre-installed
|
||||||
|
|||||||
91
secubox-tools/secubox-log.sh
Executable file
91
secubox-tools/secubox-log.sh
Executable 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
|
||||||
Loading…
Reference in New Issue
Block a user