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>
445 lines
12 KiB
JavaScript
445 lines
12 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require ui';
|
|
'require dom';
|
|
'require poll';
|
|
'require streamlit.api as api';
|
|
|
|
return view.extend({
|
|
statusData: null,
|
|
appsData: null,
|
|
logsData: null,
|
|
installProgress: null,
|
|
|
|
load: function() {
|
|
return this.refreshData();
|
|
},
|
|
|
|
refreshData: function() {
|
|
var self = this;
|
|
return api.getDashboardData().then(function(data) {
|
|
self.statusData = data.status || {};
|
|
self.appsData = data.apps || {};
|
|
self.logsData = data.logs || [];
|
|
return data;
|
|
});
|
|
},
|
|
|
|
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.renderStatsGrid(),
|
|
this.renderMainGrid()
|
|
]);
|
|
|
|
// Poll for updates
|
|
poll.add(function() {
|
|
return self.refreshData().then(function() {
|
|
self.updateDynamicContent();
|
|
});
|
|
}, 10);
|
|
|
|
return container;
|
|
},
|
|
|
|
renderHeader: function() {
|
|
var status = this.statusData;
|
|
var statusClass = !status.installed ? 'not-installed' : (status.running ? 'running' : 'stopped');
|
|
var statusText = !status.installed ? _('Not Installed') : (status.running ? _('Running') : _('Stopped'));
|
|
|
|
return E('div', { 'class': 'st-header' }, [
|
|
E('div', { 'class': 'st-header-content' }, [
|
|
E('div', { 'class': 'st-logo' }, '\u26A1'),
|
|
E('div', {}, [
|
|
E('h1', { 'class': 'st-title' }, _('STREAMLIT PLATFORM')),
|
|
E('p', { 'class': 'st-subtitle' }, _('Neural Data App Hosting for SecuBox'))
|
|
]),
|
|
E('div', { 'class': 'st-status-badge ' + statusClass, 'id': 'st-status-badge' }, [
|
|
E('span', {}, statusClass === 'running' ? '\u25CF' : '\u25CB'),
|
|
' ' + statusText
|
|
])
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderStatsGrid: function() {
|
|
var status = this.statusData;
|
|
var apps = this.appsData;
|
|
var appCount = (apps.apps || []).length;
|
|
|
|
var stats = [
|
|
{
|
|
icon: '\uD83D\uDD0C',
|
|
label: _('Status'),
|
|
value: status.running ? _('Online') : _('Offline'),
|
|
id: 'stat-status',
|
|
cardClass: status.running ? 'success' : 'error'
|
|
},
|
|
{
|
|
icon: '\uD83C\uDF10',
|
|
label: _('Port'),
|
|
value: status.http_port || '8501',
|
|
id: 'stat-port'
|
|
},
|
|
{
|
|
icon: '\uD83D\uDCBB',
|
|
label: _('Apps'),
|
|
value: appCount,
|
|
id: 'stat-apps'
|
|
},
|
|
{
|
|
icon: '\u26A1',
|
|
label: _('Active App'),
|
|
value: status.active_app || 'hello',
|
|
id: 'stat-active'
|
|
}
|
|
];
|
|
|
|
return E('div', { 'class': 'st-stats-grid' },
|
|
stats.map(function(stat) {
|
|
return E('div', { 'class': 'st-stat-card ' + (stat.cardClass || '') }, [
|
|
E('div', { 'class': 'st-stat-icon' }, stat.icon),
|
|
E('div', { 'class': 'st-stat-content' }, [
|
|
E('div', { 'class': 'st-stat-value', 'id': stat.id }, String(stat.value)),
|
|
E('div', { 'class': 'st-stat-label' }, stat.label)
|
|
])
|
|
]);
|
|
})
|
|
);
|
|
},
|
|
|
|
renderMainGrid: function() {
|
|
return E('div', { 'class': 'st-main-grid' }, [
|
|
this.renderControlCard(),
|
|
this.renderInfoCard(),
|
|
this.renderLogsCard()
|
|
]);
|
|
},
|
|
|
|
renderControlCard: function() {
|
|
var self = this;
|
|
var status = this.statusData;
|
|
|
|
var buttons = [];
|
|
|
|
if (!status.installed) {
|
|
buttons.push(
|
|
E('button', {
|
|
'class': 'st-btn st-btn-primary',
|
|
'id': 'btn-install',
|
|
'click': function() { self.handleInstall(); }
|
|
}, [E('span', {}, '\uD83D\uDCE5'), ' ' + _('Install')])
|
|
);
|
|
} else {
|
|
if (status.running) {
|
|
buttons.push(
|
|
E('button', {
|
|
'class': 'st-btn st-btn-danger',
|
|
'id': 'btn-stop',
|
|
'click': function() { self.handleStop(); }
|
|
}, [E('span', {}, '\u23F9'), ' ' + _('Stop')])
|
|
);
|
|
buttons.push(
|
|
E('button', {
|
|
'class': 'st-btn st-btn-warning',
|
|
'id': 'btn-restart',
|
|
'click': function() { self.handleRestart(); }
|
|
}, [E('span', {}, '\uD83D\uDD04'), ' ' + _('Restart')])
|
|
);
|
|
} else {
|
|
buttons.push(
|
|
E('button', {
|
|
'class': 'st-btn st-btn-success',
|
|
'id': 'btn-start',
|
|
'click': function() { self.handleStart(); }
|
|
}, [E('span', {}, '\u25B6'), ' ' + _('Start')])
|
|
);
|
|
}
|
|
|
|
buttons.push(
|
|
E('button', {
|
|
'class': 'st-btn st-btn-primary',
|
|
'id': 'btn-update',
|
|
'click': function() { self.handleUpdate(); }
|
|
}, [E('span', {}, '\u2B06'), ' ' + _('Update')])
|
|
);
|
|
|
|
buttons.push(
|
|
E('button', {
|
|
'class': 'st-btn st-btn-danger',
|
|
'id': 'btn-uninstall',
|
|
'click': function() { self.handleUninstall(); }
|
|
}, [E('span', {}, '\uD83D\uDDD1'), ' ' + _('Uninstall')])
|
|
);
|
|
}
|
|
|
|
return E('div', { 'class': 'st-card' }, [
|
|
E('div', { 'class': 'st-card-header' }, [
|
|
E('div', { 'class': 'st-card-title' }, [
|
|
E('span', {}, '\uD83C\uDFAE'),
|
|
' ' + _('Controls')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'st-card-body' }, [
|
|
E('div', { 'class': 'st-btn-group', 'id': 'st-controls' }, buttons),
|
|
E('div', { 'class': 'st-progress', 'id': 'st-progress-container', 'style': 'display:none' }, [
|
|
E('div', { 'class': 'st-progress-bar', 'id': 'st-progress-bar', 'style': 'width:0%' })
|
|
]),
|
|
E('div', { 'class': 'st-progress-text', 'id': 'st-progress-text', 'style': 'display:none' })
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderInfoCard: function() {
|
|
var status = this.statusData;
|
|
|
|
var infoItems = [
|
|
{ label: _('Container'), value: status.container_name || 'streamlit' },
|
|
{ label: _('Data Path'), value: status.data_path || '/srv/streamlit' },
|
|
{ label: _('Memory Limit'), value: status.memory_limit || '512M' },
|
|
{ label: _('Web Interface'), value: status.web_url, isLink: true }
|
|
];
|
|
|
|
return E('div', { 'class': 'st-card' }, [
|
|
E('div', { 'class': 'st-card-header' }, [
|
|
E('div', { 'class': 'st-card-title' }, [
|
|
E('span', {}, '\u2139\uFE0F'),
|
|
' ' + _('Information')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'st-card-body' }, [
|
|
E('ul', { 'class': 'st-info-list', 'id': 'st-info-list' },
|
|
infoItems.map(function(item) {
|
|
var valueEl;
|
|
if (item.isLink && item.value) {
|
|
valueEl = E('a', { 'href': item.value, 'target': '_blank' }, item.value);
|
|
} else {
|
|
valueEl = item.value || '-';
|
|
}
|
|
return E('li', {}, [
|
|
E('span', { 'class': 'st-info-label' }, item.label),
|
|
E('span', { 'class': 'st-info-value' }, valueEl)
|
|
]);
|
|
})
|
|
)
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderLogsCard: function() {
|
|
var logs = this.logsData || [];
|
|
|
|
return E('div', { 'class': 'st-card st-card-full' }, [
|
|
E('div', { 'class': 'st-card-header' }, [
|
|
E('div', { 'class': 'st-card-title' }, [
|
|
E('span', {}, '\uD83D\uDCDC'),
|
|
' ' + _('Recent Logs')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'st-card-body' }, [
|
|
logs.length > 0 ?
|
|
E('div', { 'class': 'st-logs', 'id': 'st-logs' },
|
|
logs.slice(-20).map(function(line) {
|
|
return E('div', { 'class': 'st-logs-line' }, line);
|
|
})
|
|
) :
|
|
E('div', { 'class': 'st-empty' }, [
|
|
E('div', { 'class': 'st-empty-icon' }, '\uD83D\uDCED'),
|
|
E('div', {}, _('No logs available'))
|
|
])
|
|
])
|
|
]);
|
|
},
|
|
|
|
updateDynamicContent: function() {
|
|
var status = this.statusData;
|
|
|
|
// Update status badge
|
|
var badge = document.getElementById('st-status-badge');
|
|
if (badge) {
|
|
var statusClass = !status.installed ? 'not-installed' : (status.running ? 'running' : 'stopped');
|
|
var statusText = !status.installed ? _('Not Installed') : (status.running ? _('Running') : _('Stopped'));
|
|
badge.className = 'st-status-badge ' + statusClass;
|
|
badge.innerHTML = '';
|
|
badge.appendChild(E('span', {}, statusClass === 'running' ? '\u25CF' : '\u25CB'));
|
|
badge.appendChild(document.createTextNode(' ' + statusText));
|
|
}
|
|
|
|
// Update stats
|
|
var statStatus = document.getElementById('stat-status');
|
|
if (statStatus) {
|
|
statStatus.textContent = status.running ? _('Online') : _('Offline');
|
|
}
|
|
|
|
var statActive = document.getElementById('stat-active');
|
|
if (statActive) {
|
|
statActive.textContent = status.active_app || 'hello';
|
|
}
|
|
|
|
// Update logs
|
|
var logsContainer = document.getElementById('st-logs');
|
|
if (logsContainer && this.logsData) {
|
|
logsContainer.innerHTML = '';
|
|
this.logsData.slice(-20).forEach(function(line) {
|
|
logsContainer.appendChild(E('div', { 'class': 'st-logs-line' }, line));
|
|
});
|
|
}
|
|
},
|
|
|
|
handleInstall: function() {
|
|
var self = this;
|
|
var btn = document.getElementById('btn-install');
|
|
if (btn) btn.disabled = true;
|
|
|
|
ui.showModal(_('Installing Streamlit Platform'), [
|
|
E('p', {}, _('This will download Alpine Linux rootfs and install Python 3.12 with Streamlit. This may take several minutes.')),
|
|
E('div', { 'class': 'st-progress' }, [
|
|
E('div', { 'class': 'st-progress-bar', 'id': 'modal-progress', 'style': 'width:0%' })
|
|
]),
|
|
E('p', { 'id': 'modal-status' }, _('Starting installation...'))
|
|
]);
|
|
|
|
api.install().then(function(result) {
|
|
if (result && result.started) {
|
|
self.pollInstallProgress();
|
|
} else {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, result.message || _('Installation failed')), 'error');
|
|
}
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, _('Installation failed: ') + err.message), 'error');
|
|
});
|
|
},
|
|
|
|
pollInstallProgress: function() {
|
|
var self = this;
|
|
|
|
var checkProgress = function() {
|
|
api.getInstallProgress().then(function(result) {
|
|
var progressBar = document.getElementById('modal-progress');
|
|
var statusText = document.getElementById('modal-status');
|
|
|
|
if (progressBar) {
|
|
progressBar.style.width = (result.progress || 0) + '%';
|
|
}
|
|
if (statusText) {
|
|
statusText.textContent = result.message || '';
|
|
}
|
|
|
|
if (result.status === 'completed') {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, _('Streamlit Platform installed successfully!')), 'success');
|
|
self.refreshData();
|
|
location.reload();
|
|
} else if (result.status === 'error') {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, _('Installation failed: ') + result.message), 'error');
|
|
} else if (result.status === 'running') {
|
|
setTimeout(checkProgress, 3000);
|
|
} else {
|
|
setTimeout(checkProgress, 3000);
|
|
}
|
|
}).catch(function() {
|
|
setTimeout(checkProgress, 5000);
|
|
});
|
|
};
|
|
|
|
setTimeout(checkProgress, 2000);
|
|
},
|
|
|
|
handleStart: function() {
|
|
var self = this;
|
|
api.start().then(function(result) {
|
|
if (result && result.success) {
|
|
ui.addNotification(null, E('p', {}, _('Streamlit Platform started')), 'success');
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, result.message || _('Failed to start')), 'error');
|
|
}
|
|
self.refreshData();
|
|
});
|
|
},
|
|
|
|
handleStop: function() {
|
|
var self = this;
|
|
api.stop().then(function(result) {
|
|
if (result && result.success) {
|
|
ui.addNotification(null, E('p', {}, _('Streamlit Platform stopped')), 'info');
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, result.message || _('Failed to stop')), 'error');
|
|
}
|
|
self.refreshData();
|
|
});
|
|
},
|
|
|
|
handleRestart: function() {
|
|
var self = this;
|
|
api.restart().then(function(result) {
|
|
if (result && result.success) {
|
|
ui.addNotification(null, E('p', {}, _('Streamlit Platform restarted')), 'success');
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, result.message || _('Failed to restart')), 'error');
|
|
}
|
|
self.refreshData();
|
|
});
|
|
},
|
|
|
|
handleUpdate: function() {
|
|
var self = this;
|
|
|
|
ui.showModal(_('Updating Streamlit'), [
|
|
E('p', {}, _('Updating Streamlit to the latest version...')),
|
|
E('div', { 'class': 'spinning' })
|
|
]);
|
|
|
|
api.update().then(function(result) {
|
|
ui.hideModal();
|
|
if (result && result.started) {
|
|
ui.addNotification(null, E('p', {}, _('Update started. The server will restart automatically.')), 'info');
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, result.message || _('Update failed')), 'error');
|
|
}
|
|
self.refreshData();
|
|
});
|
|
},
|
|
|
|
handleUninstall: function() {
|
|
var self = this;
|
|
|
|
ui.showModal(_('Confirm Uninstall'), [
|
|
E('p', {}, _('Are you sure you want to uninstall Streamlit Platform? Your apps will be preserved.')),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, _('Cancel')),
|
|
E('button', {
|
|
'class': 'btn cbi-button-negative',
|
|
'click': function() {
|
|
ui.hideModal();
|
|
api.uninstall().then(function(result) {
|
|
if (result && result.success) {
|
|
ui.addNotification(null, E('p', {}, _('Streamlit Platform uninstalled')), 'info');
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, result.message || _('Uninstall failed')), 'error');
|
|
}
|
|
self.refreshData();
|
|
location.reload();
|
|
});
|
|
}
|
|
}, _('Uninstall'))
|
|
])
|
|
]);
|
|
}
|
|
});
|