secubox-openwrt/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/overview.js
CyberMind-FR 280c6a08d9 fix(streamlit): Remove null text in app description display
When an app has no description, return empty string instead of null
to prevent "null" text from being rendered in the instances table.

Also: secubox-p2p bumped to v0.6.0-r3 with catalog fix.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 07:13:27 +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) : ''
]),
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'))
])
]);
}
});