Files
luci/modules/luci-mod-status/htdocs/luci-static/resources/view/status/dmesg.js
T
Paul Donald 0b9e8b4f52 luci-mod-status: correctness fixes
Signed-off-by: Paul Donald <newtwen+github@gmail.com>
2026-05-27 15:49:46 +03:00

298 lines
8.1 KiB
JavaScript

'use strict';
'require view';
'require fs';
'require poll';
'require ui';
return view.extend({
logFilterFrom: '0',
logFilterTo: '',
invertLogRangeFilter: false,
minSeverity: '',
invertMinSeverity: false,
sortLogsDescending: false,
logTextFilter: '',
invertLogTextSearch: false,
severity: [
['', 'KERN_DEFAULT', _('Default')],
// ['0', 'KERN_EMERG'], // unusable kernels tend to halt ergo no running system
['1', 'KERN_ALERT', _('Alert')],
['2', 'KERN_CRIT', _('Critical')],
['3', 'KERN_ERR', _('Error')],
['4', 'KERN_WARNING', _('Warning')],
['5', 'KERN_NOTICE', _('Notice')],
['6', 'KERN_INFO', _('Info')],
['7', 'KERN_DEBUG', _('Debug')],
// ['c', 'KERN_CONT'], // for follow-on printed lines lacking newline
/*
As of 24.10 there appear to be kernel log lines printed with severity 14-15
which seems like a bug in ubox. So we must structure the filter in an
'at least' fashion to include those.
*/
],
async retrieveLog() {
return fs.exec_direct('/bin/dmesg', [ '-r' ]).then(logdata => {
let loglines = [];
let lastSeverity = null;
let lastTime = null;
logdata.trim().split(/\n/).forEach(line => {
const priorityMatch = line.match(/^<(\w+)>/);
if (!priorityMatch) return;
const tag = priorityMatch[1];
const isCont = tag === 'c';
const cleanLine = line.replace(/^<\w+>/, '');
const timeMatch = cleanLine.match(/^\[\s*(\d+(?:\.\d+)?)\]/);
const time = timeMatch ? parseFloat(timeMatch[1]) : null;
if (time != null)
lastTime = time;
if (!isCont) {
lastSeverity = parseInt(tag, 10); // update severity
}
loglines.push({
severity: isCont ? lastSeverity : parseInt(tag, 10),
isCont,
time: time != null ? time : lastTime,
text: cleanLine
});
});
// Filter by time
const hasStart = this.logFilterFrom;
const hasEnd = this.logFilterTo;
if (hasStart || hasEnd) {
loglines = loglines.filter(({ time }) => {
if (time == null) return false;
let inRange = true;
if (hasStart && hasEnd)
inRange = time >= this.logFilterFrom && time <= this.logFilterTo;
else if (hasStart)
inRange = time >= this.logFilterFrom;
else if (hasEnd)
inRange = time <= this.logFilterTo;
return this.invertLogRangeFilter ? !inRange : inRange;
});
}
// Filter by severity
loglines = loglines.filter(entry => {
if (this.invertMinSeverity)
return entry.severity < this.minSeverity;
else
return entry.severity >= this.minSeverity;
});
// Filter by text
if (this.logTextFilter) {
loglines = loglines.filter(({ text }) => {
const match = text.includes(this.logTextFilter);
return this.invertLogTextSearch ? !match : match;
});
}
// Sort by time
if (this.sortLogsDescending) loglines.reverse();
return {
value: loglines.map(l => l.text).join('\n'),
rows: loglines.length + 1
};
}).catch(function(err) {
ui.addNotification(null, E('p', {}, _('Unable to load log data: ' + err.message)));
return '';
});
},
async pollLog() {
const element = document.getElementById('syslog');
if (element) {
const log = await this.retrieveLog();
element.value = log.value;
element.rows = log.rows;
}
},
async load() {
poll.add(this.pollLog.bind(this));
return await this.retrieveLog();
},
render(loglines) {
const scrollDownButton = E('button', {
'id': 'scrollDownButton',
'class': 'cbi-button cbi-button-neutral',
}, _('Scroll to tail', 'scroll to bottom (the tail) of the log file')
);
scrollDownButton.addEventListener('click', () => {
scrollUpButton.scrollIntoView();
scrollDownButton.blur();
});
const scrollUpButton = E('button', {
'id' : 'scrollUpButton',
'class': 'cbi-button cbi-button-neutral',
}, _('Scroll to head', 'scroll to top (the head) of the log file')
);
scrollUpButton.addEventListener('click', () => {
scrollDownButton.scrollIntoView();
scrollUpButton.blur();
});
const self = this;
// Create range invert checkbox
const rangeTimeInvert = E('input', {
'id': 'invertLogRangeTime',
'type': 'checkbox',
'class': 'cbi-input-checkbox',
});
// Create from time filter
const fromTimeFilter = E('input', {
'id': 'logFromTime',
'class': 'cbi-input-text',
'style': 'margin-bottom:10px',
'type': 'number',
'min': '0',
'step': '0.1',
'placeholder': '0.000000',
});
// Create to time filter
const toTimeFilter = E('input', {
'id': 'logToTime',
'class': 'cbi-input-text',
'style': 'margin-bottom:10px',
'type': 'number',
'min': '0',
'step': '0.1',
'placeholder': '0.000000',
});
// Create range invert checkbox
const severityInvert = E('input', {
'id': 'invertSeverity',
'type': 'checkbox',
'class': 'cbi-input-checkbox',
});
// Create severity select-dropdown from severity map
const severitySelect = E('select', {
'id': 'logSeveritySelect',
'class': 'cbi-input-select',
},
this.severity.map(([val, , label]) =>
E('option', { value: val }, label)
));
// Create range invert checkbox
const descendingSort = E('input', {
'id': 'invertAscendingSort',
'type': 'checkbox',
'class': 'cbi-input-checkbox',
});
// Create raw text search invert checkbox
const filterTextInvert = E('input', {
'id': 'invertLogTextSearch',
'type': 'checkbox',
'class': 'cbi-input-checkbox',
});
// Create raw text search text input
const filterTextInput = E('input', {
'id': 'logTextFilter',
'class': 'cbi-input-text',
});
function handleLogFilterChange() {
// time
self.invertLogRangeFilter = rangeTimeInvert.checked;
self.logFilterFrom = fromTimeFilter.value;
self.logFilterTo = toTimeFilter.value;
// severity
self.minSeverity = severitySelect.value;
self.invertMinSeverity = severityInvert.checked;
// sort
self.sortLogsDescending = descendingSort.checked;
// text
self.logTextFilter = filterTextInput.value;
self.invertLogTextSearch = filterTextInvert.checked;
self.pollLog();
}
// time
rangeTimeInvert.addEventListener('change', handleLogFilterChange);
fromTimeFilter.addEventListener('input', handleLogFilterChange);
toTimeFilter.addEventListener('input', handleLogFilterChange);
// severity
severitySelect.addEventListener('change', handleLogFilterChange);
severityInvert.addEventListener('change', handleLogFilterChange);
// sort
descendingSort.addEventListener('change', handleLogFilterChange);
// text
filterTextInput.addEventListener('input', handleLogFilterChange);
filterTextInvert.addEventListener('change', handleLogFilterChange);
return E([], [
E('h2', {}, [ _('Kernel Log') ]),
E('div', { 'id': 'content_syslog' }, [
E('div', { 'style': 'margin-bottom:10px' }, [
E('label', { 'for': 'invertLogFacilitySearch', 'style': 'margin-right:5px' }, _('Not')),
rangeTimeInvert,
E('label', { 'for': 'logFacilitySelect', 'style': 'margin: 0 5px' }, _('between:')),
fromTimeFilter,
E('label', { 'for': 'logSeveritySelect', 'style': 'margin: 0 5px' }, _('and:')),
toTimeFilter,
]),
E('div', { 'style': 'margin-bottom:10px' }, [
E('label', { 'for': 'invertLogSeveritySearch', 'style': 'margin-right:5px' }, _('Not')),
severityInvert,
'\xa0',
severitySelect,
E('label', { 'for': 'logSeveritySelect', 'style': 'margin: 0 5px' }, _('and above')),
]),
E('div', { 'style': 'margin-bottom:10px' }, [
E('label', { 'for': 'invertAscendingSort', 'style': 'margin-right:5px' }, _('Reverse sort')),
descendingSort,
]),
E('div', { 'style': 'margin-bottom:10px' }, [
E('label', { 'for': 'invertLogTextSearch', 'style': 'margin-right:5px' }, _('Not')),
filterTextInvert,
E('label', { 'for': 'logTextFilter', 'style': 'margin: 0 5px' }, _('including:')),
filterTextInput,
]),
E('div', {'style': 'padding-bottom: 20px'}, [scrollDownButton]),
E('textarea', {
'id': 'syslog',
'style': 'font-size:12px',
'readonly': 'readonly',
'wrap': 'off',
'rows': loglines.rows
}, [ loglines.value ]),
E('div', {'style': 'padding-bottom: 20px'}, [scrollUpButton])
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});