From 5317f37e7a31f6046ec48d85e83ee9b64028269b Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 1 Feb 2026 07:17:20 +0100 Subject: [PATCH] refactor(streamlit): KISS UI redesign Simplify LuCI interface from 5 tabs to 2: - Dashboard: status, controls, apps list, upload (all-in-one) - Settings: configuration options Remove complex custom CSS, use standard LuCI styles. Deleted: overview.js, apps.js, instances.js, logs.js Added: dashboard.js (single-page dashboard) Updated: settings.js (simplified form), menu.json Co-Authored-By: Claude Opus 4.5 --- .../resources/view/streamlit/apps.js | 897 ------------------ .../resources/view/streamlit/dashboard.js | 345 +++++++ .../resources/view/streamlit/instances.js | 648 ------------- .../resources/view/streamlit/logs.js | 162 ---- .../resources/view/streamlit/overview.js | 501 ---------- .../resources/view/streamlit/settings.js | 327 +++---- .../share/luci/menu.d/luci-app-streamlit.json | 32 +- 7 files changed, 458 insertions(+), 2454 deletions(-) delete mode 100644 package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/apps.js create mode 100644 package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/dashboard.js delete mode 100644 package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/instances.js delete mode 100644 package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/logs.js delete mode 100644 package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/overview.js diff --git a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/apps.js b/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/apps.js deleted file mode 100644 index e06a1183..00000000 --- a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/apps.js +++ /dev/null @@ -1,897 +0,0 @@ -'use strict'; -'require view'; -'require ui'; -'require dom'; -'require poll'; -'require rpc'; -'require streamlit.api as api'; - -// HAProxy RPC calls for publishing -var haproxyCreateBackend = rpc.declare({ - object: 'luci.haproxy', - method: 'create_backend', - params: ['name', 'mode', 'balance', 'health_check', 'enabled'], - expect: {} -}); - -var haproxyCreateServer = rpc.declare({ - object: 'luci.haproxy', - method: 'create_server', - params: ['backend', 'name', 'address', 'port', 'weight', 'check', 'enabled'], - expect: {} -}); - -var haproxyCreateVhost = rpc.declare({ - object: 'luci.haproxy', - method: 'create_vhost', - params: ['domain', 'backend', 'ssl', 'ssl_redirect', 'acme', 'enabled'], - expect: {} -}); - -var haproxyListBackends = rpc.declare({ - object: 'luci.haproxy', - method: 'list_backends', - expect: { backends: [] } -}); - -var haproxyReload = rpc.declare({ - object: 'luci.haproxy', - method: 'reload', - expect: {} -}); - -return view.extend({ - appsData: null, - statusData: null, - haproxyBackends: [], - - load: function() { - return this.refreshData(); - }, - - refreshData: function() { - var self = this; - return Promise.all([ - api.listApps(), - api.getStatus(), - haproxyListBackends().catch(function() { return { backends: [] }; }) - ]).then(function(results) { - self.appsData = results[0] || {}; - self.statusData = results[1] || {}; - var backendResult = results[2] || {}; - self.haproxyBackends = Array.isArray(backendResult) ? backendResult : (backendResult.backends || []); - return results; - }); - }, - - 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.renderAppsCard(), - this.renderUploadCard() - ]); - - // Poll for updates - poll.add(function() { - return self.refreshData().then(function() { - self.updateAppsTable(); - }); - }, 15); - - return container; - }, - - renderHeader: function() { - return E('div', { 'class': 'st-header' }, [ - E('div', { 'class': 'st-header-content' }, [ - E('div', { 'class': 'st-logo' }, '\uD83D\uDCBB'), - E('div', {}, [ - E('h1', { 'class': 'st-title' }, _('APP MANAGEMENT')), - E('p', { 'class': 'st-subtitle' }, _('Deploy and manage your Streamlit applications')) - ]) - ]) - ]); - }, - - isAppPublished: function(appName) { - var backendName = 'streamlit_' + appName; - return this.haproxyBackends.some(function(b) { - return b.name === backendName || b.id === backendName; - }); - }, - - renderAppsCard: function() { - var self = this; - var apps = this.appsData.apps || []; - var activeApp = this.appsData.active_app || 'hello'; - - var tableRows = apps.map(function(app) { - var isActive = app.active || app.name === activeApp; - var isPublished = self.isAppPublished(app.name); - return E('tr', {}, [ - E('td', { 'class': isActive ? 'st-app-active' : '' }, [ - app.name, - isActive ? E('span', { 'class': 'st-app-badge active', 'style': 'margin-left: 8px' }, _('ACTIVE')) : '', - isPublished ? E('span', { 'class': 'st-app-badge', 'style': 'margin-left: 8px; background: #059669; color: #fff;' }, _('PUBLISHED')) : '' - ]), - E('td', {}, app.path || '-'), - E('td', {}, self.formatSize(app.size)), - E('td', {}, [ - E('div', { 'class': 'st-btn-group' }, [ - !isActive ? E('button', { - 'class': 'st-btn st-btn-primary', - 'style': 'padding: 5px 10px; font-size: 12px;', - 'click': function() { self.handleActivate(app.name); } - }, _('Activate')) : '', - !isPublished ? E('button', { - 'class': 'st-btn', - 'style': 'padding: 5px 10px; font-size: 12px; background: #7c3aed; color: #fff;', - 'click': function() { self.showPublishWizard(app.name); } - }, ['\uD83C\uDF10 ', _('Publish')]) : '', - app.name !== 'hello' ? E('button', { - 'class': 'st-btn st-btn-danger', - 'style': 'padding: 5px 10px; font-size: 12px;', - 'click': function() { self.handleRemove(app.name); } - }, _('Remove')) : '' - ]) - ]) - ]); - }); - - if (apps.length === 0) { - tableRows = [ - E('tr', {}, [ - E('td', { 'colspan': '4', 'style': 'text-align: center; padding: 40px;' }, [ - E('div', { 'class': 'st-empty' }, [ - E('div', { 'class': 'st-empty-icon' }, '\uD83D\uDCE6'), - E('div', {}, _('No apps deployed yet')) - ]) - ]) - ]) - ]; - } - - return E('div', { 'class': 'st-card', 'style': 'margin-bottom: 24px;' }, [ - E('div', { 'class': 'st-card-header' }, [ - E('div', { 'class': 'st-card-title' }, [ - E('span', {}, '\uD83D\uDCCB'), - ' ' + _('Deployed Apps') - ]), - E('div', {}, [ - E('span', { 'style': 'color: #94a3b8; font-size: 13px;' }, - apps.length + ' ' + (apps.length === 1 ? _('app') : _('apps'))) - ]) - ]), - E('div', { 'class': 'st-card-body' }, [ - E('table', { 'class': 'st-apps-table', 'id': 'apps-table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, _('Name')), - E('th', {}, _('Path')), - E('th', {}, _('Size')), - E('th', {}, _('Actions')) - ]) - ]), - E('tbody', { 'id': 'apps-tbody' }, tableRows) - ]) - ]) - ]); - }, - - renderUploadCard: function() { - var self = this; - - return E('div', { 'class': 'st-card' }, [ - E('div', { 'class': 'st-card-header' }, [ - E('div', { 'class': 'st-card-title' }, [ - E('span', {}, '\uD83D\uDCE4'), - ' ' + _('Upload New App') - ]) - ]), - E('div', { 'class': 'st-card-body' }, [ - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('App Name')), - E('input', { - 'type': 'text', - 'class': 'st-form-input', - 'id': 'upload-name', - 'placeholder': _('myapp (without .py extension)') - }) - ]), - E('div', { - 'class': 'st-upload-area', - 'id': 'upload-area', - 'click': function() { document.getElementById('upload-file').click(); }, - 'dragover': function(e) { - e.preventDefault(); - this.classList.add('dragover'); - }, - 'dragleave': function(e) { - e.preventDefault(); - this.classList.remove('dragover'); - }, - 'drop': function(e) { - e.preventDefault(); - this.classList.remove('dragover'); - var files = e.dataTransfer.files; - if (files.length > 0) { - self.handleFileSelect(files[0]); - } - } - }, [ - E('div', { 'class': 'st-upload-icon' }, '\uD83D\uDCC1'), - E('div', { 'class': 'st-upload-text' }, [ - E('p', {}, _('Drop your .py or .zip file here or click to browse')), - E('p', { 'style': 'font-size: 12px; color: #64748b;' }, _('Supported: Python (.py) or ZIP archive (.zip)')) - ]) - ]), - E('input', { - 'type': 'file', - 'id': 'upload-file', - 'accept': '.py,.zip', - 'style': 'display: none;', - 'change': function(e) { - if (e.target.files.length > 0) { - self.handleFileSelect(e.target.files[0]); - } - } - }), - E('div', { 'id': 'upload-status', 'style': 'margin-top: 16px; display: none;' }) - ]) - ]); - }, - - formatSize: function(bytes) { - if (!bytes || bytes === '-') return '-'; - var num = parseInt(bytes, 10); - if (isNaN(num)) return bytes; - if (num < 1024) return num + ' B'; - if (num < 1024 * 1024) return (num / 1024).toFixed(1) + ' KB'; - return (num / (1024 * 1024)).toFixed(1) + ' MB'; - }, - - updateAppsTable: function() { - var self = this; - var tbody = document.getElementById('apps-tbody'); - if (!tbody) return; - - var apps = this.appsData.apps || []; - var activeApp = this.appsData.active_app || 'hello'; - - tbody.innerHTML = ''; - - if (apps.length === 0) { - tbody.appendChild(E('tr', {}, [ - E('td', { 'colspan': '4', 'style': 'text-align: center; padding: 40px;' }, [ - E('div', { 'class': 'st-empty' }, [ - E('div', { 'class': 'st-empty-icon' }, '\uD83D\uDCE6'), - E('div', {}, _('No apps deployed yet')) - ]) - ]) - ])); - return; - } - - apps.forEach(function(app) { - var isActive = app.active || app.name === activeApp; - var isPublished = self.isAppPublished(app.name); - tbody.appendChild(E('tr', {}, [ - E('td', { 'class': isActive ? 'st-app-active' : '' }, [ - app.name, - isActive ? E('span', { 'class': 'st-app-badge active', 'style': 'margin-left: 8px' }, _('ACTIVE')) : '', - isPublished ? E('span', { 'class': 'st-app-badge', 'style': 'margin-left: 8px; background: #059669; color: #fff;' }, _('PUBLISHED')) : '' - ]), - E('td', {}, app.path || '-'), - E('td', {}, self.formatSize(app.size)), - E('td', {}, [ - E('div', { 'class': 'st-btn-group' }, [ - !isActive ? E('button', { - 'class': 'st-btn st-btn-primary', - 'style': 'padding: 5px 10px; font-size: 12px;', - 'click': function() { self.handleActivate(app.name); } - }, _('Activate')) : '', - !isPublished ? E('button', { - 'class': 'st-btn', - 'style': 'padding: 5px 10px; font-size: 12px; background: #7c3aed; color: #fff;', - 'click': function() { self.showPublishWizard(app.name); } - }, ['\uD83C\uDF10 ', _('Publish')]) : '', - app.name !== 'hello' ? E('button', { - 'class': 'st-btn st-btn-danger', - 'style': 'padding: 5px 10px; font-size: 12px;', - 'click': function() { self.handleRemove(app.name); } - }, _('Remove')) : '' - ]) - ]) - ])); - }); - }, - - handleFileSelect: function(file) { - var self = this; - - // Check for valid file types - var isPy = file.name.endsWith('.py'); - var isZip = file.name.endsWith('.zip'); - - if (!isPy && !isZip) { - ui.addNotification(null, E('p', {}, _('Please select a Python (.py) or ZIP (.zip) file')), 'error'); - return; - } - - var nameInput = document.getElementById('upload-name'); - if (!nameInput.value) { - // Auto-fill name from filename - nameInput.value = file.name.replace(/\.(py|zip)$/, ''); - } - - var statusDiv = document.getElementById('upload-status'); - statusDiv.style.display = 'block'; - statusDiv.innerHTML = ''; - statusDiv.appendChild(E('p', { 'style': 'color: #0ff;' }, - _('Selected: ') + file.name + ' (' + this.formatSize(file.size) + ')')); - - if (isZip) { - // For ZIP files, show tree selection dialog - statusDiv.appendChild(E('button', { - 'class': 'st-btn st-btn-success', - 'style': 'margin-top: 10px;', - 'click': function() { self.showZipTreeDialog(file); } - }, [E('span', {}, '\uD83D\uDCC2'), ' ' + _('Select Files from ZIP')])); - } else { - // For .py files, direct upload - statusDiv.appendChild(E('button', { - 'class': 'st-btn st-btn-success', - 'style': 'margin-top: 10px;', - 'click': function() { self.uploadFile(file); } - }, [E('span', {}, '\uD83D\uDCE4'), ' ' + _('Upload App')])); - } - }, - - pendingZipFile: null, - pendingZipContent: null, - zipTreeData: [], - - showZipTreeDialog: function(file) { - var self = this; - - ui.showModal(_('Loading ZIP...'), [ - E('p', { 'class': 'spinning' }, _('Reading ZIP archive contents...')) - ]); - - var reader = new FileReader(); - reader.onload = function(e) { - var bytes = new Uint8Array(e.target.result); - var binary = ''; - for (var i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]); - } - var content = btoa(binary); - self.pendingZipContent = content; - self.pendingZipFile = file; - - // Call backend to preview ZIP contents - api.previewZip(content).then(function(result) { - ui.hideModal(); - if (result && result.files) { - self.zipTreeData = result.files; - self.renderZipTreeModal(result.files); - } else if (result && result.error) { - ui.addNotification(null, E('p', {}, _('Failed to read ZIP: ') + result.error), 'error'); - } else { - // Fallback: parse ZIP client-side using JSZip-like approach - self.parseZipClientSide(content); - } - }).catch(function(err) { - ui.hideModal(); - // Fallback: try client-side parsing - self.parseZipClientSide(content); - }); - }; - reader.readAsArrayBuffer(file); - }, - - parseZipClientSide: function(content) { - var self = this; - // Basic ZIP parsing - extract file list from central directory - try { - var raw = atob(content); - var files = []; - var offset = 0; - - // Find End of Central Directory - var eocdOffset = raw.lastIndexOf('PK\x05\x06'); - if (eocdOffset === -1) { - throw new Error('Invalid ZIP file'); - } - - // Read number of entries - var numEntries = raw.charCodeAt(eocdOffset + 10) | (raw.charCodeAt(eocdOffset + 11) << 8); - var cdOffset = raw.charCodeAt(eocdOffset + 16) | (raw.charCodeAt(eocdOffset + 17) << 8) | - (raw.charCodeAt(eocdOffset + 18) << 16) | (raw.charCodeAt(eocdOffset + 19) << 24); - - offset = cdOffset; - for (var i = 0; i < numEntries && offset < eocdOffset; i++) { - if (raw.substr(offset, 4) !== 'PK\x01\x02') break; - - var nameLen = raw.charCodeAt(offset + 28) | (raw.charCodeAt(offset + 29) << 8); - var extraLen = raw.charCodeAt(offset + 30) | (raw.charCodeAt(offset + 31) << 8); - var commentLen = raw.charCodeAt(offset + 32) | (raw.charCodeAt(offset + 33) << 8); - var compSize = raw.charCodeAt(offset + 20) | (raw.charCodeAt(offset + 21) << 8) | - (raw.charCodeAt(offset + 22) << 16) | (raw.charCodeAt(offset + 23) << 24); - var uncompSize = raw.charCodeAt(offset + 24) | (raw.charCodeAt(offset + 25) << 8) | - (raw.charCodeAt(offset + 26) << 16) | (raw.charCodeAt(offset + 27) << 24); - - var fileName = raw.substr(offset + 46, nameLen); - var isDir = fileName.endsWith('/'); - - files.push({ - path: fileName, - size: uncompSize, - is_dir: isDir, - selected: fileName.endsWith('.py') || isDir - }); - - offset += 46 + nameLen + extraLen + commentLen; - } - - self.zipTreeData = files; - self.renderZipTreeModal(files); - } catch (err) { - ui.addNotification(null, E('p', {}, _('Failed to parse ZIP: ') + err.message), 'error'); - } - }, - - renderZipTreeModal: function(files) { - var self = this; - var nameInput = document.getElementById('upload-name'); - var appName = nameInput ? nameInput.value.trim() : 'app'; - - // Build tree structure - var tree = this.buildFileTree(files); - var treeHtml = this.renderTreeNode(tree, '', 0); - - ui.showModal(_('Select Files to Deploy'), [ - E('div', { 'style': 'margin-bottom: 16px;' }, [ - E('p', {}, _('Select which files to extract from the ZIP archive:')), - E('div', { 'style': 'margin-top: 8px;' }, [ - E('button', { - 'class': 'st-btn', - 'style': 'padding: 4px 8px; font-size: 11px; margin-right: 8px;', - 'click': function() { self.selectAllZipFiles(true); } - }, _('Select All')), - E('button', { - 'class': 'st-btn', - 'style': 'padding: 4px 8px; font-size: 11px; margin-right: 8px;', - 'click': function() { self.selectAllZipFiles(false); } - }, _('Deselect All')), - E('button', { - 'class': 'st-btn', - 'style': 'padding: 4px 8px; font-size: 11px;', - 'click': function() { self.selectPythonFiles(); } - }, _('Python Only')) - ]) - ]), - E('div', { - 'id': 'zip-tree-container', - 'style': 'max-height: 400px; overflow-y: auto; background: #0f172a; border: 1px solid #334155; border-radius: 4px; padding: 12px; font-family: monospace; font-size: 13px;' - }, treeHtml), - E('div', { 'style': 'margin-top: 16px; padding: 12px; background: #1e293b; border-radius: 4px;' }, [ - E('span', { 'id': 'zip-selected-count', 'style': 'color: #94a3b8;' }, - this.getSelectedCount() + ' ' + _('files selected')) - ]), - E('div', { 'class': 'right', 'style': 'margin-top: 16px;' }, [ - E('button', { - 'class': 'btn', - 'click': function() { - ui.hideModal(); - self.pendingZipFile = null; - self.pendingZipContent = null; - } - }, _('Cancel')), - E('button', { - 'class': 'btn cbi-button-positive', - 'style': 'margin-left: 8px;', - 'click': function() { self.uploadSelectedZipFiles(); } - }, ['\uD83D\uDCE4 ', _('Deploy Selected')]) - ]) - ]); - }, - - buildFileTree: function(files) { - var root = { name: '', children: {}, files: [] }; - - files.forEach(function(file) { - var parts = file.path.split('/').filter(function(p) { return p; }); - var current = root; - - for (var i = 0; i < parts.length; i++) { - var part = parts[i]; - var isLast = i === parts.length - 1; - - if (isLast && !file.is_dir) { - current.files.push({ - name: part, - path: file.path, - size: file.size, - selected: file.selected !== false - }); - } else { - if (!current.children[part]) { - current.children[part] = { name: part, children: {}, files: [], path: file.path }; - } - current = current.children[part]; - } - } - }); - - return root; - }, - - renderTreeNode: function(node, indent, level) { - var self = this; - var elements = []; - - // Render subdirectories - var dirs = Object.keys(node.children).sort(); - dirs.forEach(function(dirName) { - var dir = node.children[dirName]; - var dirPath = dir.path || (indent ? indent + '/' + dirName : dirName); - - elements.push(E('div', { - 'class': 'zip-tree-dir', - 'style': 'padding: 4px 0; padding-left: ' + (level * 16) + 'px;', - 'data-path': dirPath - }, [ - E('span', { - 'class': 'zip-tree-toggle', - 'style': 'cursor: pointer; color: #60a5fa; margin-right: 4px;', - 'click': function(e) { - var next = this.parentNode.nextSibling; - while (next && next.classList && next.classList.contains('zip-tree-child-' + level)) { - next.style.display = next.style.display === 'none' ? 'block' : 'none'; - next = next.nextSibling; - } - this.textContent = this.textContent === '\u25BC' ? '\u25B6' : '\u25BC'; - } - }, '\u25BC'), - E('span', { 'style': 'color: #fbbf24;' }, '\uD83D\uDCC1 '), - E('span', { 'style': 'color: #f1f5f9;' }, dirName + '/') - ])); - - // Render children - var childElements = self.renderTreeNode(dir, dirPath, level + 1); - childElements.forEach(function(el) { - el.classList.add('zip-tree-child-' + level); - elements.push(el); - }); - }); - - // Render files - node.files.sort(function(a, b) { return a.name.localeCompare(b.name); }); - node.files.forEach(function(file) { - var isPy = file.name.endsWith('.py'); - var icon = isPy ? '\uD83D\uDC0D' : '\uD83D\uDCC4'; - var color = isPy ? '#22c55e' : '#94a3b8'; - - elements.push(E('div', { - 'class': 'zip-tree-file', - 'style': 'padding: 4px 0; padding-left: ' + (level * 16) + 'px;' - }, [ - E('label', { 'style': 'cursor: pointer; display: flex; align-items: center;' }, [ - E('input', { - 'type': 'checkbox', - 'class': 'zip-file-checkbox', - 'data-path': file.path, - 'checked': file.selected, - 'style': 'margin-right: 8px;', - 'change': function() { self.updateSelectedCount(); } - }), - E('span', { 'style': 'margin-right: 4px;' }, icon), - E('span', { 'style': 'color: ' + color + ';' }, file.name), - E('span', { 'style': 'color: #64748b; margin-left: 8px; font-size: 11px;' }, - '(' + self.formatSize(file.size) + ')') - ]) - ])); - }); - - return elements; - }, - - selectAllZipFiles: function(select) { - var checkboxes = document.querySelectorAll('.zip-file-checkbox'); - checkboxes.forEach(function(cb) { cb.checked = select; }); - this.updateSelectedCount(); - }, - - selectPythonFiles: function() { - var checkboxes = document.querySelectorAll('.zip-file-checkbox'); - checkboxes.forEach(function(cb) { - cb.checked = cb.dataset.path.endsWith('.py'); - }); - this.updateSelectedCount(); - }, - - getSelectedCount: function() { - var checkboxes = document.querySelectorAll('.zip-file-checkbox:checked'); - return checkboxes ? checkboxes.length : 0; - }, - - updateSelectedCount: function() { - var countEl = document.getElementById('zip-selected-count'); - if (countEl) { - countEl.textContent = this.getSelectedCount() + ' ' + _('files selected'); - } - }, - - uploadSelectedZipFiles: function() { - var self = this; - var nameInput = document.getElementById('upload-name'); - var name = nameInput ? nameInput.value.trim() : ''; - - if (!name) { - ui.addNotification(null, E('p', {}, _('Please enter an app name')), 'error'); - return; - } - - // Collect selected files - var selectedFiles = []; - var checkboxes = document.querySelectorAll('.zip-file-checkbox:checked'); - checkboxes.forEach(function(cb) { - selectedFiles.push(cb.dataset.path); - }); - - if (selectedFiles.length === 0) { - ui.addNotification(null, E('p', {}, _('Please select at least one file')), 'error'); - return; - } - - ui.hideModal(); - ui.showModal(_('Deploying...'), [ - E('p', { 'class': 'spinning' }, _('Extracting and deploying selected files...')) - ]); - - api.uploadZip(name, self.pendingZipContent, selectedFiles).then(function(result) { - ui.hideModal(); - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('App deployed successfully: ') + name), 'success'); - nameInput.value = ''; - document.getElementById('upload-status').style.display = 'none'; - self.pendingZipFile = null; - self.pendingZipContent = null; - self.refreshData(); - } else { - ui.addNotification(null, E('p', {}, result.message || result.error || _('Deploy failed')), 'error'); - } - }).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', {}, _('Deploy failed: ') + err.message), 'error'); - }); - }, - - uploadFile: function(file) { - var self = this; - var nameInput = document.getElementById('upload-name'); - var name = nameInput.value.trim(); - - if (!name) { - ui.addNotification(null, E('p', {}, _('Please enter an app name')), 'error'); - return; - } - - // Validate name (alphanumeric and underscore only) - if (!/^[a-zA-Z0-9_]+$/.test(name)) { - ui.addNotification(null, E('p', {}, _('App name can only contain letters, numbers, and underscores')), 'error'); - return; - } - - var reader = new FileReader(); - reader.onload = function(e) { - // Convert ArrayBuffer to base64 (handles UTF-8 correctly) - var bytes = new Uint8Array(e.target.result); - var binary = ''; - for (var i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]); - } - var content = btoa(binary); - - api.uploadApp(name, content).then(function(result) { - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('App uploaded successfully: ') + name), 'success'); - nameInput.value = ''; - document.getElementById('upload-status').style.display = 'none'; - self.refreshData(); - } else { - ui.addNotification(null, E('p', {}, result.message || _('Upload failed')), 'error'); - } - }).catch(function(err) { - ui.addNotification(null, E('p', {}, _('Upload failed: ') + err.message), 'error'); - }); - }; - reader.readAsArrayBuffer(file); - }, - - handleActivate: function(name) { - var self = this; - - api.setActiveApp(name).then(function(result) { - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Active app set to: ') + name), 'success'); - self.refreshData(); - } else { - ui.addNotification(null, E('p', {}, result.message || _('Failed to activate app')), 'error'); - } - }); - }, - - handleRemove: function(name) { - var self = this; - - ui.showModal(_('Confirm Remove'), [ - E('p', {}, _('Are you sure you want to remove the app: ') + name + '?'), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Cancel')), - E('button', { - 'class': 'btn cbi-button-negative', - 'click': function() { - ui.hideModal(); - api.removeApp(name).then(function(result) { - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('App removed: ') + name), 'info'); - self.refreshData(); - } else { - ui.addNotification(null, E('p', {}, result.message || _('Failed to remove app')), 'error'); - } - }); - } - }, _('Remove')) - ]) - ]); - }, - - showPublishWizard: function(appName) { - var self = this; - var port = this.statusData.http_port || 8501; - var lanIp = '192.168.255.1'; - - // Try to get LAN IP from status - if (this.statusData.web_url) { - var match = this.statusData.web_url.match(/\/\/([^:\/]+)/); - if (match) lanIp = match[1]; - } - - ui.showModal(_('Publish App to Web'), [ - E('div', { 'style': 'margin-bottom: 16px;' }, [ - E('p', { 'style': 'margin-bottom: 12px;' }, [ - _('Configure HAProxy to expose '), - E('strong', {}, appName), - _(' via a custom domain.') - ]) - ]), - E('div', { 'style': 'margin-bottom: 12px;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 4px; font-weight: bold;' }, _('Domain Name')), - E('input', { - 'type': 'text', - 'id': 'publish-domain', - 'style': 'width: 100%; padding: 8px; border: 1px solid #334155; background: #1e293b; color: #fff; border-radius: 4px;', - 'placeholder': appName + '.example.com' - }), - E('small', { 'style': 'color: #64748b;' }, _('Enter the domain that will route to this app')) - ]), - E('div', { 'style': 'margin-bottom: 12px;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 4px;' }, [ - E('input', { - 'type': 'checkbox', - 'id': 'publish-ssl', - 'checked': true, - 'style': 'margin-right: 8px;' - }), - _('Enable SSL (HTTPS)') - ]) - ]), - E('div', { 'style': 'margin-bottom: 12px;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 4px;' }, [ - E('input', { - 'type': 'checkbox', - 'id': 'publish-acme', - 'checked': true, - 'style': 'margin-right: 8px;' - }), - _('Auto-request Let\'s Encrypt certificate') - ]) - ]), - E('div', { 'style': 'background: #334155; padding: 12px; border-radius: 4px; margin-bottom: 16px;' }, [ - E('p', { 'style': 'margin: 0; font-size: 13px;' }, [ - _('Backend: '), - E('code', {}, lanIp + ':' + port) - ]) - ]), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Cancel')), - E('button', { - 'class': 'btn cbi-button-positive', - 'style': 'margin-left: 8px;', - 'click': function() { - var domain = document.getElementById('publish-domain').value.trim(); - var ssl = document.getElementById('publish-ssl').checked; - var acme = document.getElementById('publish-acme').checked; - - if (!domain) { - ui.addNotification(null, E('p', {}, _('Please enter a domain name')), 'error'); - return; - } - - self.publishApp(appName, domain, lanIp, port, ssl, acme); - } - }, ['\uD83D\uDE80 ', _('Publish')]) - ]) - ]); - }, - - publishApp: function(appName, domain, backendIp, backendPort, ssl, acme) { - var self = this; - var backendName = 'streamlit_' + appName; - - ui.hideModal(); - ui.showModal(_('Publishing...'), [ - E('p', { 'class': 'spinning' }, _('Creating HAProxy configuration...')) - ]); - - // Step 1: Create backend - haproxyCreateBackend(backendName, 'http', 'roundrobin', 'httpchk', '1') - .then(function(result) { - if (result && result.error) { - throw new Error(result.error); - } - // Step 2: Create server - return haproxyCreateServer(backendName, appName, backendIp, backendPort.toString(), '100', '1', '1'); - }) - .then(function(result) { - if (result && result.error) { - throw new Error(result.error); - } - // Step 3: Create vhost - var sslFlag = ssl ? '1' : '0'; - var acmeFlag = acme ? '1' : '0'; - return haproxyCreateVhost(domain, backendName, sslFlag, sslFlag, acmeFlag, '1'); - }) - .then(function(result) { - if (result && result.error) { - throw new Error(result.error); - } - // Step 4: Reload HAProxy - return haproxyReload(); - }) - .then(function() { - ui.hideModal(); - ui.addNotification(null, E('p', {}, [ - _('App published successfully! Access at: '), - E('a', { - 'href': (ssl ? 'https://' : 'http://') + domain, - 'target': '_blank', - 'style': 'color: #0ff;' - }, (ssl ? 'https://' : 'http://') + domain) - ]), 'success'); - self.refreshData(); - }) - .catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', {}, _('Publish failed: ') + (err.message || err)), 'error'); - }); - } -}); diff --git a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/dashboard.js b/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/dashboard.js new file mode 100644 index 00000000..8a527372 --- /dev/null +++ b/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/dashboard.js @@ -0,0 +1,345 @@ +'use strict'; +'require view'; +'require ui'; +'require poll'; +'require streamlit.api as api'; + +return view.extend({ + status: {}, + apps: [], + activeApp: '', + + load: function() { + return this.refresh(); + }, + + refresh: function() { + var self = this; + return Promise.all([ + api.getStatus(), + api.listApps() + ]).then(function(r) { + self.status = r[0] || {}; + self.apps = (r[1] && r[1].apps) || []; + self.activeApp = (r[1] && r[1].active_app) || ''; + }); + }, + + render: function() { + var self = this; + var s = this.status; + var running = s.running; + var installed = s.installed; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, _('Streamlit Platform')), + E('div', { 'class': 'cbi-map-descr' }, _('Python data app hosting')), + + // Status Section + E('div', { 'class': 'cbi-section', 'id': 'status-section' }, [ + E('h3', {}, _('Service Status')), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Status')), + E('div', { 'class': 'cbi-value-field', 'id': 'svc-status' }, + !installed ? E('em', { 'style': 'color:#999' }, _('Not installed')) : + running ? E('span', { 'style': 'color:#0a0' }, _('Running')) : + E('span', { 'style': 'color:#a00' }, _('Stopped')) + ) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Active App')), + E('div', { 'class': 'cbi-value-field', 'id': 'active-app' }, + this.activeApp || E('em', {}, '-')) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Web URL')), + E('div', { 'class': 'cbi-value-field' }, + s.web_url ? E('a', { 'href': s.web_url, 'target': '_blank' }, s.web_url) : '-') + ]), + E('div', { 'class': 'cbi-page-actions' }, this.renderControls(installed, running)) + ]), + + // Apps Section + E('div', { 'class': 'cbi-section', 'id': 'apps-section' }, [ + E('h3', {}, _('Applications')), + E('div', { 'id': 'apps-table' }, this.renderAppsTable()) + ]), + + // Upload Section + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Upload App')), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Python File')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { 'type': 'file', 'id': 'upload-file', 'accept': '.py' }), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'margin-left: 8px', + 'click': function() { self.uploadApp(); } + }, _('Upload')) + ]) + ]) + ]) + ]); + + poll.add(function() { + return self.refresh().then(function() { + self.updateStatus(); + }); + }, 5); + + return view; + }, + + renderControls: function(installed, running) { + var self = this; + var btns = []; + + if (!installed) { + btns.push(E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': function() { self.doInstall(); } + }, _('Install'))); + } else { + if (running) { + btns.push(E('button', { + 'class': 'cbi-button cbi-button-negative', + 'click': function() { self.doStop(); } + }, _('Stop'))); + btns.push(E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'margin-left: 8px', + 'click': function() { self.doRestart(); } + }, _('Restart'))); + } else { + btns.push(E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': function() { self.doStart(); } + }, _('Start'))); + } + btns.push(E('button', { + 'class': 'cbi-button', + 'style': 'margin-left: 8px', + 'click': function() { self.doUninstall(); } + }, _('Uninstall'))); + } + + return btns; + }, + + renderAppsTable: function() { + var self = this; + var apps = this.apps; + + if (!apps.length) { + return E('em', {}, _('No apps found')); + } + + var rows = apps.map(function(app) { + var isActive = app.name === self.activeApp; + return E('tr', { 'class': isActive ? 'cbi-rowstyle-2' : '' }, [ + E('td', {}, [ + E('strong', {}, app.name), + isActive ? E('span', { 'style': 'color:#0a0; margin-left:8px' }, _('(active)')) : '' + ]), + E('td', {}, app.path ? app.path.split('/').pop() : '-'), + E('td', {}, [ + !isActive ? E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { self.activateApp(app.name); } + }, _('Activate')) : '', + E('button', { + 'class': 'cbi-button cbi-button-remove', + 'style': 'margin-left: 4px', + 'click': function() { self.deleteApp(app.name); } + }, _('Delete')) + ]) + ]); + }); + + return E('table', { 'class': 'table cbi-section-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('Name')), + E('th', { 'class': 'th' }, _('File')), + E('th', { 'class': 'th' }, _('Actions')) + ]) + ].concat(rows)); + }, + + updateStatus: function() { + var s = this.status; + var statusEl = document.getElementById('svc-status'); + var activeEl = document.getElementById('active-app'); + var appsEl = document.getElementById('apps-table'); + + if (statusEl) { + statusEl.innerHTML = ''; + if (!s.installed) { + statusEl.appendChild(E('em', { 'style': 'color:#999' }, _('Not installed'))); + } else if (s.running) { + statusEl.appendChild(E('span', { 'style': 'color:#0a0' }, _('Running'))); + } else { + statusEl.appendChild(E('span', { 'style': 'color:#a00' }, _('Stopped'))); + } + } + + if (activeEl) { + activeEl.textContent = this.activeApp || '-'; + } + + if (appsEl) { + appsEl.innerHTML = ''; + appsEl.appendChild(this.renderAppsTable()); + } + }, + + doStart: function() { + var self = this; + api.start().then(function(r) { + if (r && r.success) { + ui.addNotification(null, E('p', {}, _('Service started')), 'info'); + } + self.refresh(); + }); + }, + + doStop: function() { + var self = this; + api.stop().then(function(r) { + if (r && r.success) { + ui.addNotification(null, E('p', {}, _('Service stopped')), 'info'); + } + self.refresh(); + }); + }, + + doRestart: function() { + var self = this; + api.restart().then(function(r) { + if (r && r.success) { + ui.addNotification(null, E('p', {}, _('Service restarted')), 'info'); + } + self.refresh(); + }); + }, + + doInstall: function() { + var self = this; + ui.showModal(_('Installing'), [ + E('p', { 'class': 'spinning' }, _('Installing Streamlit platform...')) + ]); + api.install().then(function(r) { + if (r && r.started) { + self.pollInstall(); + } else { + ui.hideModal(); + ui.addNotification(null, E('p', {}, r.message || _('Install failed')), 'error'); + } + }); + }, + + pollInstall: function() { + var self = this; + var check = function() { + api.getInstallProgress().then(function(r) { + if (r.status === 'completed') { + ui.hideModal(); + ui.addNotification(null, E('p', {}, _('Installation complete')), 'success'); + self.refresh(); + location.reload(); + } else if (r.status === 'error') { + ui.hideModal(); + ui.addNotification(null, E('p', {}, r.message || _('Install failed')), 'error'); + } else { + setTimeout(check, 3000); + } + }); + }; + setTimeout(check, 2000); + }, + + doUninstall: function() { + var self = this; + ui.showModal(_('Confirm'), [ + E('p', {}, _('Uninstall Streamlit platform?')), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Cancel')), + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'style': 'margin-left: 8px', + 'click': function() { + ui.hideModal(); + api.uninstall().then(function() { + ui.addNotification(null, E('p', {}, _('Uninstalled')), 'info'); + self.refresh(); + location.reload(); + }); + } + }, _('Uninstall')) + ]) + ]); + }, + + activateApp: function(name) { + var self = this; + api.setActiveApp(name).then(function(r) { + if (r && r.success) { + ui.addNotification(null, E('p', {}, _('App activated: ') + name), 'info'); + return api.restart(); + } + }).then(function() { + self.refresh(); + }); + }, + + deleteApp: function(name) { + var self = this; + ui.showModal(_('Confirm'), [ + E('p', {}, _('Delete app: ') + name + '?'), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Cancel')), + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'style': 'margin-left: 8px', + 'click': function() { + ui.hideModal(); + api.removeApp(name).then(function(r) { + if (r && r.success) { + ui.addNotification(null, E('p', {}, _('App deleted')), 'info'); + } + self.refresh(); + }); + } + }, _('Delete')) + ]) + ]); + }, + + uploadApp: function() { + var self = this; + var fileInput = document.getElementById('upload-file'); + if (!fileInput || !fileInput.files.length) { + ui.addNotification(null, E('p', {}, _('Select a file first')), 'error'); + return; + } + + var file = fileInput.files[0]; + var name = file.name.replace(/\.py$/, ''); + var reader = new FileReader(); + + reader.onload = function(e) { + var content = btoa(e.target.result); + api.uploadApp(name, content).then(function(r) { + if (r && r.success) { + ui.addNotification(null, E('p', {}, _('App uploaded: ') + name), 'success'); + fileInput.value = ''; + self.refresh(); + } else { + ui.addNotification(null, E('p', {}, r.message || _('Upload failed')), 'error'); + } + }); + }; + + reader.readAsText(file); + } +}); diff --git a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/instances.js b/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/instances.js deleted file mode 100644 index 0f28555e..00000000 --- a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/instances.js +++ /dev/null @@ -1,648 +0,0 @@ -'use strict'; -'require view'; -'require ui'; -'require dom'; -'require poll'; -'require rpc'; -'require streamlit.api as api'; - -// HAProxy RPC calls for publishing -var haproxyCreateBackend = rpc.declare({ - object: 'luci.haproxy', - method: 'create_backend', - params: ['name', 'mode', 'balance', 'health_check', 'enabled'], - expect: {} -}); - -var haproxyCreateServer = rpc.declare({ - object: 'luci.haproxy', - method: 'create_server', - params: ['backend', 'name', 'address', 'port', 'weight', 'check', 'enabled'], - expect: {} -}); - -var haproxyCreateVhost = rpc.declare({ - object: 'luci.haproxy', - method: 'create_vhost', - params: ['domain', 'backend', 'ssl', 'ssl_redirect', 'acme', 'enabled'], - expect: {} -}); - -var haproxyReload = rpc.declare({ - object: 'luci.haproxy', - method: 'reload', - expect: {} -}); - -return view.extend({ - instancesData: [], - appsData: [], - statusData: {}, - - load: function() { - return this.refreshData(); - }, - - getLanIp: function() { - if (this.statusData && this.statusData.web_url) { - var match = this.statusData.web_url.match(/\/\/([^:\/]+)/); - if (match) return match[1]; - } - // Fallback: get from network config - return '192.168.255.1'; - }, - - refreshData: function() { - var self = this; - return Promise.all([ - api.listInstances(), - api.listApps(), - api.getStatus() - ]).then(function(results) { - self.instancesData = results[0] || []; - self.appsData = results[1] || {}; - self.statusData = results[2] || {}; - return results; - }); - }, - - render: function() { - var self = this; - - 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.renderInstancesCard(), - this.renderAddInstanceCard() - ]); - - poll.add(function() { - return self.refreshData().then(function() { - self.updateInstancesTable(); - }); - }, 10); - - return container; - }, - - renderHeader: function() { - return E('div', { 'class': 'st-header' }, [ - E('div', { 'class': 'st-header-content' }, [ - E('div', { 'class': 'st-logo' }, '\uD83D\uDCE6'), - E('div', {}, [ - E('h1', { 'class': 'st-title' }, _('INSTANCES')), - E('p', { 'class': 'st-subtitle' }, _('Manage multiple Streamlit app instances on different ports')) - ]) - ]) - ]); - }, - - renderInstancesCard: function() { - var self = this; - var instances = this.instancesData; - - var tableRows = instances.map(function(inst) { - return self.renderInstanceRow(inst); - }); - - if (instances.length === 0) { - tableRows = [ - E('tr', {}, [ - E('td', { 'colspan': '5', 'style': 'text-align: center; padding: 40px;' }, [ - E('div', { 'class': 'st-empty' }, [ - E('div', { 'class': 'st-empty-icon' }, '\uD83D\uDCE6'), - E('div', {}, _('No instances configured')) - ]) - ]) - ]) - ]; - } - - return E('div', { 'class': 'st-card', 'style': 'margin-bottom: 24px;' }, [ - E('div', { 'class': 'st-card-header' }, [ - E('div', { 'class': 'st-card-title' }, [ - E('span', {}, '\uD83D\uDD04'), - ' ' + _('Running Instances') - ]), - E('div', {}, [ - E('span', { 'style': 'color: #94a3b8; font-size: 13px;' }, - instances.length + ' ' + (instances.length === 1 ? _('instance') : _('instances'))), - E('button', { - 'class': 'st-btn st-btn-primary', - 'style': 'margin-left: 16px; padding: 6px 12px; font-size: 13px;', - 'click': function() { self.applyChanges(); } - }, ['\u21BB ', _('Apply & Restart')]) - ]) - ]), - E('div', { 'class': 'st-card-body' }, [ - E('table', { 'class': 'st-apps-table', 'id': 'instances-table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, _('ID')), - E('th', {}, _('App')), - E('th', {}, _('Port')), - E('th', { 'style': 'text-align: center;' }, _('Enabled')), - E('th', {}, _('Actions')) - ]) - ]), - E('tbody', { 'id': 'instances-tbody' }, tableRows) - ]) - ]) - ]); - }, - - renderInstanceRow: function(inst) { - var self = this; - - // Enable/disable checkbox - var enableCheckbox = E('input', { - 'type': 'checkbox', - 'checked': inst.enabled, - 'style': 'width: 18px; height: 18px; cursor: pointer;', - 'change': function() { - if (this.checked) { - self.handleEnable(inst.id); - } else { - self.handleDisable(inst.id); - } - } - }); - - return E('tr', {}, [ - E('td', {}, [ - E('strong', {}, inst.id), - inst.name && inst.name !== inst.id ? E('span', { 'style': 'color: #94a3b8; margin-left: 8px;' }, '(' + inst.name + ')') : '' - ]), - E('td', {}, inst.app || '-'), - E('td', {}, [ - E('code', { 'style': 'background: #334155; padding: 2px 6px; border-radius: 4px;' }, ':' + inst.port) - ]), - E('td', { 'style': 'text-align: center;' }, enableCheckbox), - E('td', {}, [ - E('div', { 'class': 'st-btn-group' }, [ - E('button', { - 'class': 'st-btn', - 'style': 'padding: 5px 10px; font-size: 12px; background: #7c3aed; color: #fff;', - 'click': function() { self.showPublishWizard(inst); } - }, ['\uD83C\uDF10 ', _('Publish')]), - E('button', { - 'class': 'st-btn', - 'style': 'padding: 5px 10px; font-size: 12px; background: #0ea5e9;', - 'click': function() { self.showEditDialog(inst); } - }, ['\u270F ', _('Edit')]), - E('button', { - 'class': 'st-btn st-btn-danger', - 'style': 'padding: 5px 10px; font-size: 12px;', - 'click': function() { self.handleRemove(inst.id); } - }, _('Remove')) - ]) - ]) - ]); - }, - - renderAddInstanceCard: function() { - var self = this; - var appsList = this.appsData.apps || []; - - // Calculate next available port - var usedPorts = this.instancesData.map(function(i) { return i.port; }); - var nextPort = 8501; - while (usedPorts.indexOf(nextPort) !== -1) { - nextPort++; - } - - // Build select options array - var selectOptions = [E('option', { 'value': '' }, _('-- Select App --'))]; - if (appsList.length > 0) { - appsList.forEach(function(app) { - selectOptions.push(E('option', { 'value': app.name + '.py' }, app.name)); - }); - } else { - selectOptions.push(E('option', { 'disabled': true }, _('No apps available'))); - } - - return E('div', { 'class': 'st-card' }, [ - E('div', { 'class': 'st-card-header' }, [ - E('div', { 'class': 'st-card-title' }, [ - E('span', {}, '\u2795'), - ' ' + _('Add Instance') - ]) - ]), - E('div', { 'class': 'st-card-body' }, [ - E('div', { 'style': 'display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;' }, [ - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('Instance ID')), - E('input', { - 'type': 'text', - 'class': 'st-form-input', - 'id': 'new-inst-id', - 'placeholder': _('myapp') - }) - ]), - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('Display Name')), - E('input', { - 'type': 'text', - 'class': 'st-form-input', - 'id': 'new-inst-name', - 'placeholder': _('My Application') - }) - ]), - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('App File')), - E('select', { - 'class': 'st-form-input', - 'id': 'new-inst-app', - 'style': 'height: 42px;' - }, selectOptions) - ]), - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('Port')), - E('input', { - 'type': 'number', - 'class': 'st-form-input', - 'id': 'new-inst-port', - 'value': nextPort, - 'min': '8501', - 'max': '9999' - }) - ]) - ]), - E('div', { 'style': 'margin-top: 16px;' }, [ - E('button', { - 'class': 'st-btn st-btn-success', - 'click': function() { self.handleAdd(); } - }, ['\u2795 ', _('Add Instance')]) - ]) - ]) - ]); - }, - - updateInstancesTable: function() { - var self = this; - var tbody = document.getElementById('instances-tbody'); - if (!tbody) return; - - tbody.innerHTML = ''; - - if (this.instancesData.length === 0) { - tbody.appendChild(E('tr', {}, [ - E('td', { 'colspan': '5', 'style': 'text-align: center; padding: 40px;' }, [ - E('div', { 'class': 'st-empty' }, [ - E('div', { 'class': 'st-empty-icon' }, '\uD83D\uDCE6'), - E('div', {}, _('No instances configured')) - ]) - ]) - ])); - return; - } - - this.instancesData.forEach(function(inst) { - tbody.appendChild(self.renderInstanceRow(inst)); - }); - }, - - handleAdd: function() { - var self = this; - var id = document.getElementById('new-inst-id').value.trim(); - var name = document.getElementById('new-inst-name').value.trim(); - var app = document.getElementById('new-inst-app').value; - var port = parseInt(document.getElementById('new-inst-port').value, 10); - - if (!id) { - ui.addNotification(null, E('p', {}, _('Please enter an instance ID')), 'error'); - return; - } - - if (!/^[a-zA-Z0-9_]+$/.test(id)) { - ui.addNotification(null, E('p', {}, _('ID can only contain letters, numbers, and underscores')), 'error'); - return; - } - - if (!app) { - ui.addNotification(null, E('p', {}, _('Please select an app')), 'error'); - return; - } - - if (!port || port < 1024 || port > 65535) { - ui.addNotification(null, E('p', {}, _('Please enter a valid port (1024-65535)')), 'error'); - return; - } - - if (!name) { - name = id; - } - - api.addInstance(id, name, app, port).then(function(result) { - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Instance added: ') + id), 'success'); - document.getElementById('new-inst-id').value = ''; - document.getElementById('new-inst-name').value = ''; - document.getElementById('new-inst-app').value = ''; - self.refreshData(); - } else { - ui.addNotification(null, E('p', {}, result.message || _('Failed to add instance')), 'error'); - } - }).catch(function(err) { - ui.addNotification(null, E('p', {}, _('Error: ') + err.message), 'error'); - }); - }, - - handleEnable: function(id) { - var self = this; - api.enableInstance(id).then(function(result) { - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Instance enabled: ') + id), 'success'); - self.refreshData(); - } else { - ui.addNotification(null, E('p', {}, result.message || _('Failed to enable instance')), 'error'); - } - }); - }, - - handleDisable: function(id) { - var self = this; - api.disableInstance(id).then(function(result) { - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Instance disabled: ') + id), 'success'); - self.refreshData(); - } else { - ui.addNotification(null, E('p', {}, result.message || _('Failed to disable instance')), 'error'); - } - }); - }, - - handleRemove: function(id) { - var self = this; - - ui.showModal(_('Confirm Remove'), [ - E('p', {}, _('Are you sure you want to remove instance: ') + id + '?'), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Cancel')), - E('button', { - 'class': 'btn cbi-button-negative', - 'click': function() { - ui.hideModal(); - api.removeInstance(id).then(function(result) { - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Instance removed: ') + id), 'info'); - self.refreshData(); - } else { - ui.addNotification(null, E('p', {}, result.message || _('Failed to remove instance')), 'error'); - } - }); - } - }, _('Remove')) - ]) - ]); - }, - - applyChanges: function() { - ui.showModal(_('Applying Changes'), [ - E('p', { 'class': 'spinning' }, _('Restarting Streamlit service...')) - ]); - - api.restart().then(function(result) { - ui.hideModal(); - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Service restarted successfully')), 'success'); - } else { - ui.addNotification(null, E('p', {}, result.message || _('Restart may have issues')), 'warning'); - } - }).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', {}, _('Error: ') + err.message), 'error'); - }); - }, - - showPublishWizard: function(inst) { - var self = this; - var lanIp = this.getLanIp(); - var port = inst.port; - - ui.showModal(_('Publish Instance to Web'), [ - E('div', { 'style': 'margin-bottom: 16px;' }, [ - E('p', { 'style': 'margin-bottom: 12px;' }, [ - _('Configure HAProxy to expose '), - E('strong', {}, inst.id), - _(' (port '), - E('code', {}, port), - _(') via a custom domain.') - ]) - ]), - E('div', { 'style': 'margin-bottom: 12px;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 4px; font-weight: bold;' }, _('Domain Name')), - E('input', { - 'type': 'text', - 'id': 'publish-domain', - 'style': 'width: 100%; padding: 8px; border: 1px solid #334155; background: #1e293b; color: #fff; border-radius: 4px;', - 'placeholder': inst.id + '.example.com' - }), - E('small', { 'style': 'color: #64748b;' }, _('Enter the domain that will route to this instance')) - ]), - E('div', { 'style': 'margin-bottom: 12px;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 4px;' }, [ - E('input', { - 'type': 'checkbox', - 'id': 'publish-ssl', - 'checked': true, - 'style': 'margin-right: 8px;' - }), - _('Enable SSL (HTTPS)') - ]) - ]), - E('div', { 'style': 'margin-bottom: 12px;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 4px;' }, [ - E('input', { - 'type': 'checkbox', - 'id': 'publish-acme', - 'checked': true, - 'style': 'margin-right: 8px;' - }), - _('Auto-request Let\'s Encrypt certificate (via cron)') - ]) - ]), - E('div', { 'style': 'background: #334155; padding: 12px; border-radius: 4px; margin-bottom: 16px;' }, [ - E('p', { 'style': 'margin: 0; font-size: 13px;' }, [ - _('Backend: '), - E('code', {}, lanIp + ':' + port) - ]) - ]), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Cancel')), - E('button', { - 'class': 'btn cbi-button-positive', - 'style': 'margin-left: 8px;', - 'click': function() { - var domain = document.getElementById('publish-domain').value.trim(); - var ssl = document.getElementById('publish-ssl').checked; - var acme = document.getElementById('publish-acme').checked; - - if (!domain) { - ui.addNotification(null, E('p', {}, _('Please enter a domain name')), 'error'); - return; - } - - self.publishInstance(inst, domain, lanIp, port, ssl, acme); - } - }, ['\uD83D\uDE80 ', _('Publish')]) - ]) - ]); - }, - - publishInstance: function(inst, domain, backendIp, backendPort, ssl, acme) { - var self = this; - var backendName = 'streamlit_' + inst.id; - - ui.hideModal(); - ui.showModal(_('Publishing...'), [ - E('p', { 'class': 'spinning' }, _('Creating HAProxy configuration...')) - ]); - - // Step 1: Create backend - haproxyCreateBackend(backendName, 'http', 'roundrobin', 'httpchk', '1') - .then(function(result) { - if (result && result.error) { - throw new Error(result.error); - } - // Step 2: Create server - return haproxyCreateServer(backendName, inst.id, backendIp, backendPort.toString(), '100', '1', '1'); - }) - .then(function(result) { - if (result && result.error) { - throw new Error(result.error); - } - // Step 3: Create vhost - var sslFlag = ssl ? '1' : '0'; - var acmeFlag = acme ? '1' : '0'; - return haproxyCreateVhost(domain, backendName, sslFlag, sslFlag, acmeFlag, '1'); - }) - .then(function(result) { - if (result && result.error) { - throw new Error(result.error); - } - // Step 4: Reload HAProxy - return haproxyReload(); - }) - .then(function() { - ui.hideModal(); - var msg = acme ? - _('Instance published! Certificate will be requested via cron.') : - _('Instance published successfully!'); - ui.addNotification(null, E('p', {}, [ - msg, - E('br'), - _('URL: '), - E('a', { - 'href': (ssl ? 'https://' : 'http://') + domain, - 'target': '_blank', - 'style': 'color: #0ff;' - }, (ssl ? 'https://' : 'http://') + domain) - ]), 'success'); - }) - .catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', {}, _('Publish failed: ') + (err.message || err)), 'error'); - }); - }, - - showEditDialog: function(inst) { - var self = this; - var appsList = this.appsData.apps || []; - - // Build app options - var appOptions = appsList.map(function(app) { - var selected = (inst.app === app.name + '.py') ? { 'selected': 'selected' } : {}; - return E('option', Object.assign({ 'value': app.name + '.py' }, selected), app.name); - }); - - ui.showModal(_('Edit Instance: ') + inst.id, [ - E('div', { 'class': 'st-form-group', 'style': 'margin-bottom: 12px;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 4px; font-weight: bold;' }, _('Display Name')), - E('input', { - 'type': 'text', - 'id': 'edit-inst-name', - 'value': inst.name || inst.id, - 'style': 'width: 100%; padding: 8px; border: 1px solid #334155; background: #1e293b; color: #fff; border-radius: 4px;' - }) - ]), - E('div', { 'class': 'st-form-group', 'style': 'margin-bottom: 12px;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 4px; font-weight: bold;' }, _('App File')), - E('select', { - 'id': 'edit-inst-app', - 'style': 'width: 100%; padding: 8px; border: 1px solid #334155; background: #1e293b; color: #fff; border-radius: 4px; height: 42px;' - }, appOptions) - ]), - E('div', { 'class': 'st-form-group', 'style': 'margin-bottom: 12px;' }, [ - E('label', { 'style': 'display: block; margin-bottom: 4px; font-weight: bold;' }, _('Port')), - E('input', { - 'type': 'number', - 'id': 'edit-inst-port', - 'value': inst.port, - 'min': '1024', - 'max': '65535', - 'style': 'width: 100%; padding: 8px; border: 1px solid #334155; background: #1e293b; color: #fff; border-radius: 4px;' - }) - ]), - E('div', { 'class': 'right', 'style': 'margin-top: 16px;' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Cancel')), - E('button', { - 'class': 'btn cbi-button-positive', - 'style': 'margin-left: 8px;', - 'click': function() { - var name = document.getElementById('edit-inst-name').value.trim(); - var app = document.getElementById('edit-inst-app').value; - var port = parseInt(document.getElementById('edit-inst-port').value, 10); - - if (!app) { - ui.addNotification(null, E('p', {}, _('Please select an app')), 'error'); - return; - } - - if (!port || port < 1024 || port > 65535) { - ui.addNotification(null, E('p', {}, _('Please enter a valid port')), 'error'); - return; - } - - self.saveInstanceEdit(inst.id, name, app, port); - } - }, ['\uD83D\uDCBE ', _('Save')]) - ]) - ]); - }, - - saveInstanceEdit: function(id, name, app, port) { - var self = this; - ui.hideModal(); - - // For now, we remove and re-add (since there's no update API) - // TODO: Add update_instance to the API - api.removeInstance(id).then(function() { - return api.addInstance(id, name, app, port); - }).then(function(result) { - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Instance updated: ') + id), 'success'); - self.refreshData(); - } else { - ui.addNotification(null, E('p', {}, result.message || _('Failed to update instance')), 'error'); - } - }).catch(function(err) { - ui.addNotification(null, E('p', {}, _('Error: ') + err.message), 'error'); - }); - } -}); diff --git a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/logs.js b/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/logs.js deleted file mode 100644 index faedb13b..00000000 --- a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/logs.js +++ /dev/null @@ -1,162 +0,0 @@ -'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'))); - } - } -}); diff --git a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/overview.js b/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/overview.js deleted file mode 100644 index 91cbf0af..00000000 --- a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/overview.js +++ /dev/null @@ -1,501 +0,0 @@ -'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')) - ]) - ]); - } -}); diff --git a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/settings.js b/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/settings.js index 53c094db..04bb146f 100644 --- a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/settings.js +++ b/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/settings.js @@ -1,257 +1,148 @@ 'use strict'; 'require view'; 'require ui'; -'require dom'; 'require streamlit.api as api'; return view.extend({ - configData: null, + config: {}, load: function() { - return api.getConfig().then(function(config) { - return config; + return api.getConfig().then(function(c) { + return c || {}; }); }, - render: function(configData) { + render: function(config) { var self = this; - this.configData = configData || {}; + this.config = config; + var main = config.main || {}; + var server = config.server || {}; - // Inject CSS - var cssLink = E('link', { - 'rel': 'stylesheet', - 'type': 'text/css', - 'href': L.resource('streamlit/dashboard.css') - }); + return E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, _('Streamlit Settings')), - var container = E('div', { 'class': 'streamlit-dashboard' }, [ - cssLink, - this.renderHeader(), - E('div', { 'class': 'st-main-grid' }, [ - this.renderMainSettings(), - this.renderServerSettings() - ]) - ]); + // Main Settings + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Service')), - return container; - }, - - renderHeader: function() { - var self = this; - - return E('div', { 'class': 'st-header' }, [ - E('div', { 'class': 'st-header-content' }, [ - E('div', { 'class': 'st-logo' }, '\u2699\uFE0F'), - E('div', {}, [ - E('h1', { 'class': 'st-title' }, _('SETTINGS')), - E('p', { 'class': 'st-subtitle' }, _('Configure Streamlit Platform options')) + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Enabled')), + E('div', { 'class': 'cbi-value-field' }, + E('select', { 'id': 'cfg-enabled', 'class': 'cbi-input-select' }, [ + E('option', { 'value': '1', 'selected': main.enabled == '1' || main.enabled === true }, _('Yes')), + E('option', { 'value': '0', 'selected': main.enabled == '0' || main.enabled === false }, _('No')) + ]) + ) ]), - E('div', { 'class': 'st-btn-group' }, [ - E('button', { - 'class': 'st-btn st-btn-success', - 'click': function() { self.saveSettings(); } - }, [E('span', {}, '\uD83D\uDCBE'), ' ' + _('Save Settings')]) - ]) - ]) - ]); - }, - renderMainSettings: function() { - var config = this.configData.main || {}; - var isEnabled = config.enabled === true || config.enabled === 1 || config.enabled === '1'; + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('HTTP Port')), + E('div', { 'class': 'cbi-value-field' }, + E('input', { 'type': 'number', 'id': 'cfg-port', 'class': 'cbi-input-text', + 'value': main.http_port || '8501', 'min': '1024', 'max': '65535' }) + ) + ]), - // Normalize memory limit for comparison - var memLimit = config.memory_limit || '1024M'; - if (memLimit === '1G') memLimit = '1024M'; - if (memLimit === '2G') memLimit = '2048M'; - if (memLimit === '4G') memLimit = '4096M'; + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Listen Address')), + E('div', { 'class': 'cbi-value-field' }, + E('input', { 'type': 'text', 'id': 'cfg-host', 'class': 'cbi-input-text', + 'value': main.http_host || '0.0.0.0' }) + ) + ]), - return E('div', { 'class': 'st-card' }, [ - E('div', { 'class': 'st-card-header' }, [ - E('div', { 'class': 'st-card-title' }, [ - E('span', {}, '\uD83D\uDD27'), - ' ' + _('Main Settings') + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Data Path')), + E('div', { 'class': 'cbi-value-field' }, + E('input', { 'type': 'text', 'id': 'cfg-path', 'class': 'cbi-input-text', + 'value': main.data_path || '/srv/streamlit' }) + ) + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Memory Limit')), + E('div', { 'class': 'cbi-value-field' }, + E('select', { 'id': 'cfg-memory', 'class': 'cbi-input-select' }, [ + E('option', { 'value': '512M', 'selected': main.memory_limit === '512M' }, '512 MB'), + E('option', { 'value': '1G', 'selected': main.memory_limit === '1G' }, '1 GB'), + E('option', { 'value': '2G', 'selected': main.memory_limit === '2G' || !main.memory_limit }, '2 GB'), + E('option', { 'value': '4G', 'selected': main.memory_limit === '4G' }, '4 GB') + ]) + ) + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Active App')), + E('div', { 'class': 'cbi-value-field' }, + E('input', { 'type': 'text', 'id': 'cfg-app', 'class': 'cbi-input-text', + 'value': main.active_app || 'hello' }) + ) ]) ]), - E('div', { 'class': 'st-card-body' }, [ - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('Enabled')), - E('select', { - 'class': 'st-form-input', - 'id': 'cfg-enabled', - 'style': 'height: 42px;' - }, [ - E('option', Object.assign({ 'value': '1' }, isEnabled ? { 'selected': 'selected' } : {}), _('Enabled')), - E('option', Object.assign({ 'value': '0' }, !isEnabled ? { 'selected': 'selected' } : {}), _('Disabled')) - ]) - ]), - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('HTTP Port')), - E('input', { - 'type': 'number', - 'class': 'st-form-input', - 'id': 'cfg-http_port', - 'value': config.http_port || 8501, - 'min': 1, - 'max': 65535 - }) - ]), - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('HTTP Host')), - E('input', { - 'type': 'text', - 'class': 'st-form-input', - 'id': 'cfg-http_host', - 'value': config.http_host || '0.0.0.0', - 'placeholder': '0.0.0.0' - }) - ]), - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('Data Path')), - E('input', { - 'type': 'text', - 'class': 'st-form-input', - 'id': 'cfg-data_path', - 'value': config.data_path || '/srv/streamlit', - 'placeholder': '/srv/streamlit' - }) - ]), - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('Memory Limit')), - E('select', { - 'class': 'st-form-input', - 'id': 'cfg-memory_limit', - 'style': 'height: 42px;' - }, [ - E('option', Object.assign({ 'value': '256M' }, memLimit === '256M' ? { 'selected': 'selected' } : {}), '256 MB'), - E('option', Object.assign({ 'value': '512M' }, memLimit === '512M' ? { 'selected': 'selected' } : {}), '512 MB'), - E('option', Object.assign({ 'value': '1024M' }, memLimit === '1024M' ? { 'selected': 'selected' } : {}), '1 GB'), - E('option', Object.assign({ 'value': '2048M' }, memLimit === '2048M' ? { 'selected': 'selected' } : {}), '2 GB'), - E('option', Object.assign({ 'value': '4096M' }, memLimit === '4096M' ? { 'selected': 'selected' } : {}), '4 GB') - ]) - ]), - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('Active App')), - E('input', { - 'type': 'text', - 'class': 'st-form-input', - 'id': 'cfg-active_app', - 'value': config.active_app || 'hello', - 'placeholder': 'hello' - }) - ]) - ]) - ]); - }, - renderServerSettings: function() { - var config = this.configData.server || {}; + // Server Settings + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Server Options')), - // Normalize boolean values (can be true/false boolean or "true"/"false" string) - var isHeadless = config.headless === true || config.headless === 'true'; - var gatherStats = config.browser_gather_usage_stats === true || config.browser_gather_usage_stats === 'true'; - var themeBase = config.theme_base || 'dark'; + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Headless')), + E('div', { 'class': 'cbi-value-field' }, + E('select', { 'id': 'cfg-headless', 'class': 'cbi-input-select' }, [ + E('option', { 'value': 'true', 'selected': server.headless !== 'false' }, _('Yes')), + E('option', { 'value': 'false', 'selected': server.headless === 'false' }, _('No')) + ]) + ) + ]), - return E('div', { 'class': 'st-card' }, [ - E('div', { 'class': 'st-card-header' }, [ - E('div', { 'class': 'st-card-title' }, [ - E('span', {}, '\uD83C\uDFA8'), - ' ' + _('Server & Theme') + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Theme')), + E('div', { 'class': 'cbi-value-field' }, + E('select', { 'id': 'cfg-theme', 'class': 'cbi-input-select' }, [ + E('option', { 'value': 'dark', 'selected': server.theme_base !== 'light' }, _('Dark')), + E('option', { 'value': 'light', 'selected': server.theme_base === 'light' }, _('Light')) + ]) + ) + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Primary Color')), + E('div', { 'class': 'cbi-value-field' }, + E('input', { 'type': 'text', 'id': 'cfg-color', 'class': 'cbi-input-text', + 'value': server.theme_primary_color || '#0ff', 'placeholder': '#0ff' }) + ) ]) ]), - E('div', { 'class': 'st-card-body' }, [ - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('Headless Mode')), - E('select', { - 'class': 'st-form-input', - 'id': 'cfg-headless', - 'style': 'height: 42px;' - }, [ - E('option', Object.assign({ 'value': 'true' }, isHeadless ? { 'selected': 'selected' } : {}), _('Enabled (recommended)')), - E('option', Object.assign({ 'value': 'false' }, !isHeadless ? { 'selected': 'selected' } : {}), _('Disabled')) - ]) - ]), - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('Usage Statistics')), - E('select', { - 'class': 'st-form-input', - 'id': 'cfg-gather_stats', - 'style': 'height: 42px;' - }, [ - E('option', Object.assign({ 'value': 'false' }, !gatherStats ? { 'selected': 'selected' } : {}), _('Disabled (recommended)')), - E('option', Object.assign({ 'value': 'true' }, gatherStats ? { 'selected': 'selected' } : {}), _('Enabled')) - ]) - ]), - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('Theme Base')), - E('select', { - 'class': 'st-form-input', - 'id': 'cfg-theme_base', - 'style': 'height: 42px;' - }, [ - E('option', Object.assign({ 'value': 'dark' }, themeBase === 'dark' ? { 'selected': 'selected' } : {}), _('Dark')), - E('option', Object.assign({ 'value': 'light' }, themeBase === 'light' ? { 'selected': 'selected' } : {}), _('Light')) - ]) - ]), - E('div', { 'class': 'st-form-group' }, [ - E('label', { 'class': 'st-form-label' }, _('Primary Color')), - E('div', { 'style': 'display: flex; gap: 10px; align-items: center;' }, [ - E('input', { - 'type': 'color', - 'id': 'cfg-theme_primary_picker', - 'value': config.theme_primary_color || '#00ffff', - 'style': 'width: 50px; height: 40px; border: none; background: none; cursor: pointer;', - 'change': function() { - document.getElementById('cfg-theme_primary').value = this.value; - } - }), - E('input', { - 'type': 'text', - 'class': 'st-form-input', - 'id': 'cfg-theme_primary', - 'value': config.theme_primary_color || '#0ff', - 'placeholder': '#0ff', - 'style': 'flex: 1;', - 'change': function() { - document.getElementById('cfg-theme_primary_picker').value = this.value; - } - }) - ]) - ]), - E('div', { 'style': 'margin-top: 20px; padding: 16px; background: rgba(0, 255, 255, 0.05); border-radius: 8px; border: 1px solid rgba(0, 255, 255, 0.2);' }, [ - E('p', { 'style': 'color: #0ff; font-size: 13px; margin: 0;' }, [ - E('strong', {}, _('Note: ')), - _('Changes will take effect after restarting the Streamlit service.') - ]) - ]) + + // Save button + E('div', { 'class': 'cbi-page-actions' }, [ + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': function() { self.save(); } + }, _('Save & Apply')) ]) ]); }, - saveSettings: function() { - var self = this; - - var config = { + save: function() { + var cfg = { enabled: document.getElementById('cfg-enabled').value, - http_port: parseInt(document.getElementById('cfg-http_port').value, 10), - http_host: document.getElementById('cfg-http_host').value, - data_path: document.getElementById('cfg-data_path').value, - memory_limit: document.getElementById('cfg-memory_limit').value, - active_app: document.getElementById('cfg-active_app').value, + http_port: document.getElementById('cfg-port').value, + http_host: document.getElementById('cfg-host').value, + data_path: document.getElementById('cfg-path').value, + memory_limit: document.getElementById('cfg-memory').value, + active_app: document.getElementById('cfg-app').value, headless: document.getElementById('cfg-headless').value, - browser_gather_usage_stats: document.getElementById('cfg-gather_stats').value, - theme_base: document.getElementById('cfg-theme_base').value, - theme_primary_color: document.getElementById('cfg-theme_primary').value + browser_gather_usage_stats: 'false', + theme_base: document.getElementById('cfg-theme').value, + theme_primary_color: document.getElementById('cfg-color').value }; - api.saveConfig(config).then(function(result) { - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Settings saved successfully')), 'success'); + api.saveConfig(cfg).then(function(r) { + if (r && r.success) { + ui.addNotification(null, E('p', {}, _('Settings saved')), 'info'); } else { - ui.addNotification(null, E('p', {}, result.message || _('Failed to save settings')), 'error'); + ui.addNotification(null, E('p', {}, r.message || _('Save failed')), 'error'); } - }).catch(function(err) { - ui.addNotification(null, E('p', {}, _('Failed to save: ') + err.message), 'error'); }); } }); diff --git a/package/secubox/luci-app-streamlit/root/usr/share/luci/menu.d/luci-app-streamlit.json b/package/secubox/luci-app-streamlit/root/usr/share/luci/menu.d/luci-app-streamlit.json index ba874cbe..2020efd7 100644 --- a/package/secubox/luci-app-streamlit/root/usr/share/luci/menu.d/luci-app-streamlit.json +++ b/package/secubox/luci-app-streamlit/root/usr/share/luci/menu.d/luci-app-streamlit.json @@ -10,41 +10,17 @@ "uci": {"streamlit": true} } }, - "admin/services/streamlit/overview": { - "title": "Overview", + "admin/services/streamlit/dashboard": { + "title": "Dashboard", "order": 10, "action": { "type": "view", - "path": "streamlit/overview" - } - }, - "admin/services/streamlit/apps": { - "title": "Apps", - "order": 20, - "action": { - "type": "view", - "path": "streamlit/apps" - } - }, - "admin/services/streamlit/instances": { - "title": "Instances", - "order": 25, - "action": { - "type": "view", - "path": "streamlit/instances" - } - }, - "admin/services/streamlit/logs": { - "title": "Logs", - "order": 30, - "action": { - "type": "view", - "path": "streamlit/logs" + "path": "streamlit/dashboard" } }, "admin/services/streamlit/settings": { "title": "Settings", - "order": 40, + "order": 20, "action": { "type": "view", "path": "streamlit/settings"