secubox-openwrt/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/overview.js
CyberMind-FR 906bf6f549 feat: Add HTTP health checks, portal speedtest, and fix cert detection
- metablogizer: Add HTTP health checks for backend (uhttpd) and frontend (HAProxy)
- metablogizer: Fix BusyBox-compatible certificate expiry detection using openssl checkend
- secubox-portal: Add speed test widget with ping/download/upload measurement
- tor-shield: Fix settings save ensuring UCI sections exist
- cdn-cache: UI improvements and restructure
- streamlit: Fix port conflict (sappix now uses 8503)
- secubox-core: Add proxy mode detection
- security-threats: Dashboard improvements
- haproxy: Init.d and Makefile updates

PKG_RELEASE bumps:
- luci-app-cdn-cache: 3
- luci-app-metablogizer: 2
- luci-app-secubox-portal: 2
- luci-app-secubox-security-threats: 2
- luci-app-secubox: 4
- luci-app-streamlit: 9
- luci-app-tor-shield: 2
- secubox-app-haproxy: 23
- secubox-core: 6

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:46:26 +01:00

502 lines
15 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.renderInstancesCard()
]);
},
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)
]);
})
)
])
]);
},
renderInstancesCard: function() {
var apps = this.appsData || {};
var instances = apps.apps || [];
var self = this;
return E('div', { 'class': 'st-card st-card-full' }, [
E('div', { 'class': 'st-card-header' }, [
E('div', { 'class': 'st-card-title' }, [
E('span', {}, '\uD83D\uDCCA'),
' ' + _('Instances')
]),
E('a', {
'href': L.url('admin', 'services', 'streamlit', 'apps'),
'class': 'st-link'
}, _('Manage Apps') + ' \u2192')
]),
E('div', { 'class': 'st-card-body st-no-padding' }, [
instances.length > 0 ?
E('table', { 'class': 'st-instances-table', 'id': 'st-instances' }, [
E('thead', {}, [
E('tr', {}, [
E('th', {}, _('App')),
E('th', {}, _('Port')),
E('th', {}, _('Status')),
E('th', {}, _('Published')),
E('th', {}, _('Domain'))
])
]),
E('tbody', {},
instances.map(function(app) {
var isActive = app.active || (self.statusData && self.statusData.active_app === app.name);
var isRunning = isActive && self.statusData && self.statusData.running;
var statusIcon = isRunning ? '\uD83D\uDFE2' : '\uD83D\uDD34';
var statusText = isRunning ? _('Running') : _('Stopped');
var publishedIcon = app.published ? '\u2705' : '\u26AA';
var domain = app.domain || (app.published ? app.name + '.example.com' : '-');
return E('tr', { 'class': isActive ? 'st-row-active' : '' }, [
E('td', {}, [
E('strong', {}, app.name || app.id),
app.description ? E('div', { 'class': 'st-app-desc' }, app.description) : null
]),
E('td', { 'class': 'st-mono' }, String(app.port || 8501)),
E('td', {}, [
E('span', { 'class': 'st-status-dot ' + (isRunning ? 'st-running' : 'st-stopped') }, statusIcon),
' ' + statusText
]),
E('td', {}, publishedIcon),
E('td', {}, domain !== '-' ?
E('a', { 'href': 'https://' + domain, 'target': '_blank' }, domain) :
'-'
)
]);
})
)
]) :
E('div', { 'class': 'st-empty' }, [
E('div', { 'class': 'st-empty-icon' }, '\uD83D\uDCE6'),
E('div', {}, _('No apps deployed')),
E('a', {
'href': L.url('admin', 'services', 'streamlit', 'apps'),
'class': 'st-btn st-btn-primary st-btn-sm'
}, _('Deploy First App'))
])
])
]);
},
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 instances table status indicators
var instancesTable = document.getElementById('st-instances');
if (instancesTable) {
var apps = this.appsData && this.appsData.apps || [];
var rows = instancesTable.querySelectorAll('tbody tr');
rows.forEach(function(row, idx) {
if (apps[idx]) {
var app = apps[idx];
var isActive = app.active || (self.statusData && self.statusData.active_app === app.name);
var isRunning = isActive && self.statusData && self.statusData.running;
row.className = isActive ? 'st-row-active' : '';
var statusCell = row.querySelector('td:nth-child(3)');
if (statusCell) {
statusCell.innerHTML = '';
var statusIcon = isRunning ? '\uD83D\uDFE2' : '\uD83D\uDD34';
var statusText = isRunning ? _('Running') : _('Stopped');
statusCell.appendChild(E('span', { 'class': 'st-status-dot ' + (isRunning ? 'st-running' : 'st-stopped') }, statusIcon));
statusCell.appendChild(document.createTextNode(' ' + statusText));
}
}
});
}
},
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'))
])
]);
}
});