Major Enhancements: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Network Modes Module: - Added 3 new network modes: * Double NAT mode (doublenat.js) - Cascaded router configuration * Multi-WAN mode (multiwan.js) - Load balancing and failover * VPN Relay mode (vpnrelay.js) - VPN gateway configuration - Enhanced existing modes: * Access Point improvements * Travel mode refinements * Router mode enhancements * Relay mode updates * Sniffer mode optimizations - Updated wizard with new mode options - Enhanced API with new mode support - Improved dashboard CSS styling - Updated helpers for new modes - Extended RPCD backend functionality - Updated menu structure for new modes - Enhanced UCI configuration System Hub Module: - Added dedicated logs.css stylesheet - Enhanced logs.js view with better styling - Improved overview.css responsive design - Enhanced services.css for better UX - Updated overview.js with theme integration - Improved services.js layout SecuBox Dashboard: - Enhanced dashboard.css with theme variables - Improved dashboard.js responsiveness - Better integration with global theme Files Changed: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Network Modes (17 files): Modified: api.js, dashboard.css, helpers.js, menu, config, RPCD backend Modified Views: accesspoint, overview, relay, router, sniffer, travel, wizard New Views: doublenat, multiwan, vpnrelay System Hub (6 files): New: logs.css Modified: overview.css, services.css, logs.js, overview.js, services.js SecuBox (2 files): Modified: dashboard.css, dashboard.js Total: 25 files changed (21 modified, 4 new) Technical Improvements: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Global theme CSS variable usage - Responsive design enhancements - Improved error handling - Better mode validation - Enhanced user feedback - Optimized CSS performance - Improved accessibility Network Mode Capabilities: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. Router Mode - Standard routing 2. Access Point Mode - WiFi AP with bridge 3. Relay Mode - WiFi repeater/extender 4. Travel Mode - Portable router configuration 5. Sniffer Mode - Network monitoring 6. Double NAT Mode - Cascaded NAT for network isolation (NEW) 7. Multi-WAN Mode - Multiple uplinks with load balancing (NEW) 8. VPN Relay Mode - VPN gateway and tunnel endpoint (NEW) Breaking Changes: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ None - All changes are backward compatible 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
290 lines
8.4 KiB
JavaScript
290 lines
8.4 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require ui';
|
|
'require dom';
|
|
'require poll';
|
|
'require system-hub/api as API';
|
|
'require secubox-theme/theme as Theme';
|
|
|
|
Theme.init({ theme: 'dark', language: 'en' });
|
|
|
|
return view.extend({
|
|
logs: [],
|
|
lineCount: 200,
|
|
autoRefresh: true,
|
|
autoScroll: true,
|
|
searchQuery: '',
|
|
severityFilter: 'all',
|
|
|
|
load: function() {
|
|
return API.getLogs(this.lineCount, '');
|
|
},
|
|
|
|
render: function(data) {
|
|
this.logs = (data && data.logs) || [];
|
|
|
|
var container = E('div', { 'class': 'sh-logs-view' }, [
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/logs.css') }),
|
|
this.renderHero(),
|
|
this.renderControls(),
|
|
this.renderBody()
|
|
]);
|
|
|
|
this.updateLogStream();
|
|
this.updateStats();
|
|
|
|
var self = this;
|
|
poll.add(function() {
|
|
if (!self.autoRefresh) return;
|
|
return API.getLogs(self.lineCount, '').then(function(result) {
|
|
self.logs = (result && result.logs) || [];
|
|
self.updateStats();
|
|
self.updateLogStream();
|
|
});
|
|
}, 5);
|
|
|
|
return container;
|
|
},
|
|
|
|
renderHero: function() {
|
|
return E('section', { 'class': 'sh-logs-hero' }, [
|
|
E('div', {}, [
|
|
E('h1', {}, _('System Logs Live Stream')),
|
|
E('p', {}, _('Follow kernel, service, and security events in real time'))
|
|
]),
|
|
E('div', { 'class': 'sh-log-stats', 'id': 'sh-log-stats' }, [
|
|
this.createStat('sh-log-total', _('Lines'), this.logs.length),
|
|
this.createStat('sh-log-errors', _('Errors'), this.countSeverity('error'), 'danger'),
|
|
this.createStat('sh-log-warnings', _('Warning'), this.countSeverity('warning'), 'warn')
|
|
])
|
|
]);
|
|
},
|
|
|
|
createStat: function(id, label, value, tone) {
|
|
var cls = 'sh-log-stat';
|
|
if (tone) cls += ' ' + tone;
|
|
return E('div', { 'class': cls, 'id': id }, [
|
|
E('span', { 'class': 'label' }, label),
|
|
E('span', { 'class': 'value' }, value)
|
|
]);
|
|
},
|
|
|
|
renderControls: function() {
|
|
var self = this;
|
|
return E('div', { 'class': 'sh-log-controls' }, [
|
|
E('div', { 'class': 'sh-log-search' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'placeholder': _('Search keywords, services, IPs...'),
|
|
'input': function(ev) {
|
|
self.searchQuery = (ev.target.value || '').toLowerCase();
|
|
self.updateLogStream();
|
|
}
|
|
})
|
|
]),
|
|
E('div', { 'class': 'sh-log-selectors' }, [
|
|
E('select', {
|
|
'change': function(ev) {
|
|
self.lineCount = parseInt(ev.target.value, 10);
|
|
self.refreshLogs();
|
|
}
|
|
}, [
|
|
E('option', { 'value': '100' }, '100 lines'),
|
|
E('option', { 'value': '200', 'selected': 'selected' }, '200 lines'),
|
|
E('option', { 'value': '500' }, '500 lines'),
|
|
E('option', { 'value': '1000' }, '1000 lines')
|
|
]),
|
|
E('div', { 'class': 'sh-toggle-group' }, [
|
|
this.renderToggle(_('Auto Refresh'), this.autoRefresh, function(enabled) {
|
|
self.autoRefresh = enabled;
|
|
}),
|
|
this.renderToggle(_('Auto Scroll'), this.autoScroll, function(enabled) {
|
|
self.autoScroll = enabled;
|
|
})
|
|
]),
|
|
E('button', {
|
|
'class': 'sh-btn sh-btn-primary',
|
|
'type': 'button',
|
|
'click': ui.createHandlerFn(this, 'downloadLogs')
|
|
}, '⬇ ' + _('Export'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderToggle: function(label, state, handler) {
|
|
return E('label', { 'class': 'sh-toggle' }, [
|
|
E('input', {
|
|
'type': 'checkbox',
|
|
'checked': state ? 'checked' : null,
|
|
'change': function(ev) {
|
|
handler(ev.target.checked);
|
|
}
|
|
}),
|
|
E('span', {}, label)
|
|
]);
|
|
},
|
|
|
|
renderBody: function() {
|
|
return E('div', { 'class': 'sh-logs-body' }, [
|
|
E('div', { 'class': 'sh-log-panel' }, [
|
|
this.renderSeverityTabs(),
|
|
E('div', { 'class': 'sh-log-stream', 'id': 'sh-log-stream' })
|
|
]),
|
|
E('div', { 'class': 'sh-log-side' }, [
|
|
E('h3', {}, _('Statistics (recent)')),
|
|
E('ul', { 'id': 'sh-log-metrics' },
|
|
this.buildMetrics().map(function(metric) {
|
|
return E('li', {}, [
|
|
E('span', {}, metric.label),
|
|
E('strong', {}, metric.value)
|
|
]);
|
|
}))
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderSeverityTabs: function() {
|
|
var self = this;
|
|
var filters = [
|
|
{ id: 'all', label: _('All') },
|
|
{ id: 'error', label: _('Errors') },
|
|
{ id: 'warning', label: _('Warnings') },
|
|
{ id: 'info', label: _('Info') }
|
|
];
|
|
|
|
return E('div', { 'class': 'sh-log-filters', 'id': 'sh-log-filters' },
|
|
filters.map(function(filter) {
|
|
return E('button', {
|
|
'type': 'button',
|
|
'class': 'sh-log-filter' + (self.severityFilter === filter.id ? ' active' : ''),
|
|
'click': function() {
|
|
self.severityFilter = filter.id;
|
|
self.updateLogStream();
|
|
self.refreshSeverityTabs();
|
|
}
|
|
}, filter.label);
|
|
}));
|
|
},
|
|
|
|
refreshSeverityTabs: function() {
|
|
var tabs = document.querySelectorAll('.sh-log-filter');
|
|
var ids = ['all', 'error', 'warning', 'info'];
|
|
tabs.forEach(function(tab, idx) {
|
|
if (ids[idx] === this.severityFilter) tab.classList.add('active');
|
|
else tab.classList.remove('active');
|
|
}, this);
|
|
},
|
|
|
|
getFilteredLogs: function() {
|
|
return this.logs.filter(function(line) {
|
|
if (!line) return false;
|
|
var severity = this.detectSeverity(line);
|
|
var matchesSeverity = this.severityFilter === 'all' || severity === this.severityFilter;
|
|
var matchesSearch = !this.searchQuery || line.toLowerCase().indexOf(this.searchQuery) !== -1;
|
|
return matchesSeverity && matchesSearch;
|
|
}, this);
|
|
},
|
|
|
|
updateLogStream: function() {
|
|
var container = document.getElementById('sh-log-stream');
|
|
if (!container) return;
|
|
var filtered = this.getFilteredLogs();
|
|
var frag = filtered.map(function(line, idx) {
|
|
var severity = this.detectSeverity(line);
|
|
return E('div', { 'class': 'sh-log-line ' + severity }, [
|
|
E('span', { 'class': 'sh-log-index' }, idx + 1),
|
|
E('span', { 'class': 'sh-log-message' }, line)
|
|
]);
|
|
}, this);
|
|
dom.content(container, frag);
|
|
if (this.autoScroll) {
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
var badge = document.getElementById('sh-log-total');
|
|
if (badge) badge.querySelector('.value').textContent = filtered.length;
|
|
},
|
|
|
|
updateStats: function() {
|
|
var stats = {
|
|
'sh-log-total': this.logs.length,
|
|
'sh-log-errors': this.countSeverity('error'),
|
|
'sh-log-warnings': this.countSeverity('warning')
|
|
};
|
|
|
|
Object.keys(stats).forEach(function(id) {
|
|
var node = document.getElementById(id);
|
|
if (node) {
|
|
var span = node.querySelector('.value');
|
|
if (span) span.textContent = stats[id];
|
|
}
|
|
});
|
|
|
|
var list = document.getElementById('sh-log-metrics');
|
|
if (list) {
|
|
dom.content(list, this.buildMetrics().map(function(metric) {
|
|
return E('li', {}, [E('span', {}, metric.label), E('strong', {}, metric.value)]);
|
|
}));
|
|
}
|
|
},
|
|
|
|
buildMetrics: function() {
|
|
return [
|
|
{ label: _('Critical events (last refresh)'), value: this.countSeverity('error') },
|
|
{ label: _('Warnings'), value: this.countSeverity('warning') },
|
|
{ label: _('Info/Debug'), value: this.countSeverity('info') },
|
|
{ label: _('Matched search'), value: this.getFilteredLogs().length }
|
|
];
|
|
},
|
|
|
|
refreshLogs: function() {
|
|
var self = this;
|
|
ui.showModal(_('Loading logs...'), [
|
|
E('p', { 'class': 'spinning' }, _('Fetching system logs'))
|
|
]);
|
|
return API.getLogs(this.lineCount, '').then(function(result) {
|
|
ui.hideModal();
|
|
self.logs = (result && result.logs) || [];
|
|
self.updateStats();
|
|
self.updateLogStream();
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
|
});
|
|
},
|
|
|
|
detectSeverity: function(line) {
|
|
if (!line) return 'info';
|
|
var lc = line.toLowerCase();
|
|
if (lc.indexOf('error') !== -1 || lc.indexOf('fatal') !== -1 || lc.indexOf('crit') !== -1) return 'error';
|
|
if (lc.indexOf('warn') !== -1 || lc.indexOf('notice') !== -1) return 'warning';
|
|
return 'info';
|
|
},
|
|
|
|
countSeverity: function(level) {
|
|
return this.logs.filter(function(line) {
|
|
return this.detectSeverity(line) === level;
|
|
}, this).length;
|
|
},
|
|
|
|
downloadLogs: function() {
|
|
var blob = new Blob([this.getFilteredLogs().join('\n')], { type: 'text/plain' });
|
|
var url = URL.createObjectURL(blob);
|
|
var a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'system-logs.txt';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
},
|
|
|
|
updateLogStreamDebounced: function() {
|
|
clearTimeout(this._updateTimer);
|
|
var self = this;
|
|
this._updateTimer = setTimeout(function() {
|
|
self.updateLogStream();
|
|
}, 200);
|
|
}
|
|
});
|