secubox-openwrt/luci-app-secubox/htdocs/luci-static/resources/view/secubox/appstore.js

471 lines
17 KiB
JavaScript

'use strict';
'require view';
'require ui';
'require dom';
'require secubox/api as API';
'require secubox-theme/theme as Theme';
'require secubox/nav as SecuNav';
// Load theme resources
document.head.appendChild(E('link', {
'rel': 'stylesheet',
'type': 'text/css',
'href': L.resource('secubox-theme/secubox-theme.css')
}));
var secuLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: secuLang });
var RUNTIME_FILTERS = [
{ id: 'all', label: _('All runtimes') },
{ id: 'docker', label: _('Docker') },
{ id: 'lxc', label: _('LXC') },
{ id: 'native', label: _('Native') }
];
var STATE_FILTERS = [
{ id: 'all', label: _('All states') },
{ id: 'installed', label: _('Installed') },
{ id: 'available', label: _('Available') }
];
var RUNTIME_ICONS = {
docker: '🐳',
lxc: '📦',
native: '⚙️',
hybrid: '🧬'
};
return view.extend({
load: function() {
return Promise.all([
API.listApps()
]);
},
render: function(payload) {
this.apps = (payload[0] && payload[0].apps) || [];
this.searchQuery = '';
this.runtimeFilter = 'all';
this.stateFilter = 'all';
this.filterButtons = { runtime: {}, state: {} };
this.root = E('div', { 'class': 'secubox-appstore-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/core/variables.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/common.css') }),
SecuNav.renderTabs('appstore'),
this.renderHeader(),
this.renderStats(),
this.renderFilterBar(),
this.renderAppGrid()
]);
this.updateStats();
this.updateAppGrid();
return this.root;
},
renderHeader: function() {
return E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '🛒'),
_('SecuBox App Store')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Browse manifest-driven apps, launch guided wizards, and copy CLI commands from the SecuBox App Store.'))
])
]);
},
renderStats: function() {
this.statsNodes = {
total: E('div', { 'class': 'sb-stat-value' }, '0'),
installed: E('div', { 'class': 'sb-stat-value' }, '0'),
docker: E('div', { 'class': 'sb-stat-value' }, '0'),
lxc: E('div', { 'class': 'sb-stat-value' }, '0')
};
return E('div', { 'class': 'sb-stats-row' }, [
this.renderStatCard('📦', _('Total apps'), this.statsNodes.total, _('Manifest entries detected')),
this.renderStatCard('✅', _('Installed'), this.statsNodes.installed, _('Apps currently deployed')),
this.renderStatCard('🐳', _('Docker'), this.statsNodes.docker, _('Containerized services')),
this.renderStatCard('📦', _('LXC'), this.statsNodes.lxc, _('Lightweight containers'))
]);
},
renderStatCard: function(icon, title, valueEl, subtitle) {
return E('div', { 'class': 'sb-stat-card' }, [
E('div', { 'class': 'sb-stat-icon' }, icon),
E('div', { 'class': 'sb-stat-label' }, title),
valueEl,
E('div', { 'class': 'sb-stat-sub' }, subtitle)
]);
},
renderFilterBar: function() {
var self = this;
this.searchInput = E('input', {
'class': 'sb-wizard-input',
'type': 'search',
'placeholder': _('Search apps…')
});
this.searchInput.addEventListener('input', function(ev) {
self.searchQuery = (ev.target.value || '').trim().toLowerCase();
self.updateAppGrid();
});
return E('div', { 'class': 'secubox-appstore-filters' }, [
E('div', { 'class': 'sb-filter-group' }, [
E('div', { 'class': 'sb-filter-label' }, _('Type')),
E('div', { 'class': 'sb-filter-pills' }, RUNTIME_FILTERS.map(function(filter) {
var pill = E('button', {
'class': 'sb-filter-pill' + (filter.id === self.runtimeFilter ? ' active' : ''),
'click': self.handleFilterClick.bind(self, 'runtime', filter.id)
}, filter.label);
self.filterButtons.runtime[filter.id] = pill;
return pill;
}))
]),
E('div', { 'class': 'sb-filter-group' }, [
E('div', { 'class': 'sb-filter-label' }, _('State')),
E('div', { 'class': 'sb-filter-pills' }, STATE_FILTERS.map(function(filter) {
var pill = E('button', {
'class': 'sb-filter-pill' + (filter.id === self.stateFilter ? ' active' : ''),
'click': self.handleFilterClick.bind(self, 'state', filter.id)
}, filter.label);
self.filterButtons.state[filter.id] = pill;
return pill;
}))
]),
E('div', { 'class': 'sb-filter-search' }, [
E('span', { 'class': 'sb-filter-search-icon' }, '🔍'),
this.searchInput
])
]);
},
handleFilterClick: function(group, value, ev) {
ev.preventDefault();
if (group === 'runtime')
this.runtimeFilter = value;
else
this.stateFilter = value;
this.updateFilterButtons(group);
this.updateAppGrid();
},
updateFilterButtons: function(group) {
var buttons = this.filterButtons[group] || {};
Object.keys(buttons).forEach(function(key) {
var el = buttons[key];
if (!el)
return;
if ((group === 'runtime' && key === this.runtimeFilter) ||
(group === 'state' && key === this.stateFilter))
el.classList.add('active');
else
el.classList.remove('active');
}, this);
},
renderAppGrid: function() {
this.appGrid = E('div', { 'class': 'sb-app-grid secubox-appstore-grid' });
return this.appGrid;
},
updateStats: function() {
var total = this.apps.length;
var installed = this.apps.filter(function(app) { return app.state === 'installed'; }).length;
var docker = this.apps.filter(function(app) { return (app.runtime || app.type || '') === 'docker'; }).length;
var lxc = this.apps.filter(function(app) { return (app.runtime || app.type || '') === 'lxc'; }).length;
if (this.statsNodes) {
this.statsNodes.total.textContent = total.toString();
this.statsNodes.installed.textContent = installed.toString();
this.statsNodes.docker.textContent = docker.toString();
this.statsNodes.lxc.textContent = lxc.toString();
}
},
getFilteredApps: function() {
var q = this.searchQuery;
var runtimeFilter = this.runtimeFilter;
var state = this.stateFilter;
return this.apps.filter(function(app) {
var runtime = (app.runtime || app.type || '').toLowerCase();
var desc = ((app.description || '') + ' ' + (app.name || '') + ' ' + (app.id || '')).toLowerCase();
var matchesRuntime = runtimeFilter === 'all' || runtime === runtimeFilter;
var matchesState = state === 'all' ||
(state === 'installed' && app.state === 'installed') ||
(state === 'available' && app.state !== 'installed');
var matchesSearch = !q || desc.indexOf(q) !== -1;
return matchesRuntime && matchesState && matchesSearch;
});
},
updateAppGrid: function() {
if (!this.appGrid)
return;
var apps = this.getFilteredApps();
if (!apps.length) {
dom.content(this.appGrid, [
E('div', { 'class': 'secubox-empty-state' }, [
E('div', { 'class': 'secubox-empty-icon' }, '🕵️'),
E('div', { 'class': 'secubox-empty-title' }, _('No apps found')),
E('div', { 'class': 'secubox-empty-text' }, _('Adjust filters or add manifests under /usr/share/secubox/plugins/.'))
])
]);
return;
}
dom.content(this.appGrid, apps.map(this.renderAppCard, this));
},
renderAppCard: function(app) {
var runtime = (app.runtime || app.type || 'other').toLowerCase();
var icon = RUNTIME_ICONS[runtime] || '🧩';
var stateClass = app.state === 'installed' ? ' ok' : '';
var badges = [
E('span', { 'class': 'sb-app-tag' }, icon + ' ' + (app.runtime || app.type || _('Unknown')))
];
if (app.category)
badges.push(E('span', { 'class': 'sb-app-tag' }, _('Category: %s').format(app.category)));
if (app.maturity)
badges.push(E('span', { 'class': 'sb-app-tag' }, _('Maturity: %s').format(app.maturity)));
if (app.version)
badges.push(E('span', { 'class': 'sb-app-tag sb-app-version' }, 'v' + app.version));
return E('div', { 'class': 'sb-app-card' }, [
E('div', { 'class': 'sb-app-card-info' }, [
E('div', { 'class': 'sb-app-name' }, [
app.name || app.id,
E('span', { 'class': 'sb-app-state' + stateClass }, app.state || _('unknown'))
]),
E('div', { 'class': 'sb-app-desc' }, app.description || _('No description provided')),
E('div', { 'class': 'sb-app-tags' }, badges)
]),
E('div', { 'class': 'sb-app-actions' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'click': this.showAppDetails.bind(this, app)
}, _('Details')),
(app.has_wizard ? E('button', {
'class': 'cbi-button',
'click': this.openAppWizard.bind(this, app)
}, _('Configure')) : null)
])
]);
},
showAppDetails: function(app, ev) {
var self = this;
ui.showModal(_('Loading %s…').format(app.name || app.id), [E('div', { 'class': 'spinning' })]);
API.getAppManifest(app.id).then(function(manifest) {
ui.hideModal();
manifest = manifest || {};
var wizard = manifest.wizard || {};
var packages = manifest.packages || [];
var ports = manifest.ports || [];
var volumes = manifest.volumes || [];
var requirements = manifest.requirements || {};
var hardware = manifest.hardware || {};
var network = manifest.network || {};
var privileges = manifest.privileges || {};
var profiles = (manifest.profiles && manifest.profiles.recommended) || manifest.profiles || [];
if (!Array.isArray(profiles))
profiles = [];
var makeRow = function(label, value) {
return E('div', { 'class': 'sb-app-detail-row' }, [
E('strong', {}, label),
E('span', {}, value)
]);
};
var detailRows = [
makeRow(_('Runtime:'), manifest.runtime || app.runtime || manifest.type || app.type || _('Unknown')),
makeRow(_('Category:'), manifest.category || _('Unknown')),
makeRow(_('Maturity:'), manifest.maturity || _('Unspecified')),
makeRow(_('Version:'), manifest.version || app.version || '—'),
makeRow(_('State:'), app.state || _('unknown'))
];
var requirementRows = [];
if (requirements.arch && requirements.arch.length)
requirementRows.push(makeRow(_('Architectures:'), requirements.arch.join(', ')));
if (requirements.min_ram_mb)
requirementRows.push(makeRow(_('Min RAM:'), _('%s MB').format(requirements.min_ram_mb)));
if (requirements.min_storage_mb)
requirementRows.push(makeRow(_('Min storage:'), _('%s MB').format(requirements.min_storage_mb)));
var hardwareRows = [];
if (typeof hardware.usb === 'boolean')
hardwareRows.push(makeRow(_('USB access:'), hardware.usb ? _('Required') : _('Not needed')));
if (typeof hardware.serial === 'boolean')
hardwareRows.push(makeRow(_('Serial access:'), hardware.serial ? _('Required') : _('Not needed')));
var privilegeRows = [];
if (typeof privileges.needs_usb === 'boolean')
privilegeRows.push(makeRow(_('USB privileges:'), privileges.needs_usb ? _('Required') : _('Not needed')));
if (typeof privileges.needs_serial === 'boolean')
privilegeRows.push(makeRow(_('Serial privileges:'), privileges.needs_serial ? _('Required') : _('Not needed')));
if (typeof privileges.needs_net_admin === 'boolean')
privilegeRows.push(makeRow(_('Net admin:'), privileges.needs_net_admin ? _('Required') : _('Not needed')));
var networkRows = [];
if ((network.inbound_ports || []).length)
networkRows.push(makeRow(_('Inbound ports:'), network.inbound_ports.join(', ')));
if ((network.protocols || []).length)
networkRows.push(makeRow(_('Protocols:'), network.protocols.join(', ')));
if (typeof network.outbound_only === 'boolean')
networkRows.push(makeRow(_('Network mode:'), network.outbound_only ? _('Outbound only') : _('Inbound/Outbound')));
var cliCommands = E('pre', { 'class': 'sb-app-cli' }, [
'secubox-app install ' + app.id + '\n',
(wizard.fields && wizard.fields.length ? 'secubox-app wizard ' + app.id + '\n' : ''),
'secubox-app status ' + app.id + '\n',
'secubox-app remove ' + app.id
]);
var sections = [
E('p', { 'class': 'sb-app-desc' }, manifest.description || app.description || ''),
E('div', { 'class': 'sb-app-detail-grid' }, detailRows),
requirementRows.length ? E('div', { 'class': 'sb-app-detail-list' }, [
E('strong', {}, _('Requirements')),
E('div', { 'class': 'sb-app-detail-grid' }, requirementRows)
]) : '',
hardwareRows.length ? E('div', { 'class': 'sb-app-detail-list' }, [
E('strong', {}, _('Hardware')),
E('div', { 'class': 'sb-app-detail-grid' }, hardwareRows)
]) : '',
privilegeRows.length ? E('div', { 'class': 'sb-app-detail-list' }, [
E('strong', {}, _('Privileges')),
E('div', { 'class': 'sb-app-detail-grid' }, privilegeRows)
]) : '',
networkRows.length ? E('div', { 'class': 'sb-app-detail-list' }, [
E('strong', {}, _('Network')),
E('div', { 'class': 'sb-app-detail-grid' }, networkRows)
]) : '',
packages.length ? E('div', { 'class': 'sb-app-detail-list' }, [
E('strong', {}, _('Packages')),
E('ul', {}, packages.map(function(pkg) { return E('li', {}, pkg); }))
]) : '',
ports.length ? E('div', { 'class': 'sb-app-detail-list' }, [
E('strong', {}, _('Ports')),
E('ul', {}, ports.map(function(port) {
var label = [port.name || 'port', port.protocol || '', port.port || ''].filter(Boolean).join(' · ');
return E('li', {}, label);
}))
]) : '',
volumes.length ? E('div', { 'class': 'sb-app-detail-list' }, [
E('strong', {}, _('Volumes')),
E('ul', {}, volumes.map(function(volume) { return E('li', {}, volume); }))
]) : '',
profiles.length ? E('div', { 'class': 'sb-app-detail-list' }, [
E('strong', {}, _('Profiles')),
E('ul', {}, profiles.map(function(profile) { return E('li', {}, profile); }))
]) : '',
E('div', { 'class': 'sb-app-detail-list' }, [
E('strong', {}, _('CLI commands')),
cliCommands
])
];
var actions = [
E('button', {
'class': 'cbi-button cbi-button-cancel',
'click': ui.hideModal
}, _('Close')),
(app.has_wizard ? E('button', {
'class': 'cbi-button cbi-button-action',
'click': function() {
ui.hideModal();
self.openAppWizard(app);
}
}, _('Launch wizard')) : null)
].filter(Boolean);
ui.showModal(app.name || app.id, [
E('div', { 'class': 'sb-app-detail-body' }, sections),
E('div', { 'class': 'right', 'style': 'margin-top:16px;' }, actions)
]);
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err && err.message ? err.message : _('Unable to load manifest')), 'error');
});
},
openAppWizard: function(app) {
var self = this;
ui.showModal(_('Loading %s wizard…').format(app.name || app.id), [E('div', { 'class': 'spinning' })]);
API.getAppManifest(app.id).then(function(manifest) {
ui.hideModal();
manifest = manifest || {};
var wizard = manifest.wizard || {};
var fields = wizard.fields || [];
if (!fields.length) {
ui.addNotification(null, E('p', {}, _('No wizard metadata for this app.')), 'warn');
return;
}
var form = E('div', { 'class': 'sb-app-wizard-form' }, fields.map(function(field) {
return E('div', { 'class': 'sb-form-group' }, [
E('label', {}, field.label || field.id),
E('input', {
'class': 'sb-wizard-input',
'name': field.id,
'type': field.type || 'text',
'placeholder': field.placeholder || ''
})
]);
}));
ui.showModal(_('Configure %s').format(app.name || app.id), [
form,
E('div', { 'class': 'right', 'style': 'margin-top:16px;' }, [
E('button', {
'class': 'cbi-button cbi-button-cancel',
'click': ui.hideModal
}, _('Cancel')),
E('button', {
'class': 'cbi-button cbi-button-action',
'click': function() {
self.submitAppWizard(app.id, form, fields);
}
}, _('Apply'))
])
]);
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err && err.message ? err.message : _('Failed to load wizard')), 'error');
});
},
submitAppWizard: function(appId, form, fields) {
var values = {};
fields.forEach(function(field) {
var input = form.querySelector('[name="' + field.id + '"]');
if (input && input.value !== '')
values[field.id] = input.value;
});
ui.showModal(_('Saving…'), [E('div', { 'class': 'spinning' })]);
API.applyAppWizard(appId, values).then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, _('Wizard applied.')), 'info');
} else {
ui.addNotification(null, E('p', {}, _('Failed to apply wizard.')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err && err.message ? err.message : _('Failed to apply wizard.')), 'error');
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});