secubox-openwrt/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js
CyberMind-FR e90cf85f69 feat: implement working system logs viewer in system-hub
Fixed and enhanced the system logs functionality with real-time log viewing
and color-coded log levels.

🔧 Backend Fix (RPCD)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Problem:
- get_logs() function returned empty array
- Logs were piped through while loop creating a subshell
- json_add_string in subshell didn't affect main JSON output

Solution:
- Use temporary file to collect logs
- Read from file outside of subshell
- Properly build JSON array with all log lines

Implementation:
```sh
# Store logs in temporary file
logread | tail -n "$lines" > "$tmpfile"

# Read from file (no subshell issue)
while IFS= read -r line; do
    json_add_string "" "$line"
done < "$tmpfile"
```

Testing:
 ubus call luci.system-hub get_logs '{"lines": 100}' → Returns real logs
 Filter parameter works: get_logs '{"filter": "error"}'
 Temporary files cleaned up after use

🎨 Frontend Enhancement (logs.js)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Added color-coded log level display:

Features:
1. **Color-coded by severity:**
   - 🔴 Errors (red): error, err, fatal, crit
   - 🟡 Warnings (orange): warn, warning
   - 🔵 Info (blue): info, notice
   - 🟣 Debug (purple): debug
   - 🟢 Success (green): success, ok

2. **Visual enhancements:**
   - Colored left border for each log line
   - Semi-transparent background matching log level
   - JetBrains Mono font for better readability
   - Proper line spacing and padding

3. **Empty state:**
   - Friendly message when no logs available
   - Different message for empty search results
   - Large icon for better UX

Implementation:
```javascript
renderLogLine: function(line) {
    // Detect log level from keywords
    if (line.includes('error')) {
        color = '#ef4444';  // Red
        bgColor = 'rgba(239, 68, 68, 0.1)';
    }
    // ... other levels

    return E('div', {
        'style': 'border-left: 3px solid ' + color + '; ...'
    }, line);
}
```

📋 Features Summary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 Real-time system log viewing (logread integration)
 Filter by log level (all, errors, warnings, info)
 Search functionality (filter text in logs)
 Adjustable line count (50, 100, 200, 500, 1000 lines)
 Color-coded log levels for easy identification
 Refresh button to reload logs
 Download logs as .txt file
 Statistics: total lines, errors count, warnings count
 Beautiful empty state when no logs match

🚀 Deployment
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 Updated RPCD backend on router
 Updated frontend logs.js on router
 Permissions fixed (755 for RPCD, 644 for JS)
 Services restarted (rpcd, uhttpd)
 Tested and working

Test URL: https://192.168.8.191/cgi-bin/luci/admin/secubox/system/system-hub/logs

Files Modified:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Backend:
* luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub
  - Fixed get_logs() to return real logs (+12 lines refactor)

Frontend:
* luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js
  - Added renderLogLine() for color-coded display (+27 lines)
  - Enhanced updateLogDisplay() with empty state (+18 lines)
  - JetBrains Mono font integration

🎯 User Experience
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Before:
 Empty log viewer (backend returned [])
 Plain text display
 No visual distinction between log levels

After:
 Real system logs displayed
 Color-coded by severity level
 Easy to spot errors and warnings
 Professional terminal-like appearance
 Fully functional filters and search

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 21:07:16 +01:00

341 lines
10 KiB
JavaScript

'use strict';
'require view';
'require ui';
'require dom';
'require system-hub/api as API';
'require system-hub/theme as Theme';
return view.extend({
logs: [],
currentFilter: 'all',
searchQuery: '',
lineCount: 100,
load: function() {
return Promise.all([
API.getLogs(100, ''),
Theme.getTheme()
]);
},
render: function(data) {
this.logs = data[0] || [];
var theme = data[1];
var container = E('div', { 'class': 'system-hub-logs' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
// Header
this.renderHeader(),
// Controls
this.renderControls(),
// Filter tabs
this.renderFilterTabs(),
// Log viewer
E('div', { 'class': 'sh-card' }, [
E('div', { 'class': 'sh-card-header' }, [
E('h3', { 'class': 'sh-card-title' }, [
E('span', { 'class': 'sh-card-title-icon' }, '📟'),
'Log Output'
]),
E('div', { 'class': 'sh-card-badge' }, this.getFilteredLogs().length + ' lines')
]),
E('div', { 'class': 'sh-card-body', 'style': 'padding: 0;' }, [
E('div', { 'id': 'log-container' })
])
])
]);
// Initial render
this.updateLogDisplay();
return container;
},
renderHeader: function() {
var stats = this.getLogStats();
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '📋'),
'System Logs'
]),
E('p', { 'class': 'sh-page-subtitle' },
'View and filter system logs in real-time')
]),
E('div', { 'class': 'sh-stats-grid' }, [
E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, stats.total),
E('div', { 'class': 'sh-stat-label' }, 'Total Lines')
]),
E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value', 'style': 'color: #ef4444;' }, stats.errors),
E('div', { 'class': 'sh-stat-label' }, 'Errors')
]),
E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value', 'style': 'color: #f59e0b;' }, stats.warnings),
E('div', { 'class': 'sh-stat-label' }, 'Warnings')
])
])
]);
},
renderControls: function() {
var self = this;
return E('div', { 'style': 'display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap;' }, [
// Search box
E('input', {
'type': 'text',
'class': 'cbi-input-text',
'placeholder': '🔍 Search logs...',
'style': 'flex: 1; min-width: 250px; padding: 12px 16px; border-radius: 8px; border: 1px solid var(--sh-border); background: var(--sh-bg-card); color: var(--sh-text-primary); font-size: 14px;',
'input': function(ev) {
self.searchQuery = ev.target.value.toLowerCase();
self.updateLogDisplay();
}
}),
// Line count selector
E('select', {
'class': 'cbi-input-select',
'style': 'padding: 12px 16px; border-radius: 8px; border: 1px solid var(--sh-border); background: var(--sh-bg-card); color: var(--sh-text-primary); font-size: 14px;',
'change': function(ev) {
self.lineCount = parseInt(ev.target.value);
self.refreshLogs();
}
}, [
E('option', { 'value': '50' }, '50 lines'),
E('option', { 'value': '100', 'selected': '' }, '100 lines'),
E('option', { 'value': '200' }, '200 lines'),
E('option', { 'value': '500' }, '500 lines'),
E('option', { 'value': '1000' }, '1000 lines')
]),
// Refresh button
E('button', {
'class': 'sh-btn sh-btn-primary',
'click': L.bind(this.refreshLogs, this)
}, [
E('span', {}, '🔄'),
E('span', {}, 'Refresh')
]),
// Download button
E('button', {
'class': 'sh-btn sh-btn-secondary',
'click': function() {
self.downloadLogs();
}
}, [
E('span', {}, '📥'),
E('span', {}, 'Download')
])
]);
},
renderFilterTabs: function() {
var stats = this.getLogStats();
return E('div', { 'class': 'sh-filter-tabs' }, [
this.createFilterTab('all', '📋', 'All Logs', stats.total),
this.createFilterTab('error', '🔴', 'Errors', stats.errors),
this.createFilterTab('warning', '🟡', 'Warnings', stats.warnings),
this.createFilterTab('info', '🔵', 'Info', stats.info)
]);
},
createFilterTab: function(filter, icon, label, count) {
var self = this;
var isActive = this.currentFilter === filter;
return E('div', {
'class': 'sh-filter-tab' + (isActive ? ' active' : ''),
'click': function() {
self.currentFilter = filter;
self.updateFilterTabs();
self.updateLogDisplay();
}
}, [
E('span', { 'class': 'sh-tab-icon' }, icon),
E('span', { 'class': 'sh-tab-label' }, label + ' (' + count + ')')
]);
},
updateFilterTabs: function() {
var tabs = document.querySelectorAll('.sh-filter-tab');
tabs.forEach(function(tab, index) {
var filters = ['all', 'error', 'warning', 'info'];
if (filters[index] === this.currentFilter) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
}.bind(this));
},
updateLogDisplay: function() {
var container = document.getElementById('log-container');
if (!container) return;
var filtered = this.getFilteredLogs();
if (filtered.length === 0) {
dom.content(container, [
E('div', {
'style': 'padding: 60px 20px; text-align: center; color: var(--sh-text-secondary);'
}, [
E('div', { 'style': 'font-size: 48px; margin-bottom: 16px;' }, '📋'),
E('div', { 'style': 'font-size: 16px; font-weight: 500;' }, 'No logs available'),
E('div', { 'style': 'font-size: 14px; margin-top: 8px;' },
this.searchQuery ? 'Try a different search query' : 'System logs will appear here')
])
]);
} else {
// Render colored log lines
var logLines = filtered.map(function(line) {
return this.renderLogLine(line);
}.bind(this));
dom.content(container, [
E('div', {
'style': 'background: var(--sh-bg-tertiary); padding: 20px; overflow: auto; max-height: 600px; font-size: 12px; font-family: "JetBrains Mono", "Courier New", monospace; line-height: 1.6;'
}, logLines)
]);
}
// Update badge
var badge = document.querySelector('.sh-card-badge');
if (badge) {
badge.textContent = filtered.length + ' lines';
}
},
renderLogLine: function(line) {
var lineLower = line.toLowerCase();
var color = 'var(--sh-text-primary)';
var bgColor = 'transparent';
// Detect log level and apply color
if (lineLower.includes('error') || lineLower.includes('err') || lineLower.includes('fatal') || lineLower.includes('crit')) {
color = '#ef4444'; // Red for errors
bgColor = 'rgba(239, 68, 68, 0.1)';
} else if (lineLower.includes('warn') || lineLower.includes('warning')) {
color = '#f59e0b'; // Orange for warnings
bgColor = 'rgba(245, 158, 11, 0.1)';
} else if (lineLower.includes('info') || lineLower.includes('notice')) {
color = '#3b82f6'; // Blue for info
bgColor = 'rgba(59, 130, 246, 0.1)';
} else if (lineLower.includes('debug')) {
color = '#8b5cf6'; // Purple for debug
bgColor = 'rgba(139, 92, 246, 0.1)';
} else if (lineLower.includes('success') || lineLower.includes('ok')) {
color = '#22c55e'; // Green for success
bgColor = 'rgba(34, 197, 94, 0.1)';
}
return E('div', {
'style': 'padding: 4px 8px; margin: 0; color: ' + color + '; background: ' + bgColor + '; border-left: 3px solid ' + color + '; margin-bottom: 2px; border-radius: 2px;'
}, line);
},
getFilteredLogs: function() {
return this.logs.filter(function(line) {
var lineLower = line.toLowerCase();
// Apply filter
var matchesFilter = true;
switch (this.currentFilter) {
case 'error':
matchesFilter = lineLower.includes('error') || lineLower.includes('err') || lineLower.includes('fail');
break;
case 'warning':
matchesFilter = lineLower.includes('warn') || lineLower.includes('warning');
break;
case 'info':
matchesFilter = lineLower.includes('info') || lineLower.includes('notice');
break;
}
// Apply search
var matchesSearch = !this.searchQuery || lineLower.includes(this.searchQuery);
return matchesFilter && matchesSearch;
}.bind(this));
},
getLogStats: function() {
var stats = {
total: this.logs.length,
errors: 0,
warnings: 0,
info: 0
};
this.logs.forEach(function(line) {
var lineLower = line.toLowerCase();
if (lineLower.includes('error') || lineLower.includes('err') || lineLower.includes('fail')) {
stats.errors++;
} else if (lineLower.includes('warn') || lineLower.includes('warning')) {
stats.warnings++;
} else if (lineLower.includes('info') || lineLower.includes('notice')) {
stats.info++;
}
});
return stats;
},
refreshLogs: function() {
ui.showModal(_('Loading Logs'), [
E('p', { 'class': 'spinning' }, _('Fetching logs...'))
]);
API.getLogs(this.lineCount, '').then(L.bind(function(logs) {
ui.hideModal();
this.logs = logs || [];
this.updateLogDisplay();
// Update stats
var stats = this.getLogStats();
var statBadges = document.querySelectorAll('.sh-stat-value');
if (statBadges.length >= 3) {
statBadges[0].textContent = stats.total;
statBadges[1].textContent = stats.errors;
statBadges[2].textContent = stats.warnings;
}
// Update filter tabs counts
var tabs = document.querySelectorAll('.sh-tab-label');
if (tabs.length >= 4) {
tabs[0].textContent = 'All Logs (' + stats.total + ')';
tabs[1].textContent = 'Errors (' + stats.errors + ')';
tabs[2].textContent = 'Warnings (' + stats.warnings + ')';
tabs[3].textContent = 'Info (' + stats.info + ')';
}
}, this)).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Failed to load logs: ') + err.message), 'error');
});
},
downloadLogs: function() {
var filtered = this.getFilteredLogs();
var content = filtered.join('\n');
var blob = new Blob([content], { type: 'text/plain' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'system-logs-' + new Date().toISOString().split('T')[0] + '.txt';
a.click();
URL.revokeObjectURL(url);
ui.addNotification(null, E('p', '✓ Logs downloaded'), 'info');
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});