secubox-openwrt/package/secubox/luci-app-rezapp/htdocs/luci-static/resources/view/rezapp/overview.js
CyberMind-FR 66b58c74d6 feat(catalog): Add Streamlit Forge and RezApp Forge to KISS Apps
- luci-app-streamlit-forge: Streamlit app publishing platform
  - Category: productivity, runtime: lxc
  - Templates, SSL exposure, mesh publishing

- luci-app-rezapp: Docker to LXC app converter
  - Category: system, runtime: native
  - Catalog browsing, package generation

- Updated new_releases section
- Total plugins: 37 → 39

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-11 09:44:08 +01:00

397 lines
11 KiB
JavaScript

'use strict';
'require view';
'require rpc';
'require ui';
'require poll';
var callStatus = rpc.declare({
object: 'luci.rezapp',
method: 'status',
expect: {}
});
var callCatalogs = rpc.declare({
object: 'luci.rezapp',
method: 'catalogs',
expect: {}
});
var callApps = rpc.declare({
object: 'luci.rezapp',
method: 'apps',
expect: {}
});
var callSearch = rpc.declare({
object: 'luci.rezapp',
method: 'search',
params: ['query'],
expect: {}
});
var callInfo = rpc.declare({
object: 'luci.rezapp',
method: 'info',
params: ['image'],
expect: {}
});
var callConvert = rpc.declare({
object: 'luci.rezapp',
method: 'convert',
params: ['image', 'name', 'tag', 'memory'],
expect: {}
});
var callPackage = rpc.declare({
object: 'luci.rezapp',
method: 'package',
params: ['name'],
expect: {}
});
var callPublish = rpc.declare({
object: 'luci.rezapp',
method: 'publish',
params: ['name'],
expect: {}
});
var callDelete = rpc.declare({
object: 'luci.rezapp',
method: 'delete',
params: ['name'],
expect: {}
});
return view.extend({
status: {},
catalogs: [],
apps: [],
searchResults: [],
load: function() {
return Promise.all([
callStatus(),
callCatalogs(),
callApps()
]);
},
render: function(data) {
var self = this;
this.status = data[0] || {};
this.catalogs = (data[1] && data[1].catalogs) || [];
this.apps = (data[2] && data[2].apps) || [];
var view = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'RezApp Forge'),
E('div', { 'class': 'cbi-map-descr' },
'Convert Docker images to SecuBox LXC apps.'),
// Status cards
E('div', { 'class': 'sh-stats', 'style': 'display:flex;gap:1rem;margin:1rem 0;flex-wrap:wrap;' }, [
this.renderStatCard('Converted Apps', this.status.apps || 0, '#2196f3'),
this.renderStatCard('Catalogs', this.status.catalogs || 0, '#9c27b0'),
this.renderStatCard('Docker', this.status.docker_status || 'unknown',
this.status.docker_status === 'running' ? '#4caf50' : '#ff9800')
]),
// Search section
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Search Docker Hub'),
E('div', { 'style': 'display:flex;gap:0.5rem;margin-bottom:1rem;' }, [
E('input', {
'type': 'text',
'id': 'search-query',
'class': 'cbi-input-text',
'placeholder': 'Search images (e.g., nginx, heimdall)',
'style': 'flex:1;'
}),
E('button', {
'class': 'cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, 'handleSearch')
}, 'Search')
]),
E('div', { 'id': 'search-results' })
]),
// Converted apps section
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Converted Apps'),
E('div', { 'id': 'apps-table' }, this.renderAppsTable())
])
]);
poll.add(L.bind(this.pollApps, this), 15);
return view;
},
renderStatCard: function(label, value, color) {
return E('div', {
'style': 'background:#1a1a2e;border-left:4px solid ' + color + ';padding:1rem;min-width:120px;border-radius:4px;'
}, [
E('div', { 'style': 'font-size:1.5rem;font-weight:bold;color:' + color }, String(value)),
E('div', { 'style': 'color:#888;font-size:0.85rem;' }, label)
]);
},
renderAppsTable: function() {
if (!this.apps.length) {
return E('p', { 'class': 'cbi-value-description' },
'No converted apps yet. Search and convert a Docker image to get started.');
}
var rows = this.apps.map(L.bind(function(app) {
var statusBadge = E('span', {
'class': 'badge',
'style': 'background:' + (app.lxc_status === 'running' ? '#4caf50' : app.lxc_status === 'stopped' ? '#ff9800' : '#666') + ';color:#fff;padding:2px 8px;border-radius:10px;font-size:0.8rem;'
}, app.lxc_status || 'none');
var packageBadge = E('span', {
'style': 'color:' + (app.packaged === 'yes' ? '#4caf50' : '#666')
}, app.packaged === 'yes' ? 'Yes' : 'No');
var actions = E('div', { 'style': 'display:flex;gap:4px;flex-wrap:wrap;' }, [
app.packaged !== 'yes' ?
E('button', {
'class': 'cbi-button',
'style': 'padding:4px 8px;font-size:0.8rem;',
'click': ui.createHandlerFn(this, 'handlePackage', app.name)
}, 'Package') : '',
E('button', {
'class': 'cbi-button',
'style': 'padding:4px 8px;font-size:0.8rem;',
'click': ui.createHandlerFn(this, 'handlePublish', app.name)
}, 'Publish'),
E('button', {
'class': 'cbi-button cbi-button-remove',
'style': 'padding:4px 8px;font-size:0.8rem;',
'click': ui.createHandlerFn(this, 'handleDelete', app.name)
}, 'Delete')
]);
return E('tr', {}, [
E('td', {}, app.name),
E('td', { 'style': 'font-size:0.85rem;color:#888;' }, app.source_image || '-'),
E('td', {}, statusBadge),
E('td', {}, packageBadge),
E('td', { 'style': 'font-size:0.85rem;' }, app.converted_at || '-'),
E('td', {}, actions)
]);
}, this));
return E('table', { 'class': 'table cbi-section-table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, 'Name'),
E('th', { 'class': 'th' }, 'Source Image'),
E('th', { 'class': 'th' }, 'LXC Status'),
E('th', { 'class': 'th' }, 'Packaged'),
E('th', { 'class': 'th' }, 'Converted'),
E('th', { 'class': 'th' }, 'Actions')
])
].concat(rows));
},
renderSearchResults: function() {
var container = document.getElementById('search-results');
if (!container) return;
if (!this.searchResults.length) {
container.innerHTML = '';
return;
}
var rows = this.searchResults.map(L.bind(function(result) {
return E('tr', {}, [
E('td', {}, [
E('strong', {}, result.image),
E('span', { 'style': 'margin-left:0.5rem;color:#ffd700;' }, result.stars ? result.stars + '*' : '')
]),
E('td', { 'style': 'font-size:0.85rem;color:#888;max-width:300px;overflow:hidden;text-overflow:ellipsis;' },
result.description || ''),
E('td', {}, [
E('button', {
'class': 'cbi-button cbi-button-action',
'style': 'padding:4px 8px;font-size:0.8rem;',
'click': ui.createHandlerFn(this, 'handleConvert', result.image)
}, 'Convert')
])
]);
}, this));
var table = E('table', { 'class': 'table cbi-section-table', 'style': 'margin-top:1rem;' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, 'Image'),
E('th', { 'class': 'th' }, 'Description'),
E('th', { 'class': 'th' }, 'Action')
])
].concat(rows));
container.innerHTML = '';
container.appendChild(table);
},
pollApps: function() {
var self = this;
return callApps().then(function(data) {
self.apps = (data && data.apps) || [];
var container = document.getElementById('apps-table');
if (container) {
container.innerHTML = '';
container.appendChild(self.renderAppsTable());
}
});
},
handleSearch: function() {
var self = this;
var query = document.getElementById('search-query').value.trim();
if (!query) {
ui.addNotification(null, E('p', 'Please enter a search query'));
return;
}
var container = document.getElementById('search-results');
container.innerHTML = '<p class="spinning">Searching Docker Hub...</p>';
return callSearch(query).then(function(res) {
if (res.code !== 0) {
container.innerHTML = '<p style="color:#f44336;">Search failed</p>';
return;
}
self.searchResults = res.results || [];
self.renderSearchResults();
if (!self.searchResults.length) {
container.innerHTML = '<p>No results found for "' + query + '"</p>';
}
}).catch(function(err) {
container.innerHTML = '<p style="color:#f44336;">Error: ' + err.message + '</p>';
});
},
handleConvert: function(image) {
var self = this;
// Extract default name from image
var defaultName = image.split('/').pop().split(':')[0].replace(/[^a-zA-Z0-9_-]/g, '_');
ui.showModal('Convert Docker Image', [
E('div', { 'class': 'cbi-section' }, [
E('p', {}, ['Converting: ', E('strong', {}, image)]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'App Name'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', { 'type': 'text', 'id': 'convert-name', 'class': 'cbi-input-text', 'value': defaultName })
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Tag'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', { 'type': 'text', 'id': 'convert-tag', 'class': 'cbi-input-text', 'value': 'latest', 'placeholder': 'latest' })
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Memory Limit'),
E('div', { 'class': 'cbi-value-field' }, [
E('select', { 'id': 'convert-memory', 'class': 'cbi-input-select' }, [
E('option', { 'value': '256M' }, '256 MB'),
E('option', { 'value': '512M', 'selected': true }, '512 MB'),
E('option', { 'value': '1G' }, '1 GB'),
E('option', { 'value': '2G' }, '2 GB')
])
])
])
]),
E('div', { 'class': 'right' }, [
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'),
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': function() {
var name = document.getElementById('convert-name').value.trim();
var tag = document.getElementById('convert-tag').value.trim() || 'latest';
var memory = document.getElementById('convert-memory').value;
if (!name) {
ui.addNotification(null, E('p', 'Please enter an app name'));
return;
}
ui.hideModal();
ui.showModal('Converting...', [
E('p', { 'class': 'spinning' }, 'Converting ' + image + ':' + tag + '...'),
E('p', { 'style': 'color:#888;font-size:0.85rem;' }, 'This may take several minutes. Please wait...')
]);
callConvert(image, name, tag, memory).then(function(res) {
ui.hideModal();
if (res.code === 0) {
ui.addNotification(null, E('p', 'Conversion complete!'));
self.pollApps();
} else {
ui.addNotification(null, E('p', 'Error: ' + (res.output || 'Conversion failed')));
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', 'Error: ' + err.message));
});
}
}, 'Convert')
])
]);
},
handlePackage: function(name) {
var self = this;
ui.showModal('Generating Package...', [
E('p', { 'class': 'spinning' }, 'Generating SecuBox package for ' + name + '...')
]);
return callPackage(name).then(function(res) {
ui.hideModal();
if (res.code === 0) {
ui.addNotification(null, E('p', 'Package generated!'));
self.pollApps();
} else {
ui.addNotification(null, E('p', 'Error: ' + (res.output || 'Package generation failed')));
}
});
},
handlePublish: function(name) {
var self = this;
ui.showModal('Publishing...', [
E('p', { 'class': 'spinning' }, 'Publishing ' + name + ' to catalog...')
]);
return callPublish(name).then(function(res) {
ui.hideModal();
if (res.code === 0) {
ui.addNotification(null, E('p', 'Published to catalog!'));
} else {
ui.addNotification(null, E('p', 'Error: ' + (res.output || 'Publish failed')));
}
});
},
handleDelete: function(name) {
var self = this;
if (!confirm('Delete converted app "' + name + '"? This cannot be undone.')) {
return;
}
return callDelete(name).then(function(res) {
if (res.code === 0) {
ui.addNotification(null, E('p', 'Deleted ' + name));
self.pollApps();
} else {
ui.addNotification(null, E('p', 'Error deleting'));
}
});
}
});