diff --git a/package/secubox/luci-app-streamlit/Makefile b/package/secubox/luci-app-streamlit/Makefile index 7d72eb67..c4706355 100644 --- a/package/secubox/luci-app-streamlit/Makefile +++ b/package/secubox/luci-app-streamlit/Makefile @@ -8,14 +8,14 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-streamlit PKG_VERSION:=1.0.0 -PKG_RELEASE:=9 +PKG_RELEASE:=11 PKG_ARCH:=all PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=CyberMind LUCI_TITLE:=LuCI Streamlit Dashboard -LUCI_DESCRIPTION:=Modern dashboard for Streamlit Platform management on OpenWrt +LUCI_DESCRIPTION:=Multi-instance Streamlit management with Gitea integration LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +rpcd +rpcd-mod-luci +secubox-app-streamlit LUCI_PKGARCH:=all diff --git a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/streamlit/api.js b/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/streamlit/api.js index efc57729..d752038f 100644 --- a/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/streamlit/api.js +++ b/package/secubox/luci-app-streamlit/htdocs/luci-static/resources/streamlit/api.js @@ -164,6 +164,39 @@ var callDisableInstance = rpc.declare({ expect: { result: {} } }); +var callGetGiteaConfig = rpc.declare({ + object: 'luci.streamlit', + method: 'get_gitea_config', + expect: { result: {} } +}); + +var callSaveGiteaConfig = rpc.declare({ + object: 'luci.streamlit', + method: 'save_gitea_config', + params: ['enabled', 'url', 'user', 'token'], + expect: { result: {} } +}); + +var callGiteaClone = rpc.declare({ + object: 'luci.streamlit', + method: 'gitea_clone', + params: ['name', 'repo'], + expect: { result: {} } +}); + +var callGiteaPull = rpc.declare({ + object: 'luci.streamlit', + method: 'gitea_pull', + params: ['name'], + expect: { result: {} } +}); + +var callGiteaListRepos = rpc.declare({ + object: 'luci.streamlit', + method: 'gitea_list_repos', + expect: { result: {} } +}); + return baseclass.extend({ getStatus: function() { return callGetStatus(); @@ -282,6 +315,26 @@ return baseclass.extend({ return callDisableInstance(id); }, + getGiteaConfig: function() { + return callGetGiteaConfig(); + }, + + saveGiteaConfig: function(enabled, url, user, token) { + return callSaveGiteaConfig(enabled, url, user, token); + }, + + giteaClone: function(name, repo) { + return callGiteaClone(name, repo); + }, + + giteaPull: function(name) { + return callGiteaPull(name); + }, + + giteaListRepos: function() { + return callGiteaListRepos(); + }, + getDashboardData: function() { var self = this; return Promise.all([ 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 index 8a527372..2ccd5483 100644 --- 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 @@ -7,7 +7,9 @@ return view.extend({ status: {}, apps: [], + instances: [], activeApp: '', + giteaConfig: {}, load: function() { return this.refresh(); @@ -17,11 +19,15 @@ return view.extend({ var self = this; return Promise.all([ api.getStatus(), - api.listApps() + api.listApps(), + api.listInstances(), + api.getGiteaConfig().catch(function() { return {}; }) ]).then(function(r) { self.status = r[0] || {}; self.apps = (r[1] && r[1].apps) || []; self.activeApp = (r[1] && r[1].active_app) || ''; + self.instances = r[2] || []; + self.giteaConfig = r[3] || {}; }); }, @@ -33,7 +39,7 @@ return view.extend({ var view = E('div', { 'class': 'cbi-map' }, [ E('h2', {}, _('Streamlit Platform')), - E('div', { 'class': 'cbi-map-descr' }, _('Python data app hosting')), + E('div', { 'class': 'cbi-map-descr' }, _('Python data app hosting with multi-instance support')), // Status Section E('div', { 'class': 'cbi-section', 'id': 'status-section' }, [ @@ -59,6 +65,18 @@ return view.extend({ E('div', { 'class': 'cbi-page-actions' }, this.renderControls(installed, running)) ]), + // Instances Section + E('div', { 'class': 'cbi-section', 'id': 'instances-section' }, [ + E('h3', {}, _('Running Instances')), + E('div', { 'id': 'instances-table' }, this.renderInstancesTable()) + ]), + + // Add Instance Section + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Add Instance')), + this.renderAddInstanceForm() + ]), + // Apps Section E('div', { 'class': 'cbi-section', 'id': 'apps-section' }, [ E('h3', {}, _('Applications')), @@ -69,16 +87,23 @@ return view.extend({ E('div', { 'class': 'cbi-section' }, [ E('h3', {}, _('Upload App')), E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Python File')), + E('label', { 'class': 'cbi-value-title' }, _('File')), E('div', { 'class': 'cbi-value-field' }, [ - E('input', { 'type': 'file', 'id': 'upload-file', 'accept': '.py' }), + E('input', { 'type': 'file', 'id': 'upload-file', 'accept': '.py,.zip' }), E('button', { 'class': 'cbi-button cbi-button-action', 'style': 'margin-left: 8px', 'click': function() { self.uploadApp(); } }, _('Upload')) - ]) + ]), + E('div', { 'class': 'cbi-value-description' }, _('Accepts .py files or .zip archives')) ]) + ]), + + // Gitea Section + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Clone from Gitea')), + this.renderGiteaSection() ]) ]); @@ -127,6 +152,105 @@ return view.extend({ return btns; }, + renderInstancesTable: function() { + var self = this; + var instances = this.instances; + + if (!instances.length) { + return E('em', {}, _('No instances configured. Add one below.')); + } + + var rows = instances.map(function(inst) { + var statusIcon = inst.enabled ? + E('span', { 'style': 'color:#0a0' }, '\u25CF') : + E('span', { 'style': 'color:#999' }, '\u25CB'); + + return E('tr', {}, [ + E('td', {}, [ + E('strong', {}, inst.id), + inst.name && inst.name !== inst.id ? + E('span', { 'style': 'color:#666; margin-left:4px' }, '(' + inst.name + ')') : '' + ]), + E('td', {}, inst.app || '-'), + E('td', {}, ':' + inst.port), + E('td', { 'style': 'text-align:center' }, [ + statusIcon, + E('span', { 'style': 'margin-left:4px' }, inst.enabled ? _('Enabled') : _('Disabled')) + ]), + E('td', {}, [ + inst.enabled ? + E('button', { + 'class': 'cbi-button', + 'click': function() { self.disableInstance(inst.id); } + }, _('Disable')) : + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': function() { self.enableInstance(inst.id); } + }, _('Enable')), + E('button', { + 'class': 'cbi-button cbi-button-remove', + 'style': 'margin-left: 4px', + 'click': function() { self.deleteInstance(inst.id); } + }, _('Delete')) + ]) + ]); + }); + + return E('table', { 'class': 'table cbi-section-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('ID')), + E('th', { 'class': 'th' }, _('App')), + E('th', { 'class': 'th' }, _('Port')), + E('th', { 'class': 'th', 'style': 'text-align:center' }, _('Status')), + E('th', { 'class': 'th' }, _('Actions')) + ]) + ].concat(rows)); + }, + + renderAddInstanceForm: function() { + var self = this; + var appOptions = [E('option', { 'value': '' }, _('-- Select App --'))]; + + this.apps.forEach(function(app) { + appOptions.push(E('option', { 'value': app.name }, app.name)); + }); + + // Calculate next available port + var usedPorts = this.instances.map(function(i) { return i.port; }); + var nextPort = 8501; + while (usedPorts.indexOf(nextPort) !== -1) { + nextPort++; + } + + return E('div', {}, [ + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Instance ID')), + E('div', { 'class': 'cbi-value-field' }, + E('input', { 'type': 'text', 'id': 'new-inst-id', 'class': 'cbi-input-text', 'placeholder': _('myapp') }) + ) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('App')), + E('div', { 'class': 'cbi-value-field' }, + E('select', { 'id': 'new-inst-app', 'class': 'cbi-input-select' }, appOptions) + ) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Port')), + E('div', { 'class': 'cbi-value-field' }, + E('input', { 'type': 'number', 'id': 'new-inst-port', 'class': 'cbi-input-text', + 'value': nextPort, 'min': '1024', 'max': '65535' }) + ) + ]), + E('div', { 'class': 'cbi-page-actions' }, [ + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': function() { self.addInstance(); } + }, _('Add Instance')) + ]) + ]); + }, + renderAppsTable: function() { var self = this; var apps = this.apps; @@ -166,11 +290,52 @@ return view.extend({ ].concat(rows)); }, + renderGiteaSection: function() { + var self = this; + var cfg = this.giteaConfig; + var enabled = cfg.enabled; + + return E('div', {}, [ + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Status')), + E('div', { 'class': 'cbi-value-field' }, + enabled ? + E('span', { 'style': 'color:#0a0' }, _('Configured')) : + E('span', { 'style': 'color:#999' }, _('Not configured')) + ) + ]), + enabled ? E('div', {}, [ + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Repository')), + E('div', { 'class': 'cbi-value-field' }, + E('input', { 'type': 'text', 'id': 'gitea-repo', 'class': 'cbi-input-text', + 'placeholder': _('owner/repo or full URL') }) + ) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('App Name')), + E('div', { 'class': 'cbi-value-field' }, + E('input', { 'type': 'text', 'id': 'gitea-name', 'class': 'cbi-input-text', + 'placeholder': _('myapp') }) + ) + ]), + E('div', { 'class': 'cbi-page-actions' }, [ + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': function() { self.giteaClone(); } + }, _('Clone')) + ]) + ]) : E('div', { 'class': 'cbi-value-description' }, + _('Configure Gitea in Settings to enable cloning')) + ]); + }, + updateStatus: function() { var s = this.status; var statusEl = document.getElementById('svc-status'); var activeEl = document.getElementById('active-app'); var appsEl = document.getElementById('apps-table'); + var instancesEl = document.getElementById('instances-table'); if (statusEl) { statusEl.innerHTML = ''; @@ -191,6 +356,11 @@ return view.extend({ appsEl.innerHTML = ''; appsEl.appendChild(this.renderAppsTable()); } + + if (instancesEl) { + instancesEl.innerHTML = ''; + instancesEl.appendChild(this.renderInstancesTable()); + } }, doStart: function() { @@ -280,6 +450,91 @@ return view.extend({ ]); }, + addInstance: function() { + var self = this; + var id = document.getElementById('new-inst-id').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; + } + + api.addInstance(id, id, app, port).then(function(r) { + if (r && r.success) { + ui.addNotification(null, E('p', {}, _('Instance added: ') + id), 'success'); + document.getElementById('new-inst-id').value = ''; + document.getElementById('new-inst-app').value = ''; + self.refresh().then(function() { self.updateStatus(); }); + } else { + ui.addNotification(null, E('p', {}, r.message || _('Failed to add instance')), 'error'); + } + }); + }, + + enableInstance: function(id) { + var self = this; + api.enableInstance(id).then(function(r) { + if (r && r.success) { + ui.addNotification(null, E('p', {}, _('Instance enabled: ') + id), 'success'); + self.refresh().then(function() { self.updateStatus(); }); + } else { + ui.addNotification(null, E('p', {}, r.message || _('Failed')), 'error'); + } + }); + }, + + disableInstance: function(id) { + var self = this; + api.disableInstance(id).then(function(r) { + if (r && r.success) { + ui.addNotification(null, E('p', {}, _('Instance disabled: ') + id), 'success'); + self.refresh().then(function() { self.updateStatus(); }); + } else { + ui.addNotification(null, E('p', {}, r.message || _('Failed')), 'error'); + } + }); + }, + + deleteInstance: function(id) { + var self = this; + ui.showModal(_('Confirm'), [ + E('p', {}, _('Delete instance: ') + id + '?'), + 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.removeInstance(id).then(function(r) { + if (r && r.success) { + ui.addNotification(null, E('p', {}, _('Instance deleted')), 'info'); + } + self.refresh().then(function() { self.updateStatus(); }); + }); + } + }, _('Delete')) + ]) + ]); + }, + activateApp: function(name) { var self = this; api.setActiveApp(name).then(function(r) { @@ -288,7 +543,7 @@ return view.extend({ return api.restart(); } }).then(function() { - self.refresh(); + self.refresh().then(function() { self.updateStatus(); }); }); }, @@ -307,7 +562,7 @@ return view.extend({ if (r && r.success) { ui.addNotification(null, E('p', {}, _('App deleted')), 'info'); } - self.refresh(); + self.refresh().then(function() { self.updateStatus(); }); }); } }, _('Delete')) @@ -324,22 +579,63 @@ return view.extend({ } var file = fileInput.files[0]; - var name = file.name.replace(/\.py$/, ''); + var name = file.name.replace(/\.(py|zip)$/, ''); + var isZip = file.name.endsWith('.zip'); var reader = new FileReader(); reader.onload = function(e) { var content = btoa(e.target.result); - api.uploadApp(name, content).then(function(r) { + var uploadFn = isZip ? api.uploadZip(name, content, null) : api.uploadApp(name, content); + + uploadFn.then(function(r) { if (r && r.success) { ui.addNotification(null, E('p', {}, _('App uploaded: ') + name), 'success'); fileInput.value = ''; - self.refresh(); + self.refresh().then(function() { self.updateStatus(); }); } else { ui.addNotification(null, E('p', {}, r.message || _('Upload failed')), 'error'); } }); }; - reader.readAsText(file); + if (isZip) { + reader.readAsBinaryString(file); + } else { + reader.readAsText(file); + } + }, + + giteaClone: function() { + var self = this; + var repo = document.getElementById('gitea-repo').value.trim(); + var name = document.getElementById('gitea-name').value.trim(); + + if (!repo) { + ui.addNotification(null, E('p', {}, _('Please enter a repository')), 'error'); + return; + } + + if (!name) { + // Extract name from repo + name = repo.split('/').pop().replace(/\.git$/, ''); + } + + ui.showModal(_('Cloning'), [ + E('p', { 'class': 'spinning' }, _('Cloning ') + repo + '...') + ]); + + api.giteaClone(name, repo).then(function(r) { + ui.hideModal(); + if (r && r.success) { + ui.addNotification(null, E('p', {}, _('Clone started: ') + name), 'success'); + document.getElementById('gitea-repo').value = ''; + document.getElementById('gitea-name').value = ''; + setTimeout(function() { + self.refresh().then(function() { self.updateStatus(); }); + }, 3000); + } else { + ui.addNotification(null, E('p', {}, r.message || _('Clone failed')), 'error'); + } + }); } }); 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 04bb146f..c35dd1c5 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 @@ -5,18 +5,24 @@ return view.extend({ config: {}, + giteaConfig: {}, load: function() { - return api.getConfig().then(function(c) { - return c || {}; + var self = this; + return Promise.all([ + api.getConfig().then(function(c) { return c || {}; }), + api.getGiteaConfig().catch(function() { return {}; }) + ]).then(function(r) { + self.config = r[0]; + self.giteaConfig = r[1]; }); }, - render: function(config) { + render: function() { var self = this; - this.config = config; - var main = config.main || {}; - var server = config.server || {}; + var main = this.config.main || {}; + var server = this.config.server || {}; + var gitea = this.giteaConfig || {}; return E('div', { 'class': 'cbi-map' }, [ E('h2', {}, _('Streamlit Settings')), @@ -113,7 +119,49 @@ return view.extend({ ]) ]), - // Save button + // Gitea Settings + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Gitea Integration')), + E('div', { 'class': 'cbi-section-descr' }, _('Configure Gitea to clone apps from repositories')), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Enabled')), + E('div', { 'class': 'cbi-value-field' }, + E('select', { 'id': 'cfg-gitea-enabled', 'class': 'cbi-input-select' }, [ + E('option', { 'value': '1', 'selected': gitea.enabled == true || gitea.enabled == '1' }, _('Yes')), + E('option', { 'value': '0', 'selected': !gitea.enabled || gitea.enabled == '0' }, _('No')) + ]) + ) + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Gitea URL')), + E('div', { 'class': 'cbi-value-field' }, + E('input', { 'type': 'text', 'id': 'cfg-gitea-url', 'class': 'cbi-input-text', + 'value': gitea.url || '', 'placeholder': 'http://192.168.255.1:3000' }) + ) + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Username')), + E('div', { 'class': 'cbi-value-field' }, + E('input', { 'type': 'text', 'id': 'cfg-gitea-user', 'class': 'cbi-input-text', + 'value': gitea.user || '', 'placeholder': 'admin' }) + ) + ]), + + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Access Token')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { 'type': 'password', 'id': 'cfg-gitea-token', 'class': 'cbi-input-text', + 'value': '', 'placeholder': gitea.has_token ? _('(token configured)') : _('Enter token') }), + E('div', { 'class': 'cbi-value-description' }, + _('Generate from Gitea: Settings > Applications > Generate Token')) + ]) + ]) + ]), + + // Save buttons E('div', { 'class': 'cbi-page-actions' }, [ E('button', { 'class': 'cbi-button cbi-button-positive', @@ -124,6 +172,9 @@ return view.extend({ }, save: function() { + var self = this; + + // Save main config var cfg = { enabled: document.getElementById('cfg-enabled').value, http_port: document.getElementById('cfg-port').value, @@ -137,12 +188,24 @@ return view.extend({ theme_primary_color: document.getElementById('cfg-color').value }; - api.saveConfig(cfg).then(function(r) { + // Save Gitea config + var giteaEnabled = document.getElementById('cfg-gitea-enabled').value; + var giteaUrl = document.getElementById('cfg-gitea-url').value; + var giteaUser = document.getElementById('cfg-gitea-user').value; + var giteaToken = document.getElementById('cfg-gitea-token').value; + + Promise.all([ + api.saveConfig(cfg), + api.saveGiteaConfig(giteaEnabled, giteaUrl, giteaUser, giteaToken || '') + ]).then(function(results) { + var r = results[0]; if (r && r.success) { ui.addNotification(null, E('p', {}, _('Settings saved')), 'info'); } else { ui.addNotification(null, E('p', {}, r.message || _('Save failed')), 'error'); } + }).catch(function(err) { + ui.addNotification(null, E('p', {}, _('Save failed: ') + err.message), 'error'); }); } }); diff --git a/package/secubox/luci-app-streamlit/root/usr/libexec/rpcd/luci.streamlit b/package/secubox/luci-app-streamlit/root/usr/libexec/rpcd/luci.streamlit index 58fefb99..6c81dfab 100755 --- a/package/secubox/luci-app-streamlit/root/usr/libexec/rpcd/luci.streamlit +++ b/package/secubox/luci-app-streamlit/root/usr/libexec/rpcd/luci.streamlit @@ -712,6 +712,130 @@ upload_zip() { fi } +# Get Gitea config +get_gitea_config() { + config_load "$CONFIG" + local enabled url user token + + config_get enabled gitea enabled "0" + config_get url gitea url "" + config_get user gitea user "" + config_get token gitea token "" + + json_init_obj + json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )" + json_add_string "url" "$url" + json_add_string "user" "$user" + json_add_boolean "has_token" "$( [ -n "$token" ] && echo 1 || echo 0 )" + json_close_obj +} + +# Save Gitea config +save_gitea_config() { + read -r input + local enabled url user token + + enabled=$(echo "$input" | jsonfilter -e '@.enabled' 2>/dev/null) + url=$(echo "$input" | jsonfilter -e '@.url' 2>/dev/null) + user=$(echo "$input" | jsonfilter -e '@.user' 2>/dev/null) + token=$(echo "$input" | jsonfilter -e '@.token' 2>/dev/null) + + # Ensure gitea section exists + uci -q get "${CONFIG}.gitea" >/dev/null || uci set "${CONFIG}.gitea=gitea" + + [ -n "$enabled" ] && uci set "${CONFIG}.gitea.enabled=$enabled" + [ -n "$url" ] && uci set "${CONFIG}.gitea.url=$url" + [ -n "$user" ] && uci set "${CONFIG}.gitea.user=$user" + [ -n "$token" ] && uci set "${CONFIG}.gitea.token=$token" + + uci commit "$CONFIG" + + json_success "Gitea configuration saved" +} + +# Clone app from Gitea +gitea_clone() { + read -r input + local name repo + + name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) + repo=$(echo "$input" | jsonfilter -e '@.repo' 2>/dev/null) + + if [ -z "$name" ] || [ -z "$repo" ]; then + json_error "Missing name or repo" + return + fi + + # Run clone in background + /usr/sbin/streamlitctl gitea clone "$name" "$repo" >/var/log/streamlit-gitea.log 2>&1 & + + json_init_obj + json_add_boolean "success" 1 + json_add_string "message" "Cloning $repo to $name in background" + json_close_obj +} + +# Pull app from Gitea +gitea_pull() { + read -r input + local name + + name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) + + if [ -z "$name" ]; then + json_error "Missing app name" + return + fi + + # Run pull + /usr/sbin/streamlitctl gitea pull "$name" >/var/log/streamlit-gitea.log 2>&1 + + if [ $? -eq 0 ]; then + json_success "App updated from Gitea: $name" + else + json_error "Failed to pull app from Gitea" + fi +} + +# List Gitea repositories +gitea_list_repos() { + config_load "$CONFIG" + local enabled url user token + + config_get enabled gitea enabled "0" + config_get url gitea url "" + config_get user gitea user "" + config_get token gitea token "" + + if [ "$enabled" != "1" ] || [ -z "$url" ] || [ -z "$token" ]; then + json_error "Gitea not configured" + return + fi + + # Call Gitea API to list user repos + local api_url="${url}/api/v1/user/repos" + local response + + response=$(curl -s -H "Authorization: token $token" "$api_url" 2>/dev/null) + + if [ -z "$response" ]; then + json_error "Failed to connect to Gitea" + return + fi + + json_init_obj + json_add_array "repos" + + # Parse JSON response (simple extraction) + echo "$response" | jsonfilter -e '@[*].full_name' 2>/dev/null | while read -r repo; do + [ -z "$repo" ] && continue + json_add_string "" "$repo" + done + + json_close_array + json_close_obj +} + # Check install progress get_install_progress() { local log_file="/var/log/streamlit-install.log" @@ -793,7 +917,12 @@ case "$1" in "add_instance": {"id": "str", "name": "str", "app": "str", "port": 8501}, "remove_instance": {"id": "str"}, "enable_instance": {"id": "str"}, - "disable_instance": {"id": "str"} + "disable_instance": {"id": "str"}, + "get_gitea_config": {}, + "save_gitea_config": {"enabled": "str", "url": "str", "user": "str", "token": "str"}, + "gitea_clone": {"name": "str", "repo": "str"}, + "gitea_pull": {"name": "str"}, + "gitea_list_repos": {} } EOF ;; @@ -871,6 +1000,21 @@ case "$1" in disable_instance) disable_instance ;; + get_gitea_config) + get_gitea_config + ;; + save_gitea_config) + save_gitea_config + ;; + gitea_clone) + gitea_clone + ;; + gitea_pull) + gitea_pull + ;; + gitea_list_repos) + gitea_list_repos + ;; *) json_error "Unknown method: $2" ;; diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-streamlit_1.0.0-r11_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-streamlit_1.0.0-r11_all.ipk new file mode 100644 index 00000000..3b072da0 Binary files /dev/null and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-streamlit_1.0.0-r11_all.ipk differ