Metabolizer Blog Pipeline - integrated CMS for SecuBox: - Gitea: Mirror GitHub repos, store blog content - Streamlit: CMS app with markdown editor and live preview - HexoJS: Static site generator (clean → generate → publish) - Webhooks: Auto-rebuild on git push - Portal: Static blog served at /blog/ Pipeline: Edit in Streamlit CMS → Push to Gitea → Build with Hexo → Publish Packages: - secubox-app-streamlit: Streamlit server with LXC container - luci-app-streamlit: LuCI dashboard for Streamlit apps - secubox-app-metabolizer: CMS pipeline orchestrator CMS Features: - Two-column markdown editor with live preview - YAML front matter editor - Post management (drafts, publish, unpublish) - Media library with image upload - Git sync and Hexo build controls - Cyberpunk theme styling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
163 lines
4.1 KiB
JavaScript
163 lines
4.1 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require ui';
|
|
'require dom';
|
|
'require poll';
|
|
'require streamlit.api as api';
|
|
|
|
return view.extend({
|
|
logsData: null,
|
|
autoScroll: true,
|
|
|
|
load: function() {
|
|
return this.refreshData();
|
|
},
|
|
|
|
refreshData: function() {
|
|
var self = this;
|
|
return api.getLogs(200).then(function(logs) {
|
|
self.logsData = logs || [];
|
|
return logs;
|
|
});
|
|
},
|
|
|
|
render: function() {
|
|
var self = this;
|
|
|
|
// Inject CSS
|
|
var cssLink = E('link', {
|
|
'rel': 'stylesheet',
|
|
'type': 'text/css',
|
|
'href': L.resource('streamlit/dashboard.css')
|
|
});
|
|
|
|
var container = E('div', { 'class': 'streamlit-dashboard' }, [
|
|
cssLink,
|
|
this.renderHeader(),
|
|
this.renderLogsCard()
|
|
]);
|
|
|
|
// Poll for updates
|
|
poll.add(function() {
|
|
return self.refreshData().then(function() {
|
|
self.updateLogs();
|
|
});
|
|
}, 5);
|
|
|
|
return container;
|
|
},
|
|
|
|
renderHeader: function() {
|
|
var self = this;
|
|
|
|
return E('div', { 'class': 'st-header' }, [
|
|
E('div', { 'class': 'st-header-content' }, [
|
|
E('div', { 'class': 'st-logo' }, '\uD83D\uDCDC'),
|
|
E('div', {}, [
|
|
E('h1', { 'class': 'st-title' }, _('SYSTEM LOGS')),
|
|
E('p', { 'class': 'st-subtitle' }, _('Real-time container and application logs'))
|
|
]),
|
|
E('div', { 'class': 'st-btn-group' }, [
|
|
E('button', {
|
|
'class': 'st-btn st-btn-secondary',
|
|
'id': 'btn-autoscroll',
|
|
'click': function() { self.toggleAutoScroll(); }
|
|
}, [E('span', {}, '\u2193'), ' ' + _('Auto-scroll: ON')]),
|
|
E('button', {
|
|
'class': 'st-btn st-btn-primary',
|
|
'click': function() { self.refreshData().then(function() { self.updateLogs(); }); }
|
|
}, [E('span', {}, '\uD83D\uDD04'), ' ' + _('Refresh')])
|
|
])
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderLogsCard: function() {
|
|
var logs = this.logsData || [];
|
|
|
|
var logsContent;
|
|
if (logs.length > 0) {
|
|
logsContent = E('div', {
|
|
'class': 'st-logs',
|
|
'id': 'logs-container',
|
|
'style': 'max-height: 600px; font-size: 11px;'
|
|
}, logs.map(function(line, idx) {
|
|
return E('div', {
|
|
'class': 'st-logs-line',
|
|
'data-line': idx
|
|
}, self.formatLogLine(line));
|
|
}));
|
|
} else {
|
|
logsContent = E('div', { 'class': 'st-empty' }, [
|
|
E('div', { 'class': 'st-empty-icon' }, '\uD83D\uDCED'),
|
|
E('div', {}, _('No logs available yet')),
|
|
E('p', { 'style': 'font-size: 13px; color: #64748b; margin-top: 8px;' },
|
|
_('Logs will appear here once the Streamlit container is running'))
|
|
]);
|
|
}
|
|
|
|
return E('div', { 'class': 'st-card' }, [
|
|
E('div', { 'class': 'st-card-header' }, [
|
|
E('div', { 'class': 'st-card-title' }, [
|
|
E('span', {}, '\uD83D\uDCBB'),
|
|
' ' + _('Container Logs')
|
|
]),
|
|
E('div', { 'style': 'color: #94a3b8; font-size: 13px;' },
|
|
logs.length + ' ' + _('lines'))
|
|
]),
|
|
E('div', { 'class': 'st-card-body' }, [
|
|
logsContent
|
|
])
|
|
]);
|
|
},
|
|
|
|
formatLogLine: function(line) {
|
|
if (!line) return '';
|
|
|
|
// Add some color coding based on content
|
|
var color = '#0ff';
|
|
if (line.includes('ERROR') || line.includes('error') || line.includes('Error')) {
|
|
color = '#f43f5e';
|
|
} else if (line.includes('WARNING') || line.includes('warning') || line.includes('Warning')) {
|
|
color = '#f59e0b';
|
|
} else if (line.includes('INFO') || line.includes('info')) {
|
|
color = '#10b981';
|
|
} else if (line.includes('DEBUG') || line.includes('debug')) {
|
|
color = '#64748b';
|
|
}
|
|
|
|
return E('span', { 'style': 'color: ' + color }, line);
|
|
},
|
|
|
|
updateLogs: function() {
|
|
var self = this;
|
|
var container = document.getElementById('logs-container');
|
|
if (!container) return;
|
|
|
|
var logs = this.logsData || [];
|
|
|
|
container.innerHTML = '';
|
|
logs.forEach(function(line, idx) {
|
|
container.appendChild(E('div', {
|
|
'class': 'st-logs-line',
|
|
'data-line': idx
|
|
}, self.formatLogLine(line)));
|
|
});
|
|
|
|
// Auto-scroll to bottom
|
|
if (this.autoScroll) {
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
},
|
|
|
|
toggleAutoScroll: function() {
|
|
this.autoScroll = !this.autoScroll;
|
|
var btn = document.getElementById('btn-autoscroll');
|
|
if (btn) {
|
|
btn.innerHTML = '';
|
|
btn.appendChild(E('span', {}, '\u2193'));
|
|
btn.appendChild(document.createTextNode(' ' + _('Auto-scroll: ') + (this.autoScroll ? 'ON' : 'OFF')));
|
|
}
|
|
}
|
|
});
|