feat(appstore): add normalized catalog manifests
This commit is contained in:
parent
7179d71a6c
commit
e4c9ec0237
@ -8,20 +8,42 @@ This guide outlines the “SecuBox Apps” registry format and the `secubox-app`
|
||||
|
||||
---
|
||||
|
||||
## Manifest Layout (`plugins/<app>/manifest.json`)
|
||||
## Manifest Layout (`plugins/catalog/<app>.json`)
|
||||
|
||||
Each plugin folder contains a `manifest.json`. Example (Zigbee2MQTT):
|
||||
Each app now ships a normalized JSON manifest under `plugins/catalog/<app-id>.json` (legacy `plugins/<app>/manifest.json` entries remain for backward compatibility). Example (Zigbee2MQTT):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "zigbee2mqtt",
|
||||
"name": "Zigbee2MQTT",
|
||||
"type": "docker",
|
||||
"description": "Dockerized Zigbee gateway",
|
||||
"category": "home-automation",
|
||||
"runtime": "docker",
|
||||
"maturity": "stable",
|
||||
"description": "Dockerized Zigbee gateway bridging Zigbee coordinators with MQTT brokers.",
|
||||
"source": {
|
||||
"homepage": "https://www.zigbee2mqtt.io/",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/secubox-app-zigbee2mqtt"
|
||||
},
|
||||
"packages": ["secubox-app-zigbee2mqtt", "luci-app-zigbee2mqtt"],
|
||||
"capabilities": ["zigbee-gateway", "mqtt", "docker-runner"],
|
||||
"requirements": {
|
||||
"arch": ["arm64"],
|
||||
"min_ram_mb": 256,
|
||||
"min_storage_mb": 512
|
||||
},
|
||||
"hardware": { "usb": true, "serial": true },
|
||||
"network": {
|
||||
"inbound_ports": [8080],
|
||||
"protocols": ["http", "mqtt"],
|
||||
"outbound_only": false
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": true,
|
||||
"needs_serial": true,
|
||||
"needs_net_admin": false
|
||||
},
|
||||
"ports": [{ "name": "frontend", "protocol": "http", "port": 8080 }],
|
||||
"volumes": ["/srv/zigbee2mqtt"],
|
||||
"network": { "default_mode": "lan", "dmz_supported": true },
|
||||
"wizard": {
|
||||
"uci": { "config": "zigbee2mqtt", "section": "main" },
|
||||
"fields": [
|
||||
@ -33,7 +55,7 @@ Each plugin folder contains a `manifest.json`. Example (Zigbee2MQTT):
|
||||
{ "id": "frontend_port", "label": "Frontend Port", "type": "number", "uci_option": "frontend_port" }
|
||||
]
|
||||
},
|
||||
"profiles": ["home", "lab"],
|
||||
"profiles": { "recommended": ["home", "lab", "iot"] },
|
||||
"actions": {
|
||||
"install": "zigbee2mqttctl install",
|
||||
"check": "zigbee2mqttctl check",
|
||||
@ -49,17 +71,22 @@ Each plugin folder contains a `manifest.json`. Example (Zigbee2MQTT):
|
||||
|-----|---------|
|
||||
| `id` | Unique identifier used by the CLI (`secubox-app install <id>`). |
|
||||
| `name` / `description` | Display metadata. |
|
||||
| `type` | `docker`, `lxc`, or `native`. |
|
||||
| `category` | One of: home-automation, networking, security, media, monitoring, storage, development, system, iot, radio, misc. |
|
||||
| `runtime` | `docker`, `lxc`, `native`, or `hybrid`. |
|
||||
| `packages` | List of OpenWrt packages to install/remove. |
|
||||
| `requirements.arch` | Architectures supported by the app/runtime. |
|
||||
| `requirements.min_ram_mb` / `requirements.min_storage_mb` | Conservative resource guidance for UI filters. |
|
||||
| `actions.install/update/check/status` | Optional shell commands executed after opkg operations. |
|
||||
|
||||
**Optional keys**
|
||||
|
||||
- `ports`: Document exposed services for the App Store UI.
|
||||
- `volumes`: Persistent directories (e.g., `/srv/zigbee2mqtt`).
|
||||
- `network`: Defaults + whether DMZ mode is supported.
|
||||
- `network`: Connection hints (protocols, inbound ports, outbound-only flag).
|
||||
- `hardware` / `privileges`: USB/serial/net_admin hints for wizards.
|
||||
- `wizard`: UCI target plus the declarative field list consumed by the LuCI wizard.
|
||||
- `profiles`: Tags to pre-load when applying OS-like profiles.
|
||||
- `profiles`: Tags to pre-load when applying OS-like profiles (e.g., `profiles.recommended` array).
|
||||
- `capabilities`, `maturity`, `source`, `update.strategy`: Additional metadata for filter chips and CLI instructions.
|
||||
|
||||
---
|
||||
|
||||
@ -83,6 +110,9 @@ secubox-app status zigbee2mqtt
|
||||
# Update or remove
|
||||
secubox-app update zigbee2mqtt
|
||||
secubox-app remove zigbee2mqtt
|
||||
|
||||
# Validate manifests (schema + requirements)
|
||||
secubox-app validate
|
||||
```
|
||||
|
||||
Environment variables:
|
||||
@ -104,6 +134,9 @@ The CLI relies on `opkg` and `jsonfilter`, so run it on the router (or within th
|
||||
|
||||
All three packages declare their dependencies (Docker, vhost manager, etc.) so `secubox-app install <id>` only has to orchestrate actions, not guess at required feeds.
|
||||
|
||||
- **Manifest QA**: run `secubox-app validate` before commits/releases to catch missing IDs, runtimes, or packages.
|
||||
- **Specs refresh**: `python scripts/refresh-manifest-specs.py` re-applies shared architecture/min-spec heuristics so individual JSON files stay in sync.
|
||||
|
||||
---
|
||||
|
||||
## Future Integration
|
||||
|
||||
@ -0,0 +1,456 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require dom';
|
||||
'require secubox/api as API';
|
||||
'require secubox/nav as SecuNav';
|
||||
|
||||
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/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
|
||||
});
|
||||
@ -61,6 +61,46 @@ DEFAULT_STORAGE_PATH="/srv/secubox"
|
||||
SECOBOX_APP="/usr/sbin/secubox-app"
|
||||
OPKG_UPDATED=0
|
||||
|
||||
manifest_files() {
|
||||
if [ -d "$PLUGIN_DIR/catalog" ]; then
|
||||
for file in "$PLUGIN_DIR"/catalog/*.json; do
|
||||
[ -f "$file" ] || continue
|
||||
echo "$file"
|
||||
done
|
||||
fi
|
||||
for file in "$PLUGIN_DIR"/*/manifest.json; do
|
||||
[ -f "$file" ] || continue
|
||||
echo "$file"
|
||||
done
|
||||
}
|
||||
|
||||
manifest_file_for_id() {
|
||||
local id="$1"
|
||||
local catalog="$PLUGIN_DIR/catalog/$id.json"
|
||||
local legacy="$PLUGIN_DIR/$id/manifest.json"
|
||||
if [ -f "$catalog" ]; then
|
||||
printf '%s' "$catalog"
|
||||
return 0
|
||||
fi
|
||||
if [ -f "$legacy" ]; then
|
||||
printf '%s' "$legacy"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
validate_manifest_json() {
|
||||
local manifest="$1"
|
||||
local id name runtime packages category
|
||||
id=$(jsonfilter -s "$manifest" -e '@.id' 2>/dev/null)
|
||||
name=$(jsonfilter -s "$manifest" -e '@.name' 2>/dev/null)
|
||||
category=$(jsonfilter -s "$manifest" -e '@.category' 2>/dev/null)
|
||||
runtime=$(jsonfilter -s "$manifest" -e '@.runtime' 2>/dev/null)
|
||||
[ -n "$runtime" ] || runtime=$(jsonfilter -s "$manifest" -e '@.type' 2>/dev/null)
|
||||
packages=$(jsonfilter -s "$manifest" -e '@.packages[*]' 2>/dev/null)
|
||||
[ -n "$id" ] && [ -n "$name" ] && [ -n "$runtime" ] && [ -n "$category" ] && [ -n "$packages" ]
|
||||
}
|
||||
|
||||
# Module registry - auto-detected from /usr/libexec/rpcd/
|
||||
detect_modules() {
|
||||
local modules=""
|
||||
@ -1259,30 +1299,44 @@ list_apps() {
|
||||
ensure_directory "$PLUGIN_DIR"
|
||||
json_init
|
||||
json_add_array "apps"
|
||||
local manifest_file
|
||||
for manifest_file in "$PLUGIN_DIR"/*/manifest.json; do
|
||||
local manifest_file manifest id name runtime type version description state wizard_field category maturity seen_ids=""
|
||||
while IFS= read -r manifest_file; do
|
||||
[ -f "$manifest_file" ] || continue
|
||||
local manifest
|
||||
manifest=$(cat "$manifest_file")
|
||||
local id name type version description state wizard_field
|
||||
id=$(jsonfilter -s "$manifest" -e '@.id' 2>/dev/null)
|
||||
[ -n "$id" ] || continue
|
||||
case " $seen_ids " in
|
||||
*" $id "*) continue ;;
|
||||
esac
|
||||
if ! validate_manifest_json "$manifest"; then
|
||||
continue
|
||||
fi
|
||||
seen_ids="$seen_ids $id"
|
||||
name=$(jsonfilter -s "$manifest" -e '@.name' 2>/dev/null)
|
||||
type=$(jsonfilter -s "$manifest" -e '@.type' 2>/dev/null)
|
||||
runtime=$(jsonfilter -s "$manifest" -e '@.runtime' 2>/dev/null)
|
||||
[ -n "$type" ] || type="$runtime"
|
||||
version=$(jsonfilter -s "$manifest" -e '@.version' 2>/dev/null)
|
||||
description=$(jsonfilter -s "$manifest" -e '@.description' 2>/dev/null)
|
||||
[ -n "$id" ] || continue
|
||||
category=$(jsonfilter -s "$manifest" -e '@.category' 2>/dev/null)
|
||||
maturity=$(jsonfilter -s "$manifest" -e '@.maturity' 2>/dev/null)
|
||||
state=$(packages_state "$manifest")
|
||||
wizard_field=$(jsonfilter -s "$manifest" -e '@.wizard.fields[0].id' 2>/dev/null)
|
||||
json_add_object
|
||||
json_add_string "id" "$id"
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "runtime" "$runtime"
|
||||
json_add_string "type" "$type"
|
||||
json_add_string "version" "$version"
|
||||
json_add_string "description" "$description"
|
||||
json_add_string "category" "$category"
|
||||
json_add_string "maturity" "$maturity"
|
||||
json_add_string "state" "$state"
|
||||
json_add_boolean "has_wizard" "$([ -n "$wizard_field" ] && echo 1 || echo 0)"
|
||||
json_close_object
|
||||
done
|
||||
done <<EOF
|
||||
$(manifest_files | sort)
|
||||
EOF
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
@ -1298,8 +1352,8 @@ get_app_manifest() {
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
file="$PLUGIN_DIR/$app_id/manifest.json"
|
||||
if [ ! -f "$file" ]; then
|
||||
file=$(manifest_file_for_id "$app_id")
|
||||
if [ -z "$file" ] || [ ! -f "$file" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Manifest not found"
|
||||
@ -1324,8 +1378,8 @@ apply_app_wizard() {
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
file="$PLUGIN_DIR/$app_id/manifest.json"
|
||||
if [ ! -f "$file" ]; then
|
||||
file=$(manifest_file_for_id "$app_id")
|
||||
if [ -z "$file" ] || [ ! -f "$file" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Manifest not found"
|
||||
|
||||
@ -32,6 +32,7 @@ Commands:
|
||||
remove <app-id> Remove packages listed in manifest
|
||||
status <app-id> Show install state and run status action if defined
|
||||
update <app-id> Run plugin update action or opkg upgrade
|
||||
validate Verify manifest schema/metadata
|
||||
|
||||
Environment:
|
||||
SECUBOX_PLUGINS_DIR Override manifest directory (default: /usr/share/secubox/plugins)
|
||||
@ -110,11 +111,33 @@ pkg_upgrade() {
|
||||
esac
|
||||
}
|
||||
|
||||
manifest_files() {
|
||||
if [ -d "$PLUGINS_DIR/catalog" ]; then
|
||||
for file in "$PLUGINS_DIR"/catalog/*.json; do
|
||||
[ -f "$file" ] || continue
|
||||
echo "$file"
|
||||
done
|
||||
fi
|
||||
for file in "$PLUGINS_DIR"/*/manifest.json; do
|
||||
[ -f "$file" ] || continue
|
||||
echo "$file"
|
||||
done
|
||||
}
|
||||
|
||||
manifest_path() {
|
||||
local id="$1"
|
||||
local file="$PLUGINS_DIR/$id/manifest.json"
|
||||
[ -f "$file" ] || { err "Manifest not found for '$id' ($file)"; exit 1; }
|
||||
printf '%s' "$file"
|
||||
local catalog="$PLUGINS_DIR/catalog/$id.json"
|
||||
local legacy="$PLUGINS_DIR/$id/manifest.json"
|
||||
if [ -f "$catalog" ]; then
|
||||
printf '%s' "$catalog"
|
||||
return 0
|
||||
fi
|
||||
if [ -f "$legacy" ]; then
|
||||
printf '%s' "$legacy"
|
||||
return 0
|
||||
fi
|
||||
err "Manifest not found for '$id'"
|
||||
exit 1
|
||||
}
|
||||
|
||||
manifest_field() {
|
||||
@ -132,6 +155,38 @@ manifest_action() {
|
||||
jsonfilter -f "$file" -e "@.actions.$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
manifest_runtime() {
|
||||
local file="$1"
|
||||
local value
|
||||
value=$(manifest_field "$file" '@.type')
|
||||
[ -n "$value" ] || value=$(manifest_field "$file" '@.runtime')
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
validate_manifest_file() {
|
||||
require_jsonfilter
|
||||
local file="$1"
|
||||
local id name runtime packages category
|
||||
id=$(manifest_field "$file" '@.id')
|
||||
name=$(manifest_field "$file" '@.name')
|
||||
category=$(manifest_field "$file" '@.category')
|
||||
runtime=$(manifest_runtime "$file")
|
||||
packages=$(manifest_packages "$file")
|
||||
if [ -z "$id" ] || [ -z "$name" ] || [ -z "$runtime" ]; then
|
||||
warn "Skipping manifest $file (missing id/name/runtime)"
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$packages" ]; then
|
||||
warn "Manifest $id has no packages defined"
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$category" ]; then
|
||||
warn "Manifest $id missing category"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
plugin_state() {
|
||||
local file="$1"
|
||||
local pkgs pkg missing=0 installed=0 total=0
|
||||
@ -158,16 +213,28 @@ plugin_state() {
|
||||
list_plugins() {
|
||||
require_jsonfilter
|
||||
ensure_pkg_mgr
|
||||
printf '%-16s %-22s %-10s %-10s\n' "ID" "Name" "Type" "State"
|
||||
printf '%-16s %-22s %-10s %-10s\n' "--" "----" "----" "-----"
|
||||
find "$PLUGINS_DIR" -mindepth 2 -maxdepth 2 -name manifest.json | sort | while read -r file; do
|
||||
local id name type state
|
||||
local seen_ids=""
|
||||
printf '%-18s %-24s %-10s %-10s %-10s\n' "ID" "Name" "Runtime" "Category" "State"
|
||||
printf '%-18s %-24s %-10s %-10s %-10s\n' "--" "----" "-------" "--------" "-----"
|
||||
while IFS= read -r file; do
|
||||
[ -f "$file" ] || continue
|
||||
if ! validate_manifest_file "$file"; then
|
||||
continue
|
||||
fi
|
||||
local id name runtime state category
|
||||
id=$(manifest_field "$file" '@.id')
|
||||
name=$(manifest_field "$file" '@.name')
|
||||
type=$(manifest_field "$file" '@.type')
|
||||
runtime=$(manifest_runtime "$file")
|
||||
category=$(manifest_field "$file" '@.category')
|
||||
case " $seen_ids " in
|
||||
*" $id "*) continue ;;
|
||||
esac
|
||||
seen_ids="$seen_ids $id"
|
||||
state=$(plugin_state "$file")
|
||||
printf '%-16s %-22s %-10s %-10s\n' "$id" "${name:-Unknown}" "${type:-?}" "$state"
|
||||
done
|
||||
printf '%-18s %-24s %-10s %-10s %-10s\n' "$id" "${name:-Unknown}" "${runtime:-unknown}" "${category:-?}" "$state"
|
||||
done <<EOF
|
||||
$(manifest_files | sort)
|
||||
EOF
|
||||
}
|
||||
|
||||
show_manifest() {
|
||||
@ -241,6 +308,27 @@ update_plugin() {
|
||||
fi
|
||||
}
|
||||
|
||||
validate_manifests_cmd() {
|
||||
require_jsonfilter
|
||||
local failed=0 passed=0 file
|
||||
while IFS= read -r file; do
|
||||
[ -f "$file" ] || continue
|
||||
if validate_manifest_file "$file"; then
|
||||
passed=$((passed + 1))
|
||||
else
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done <<EOF
|
||||
$(manifest_files)
|
||||
EOF
|
||||
info "Validated $passed manifest(s)"
|
||||
if [ "$failed" -gt 0 ]; then
|
||||
err "$failed manifest(s) failed validation"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
list) shift; list_plugins ;;
|
||||
show) shift; [ $# -ge 1 ] || { err "show requires an app id"; exit 1; }; show_manifest "$1" ;;
|
||||
@ -248,6 +336,7 @@ case "${1:-}" in
|
||||
remove) shift; [ $# -ge 1 ] || { err "remove requires an app id"; exit 1; }; remove_plugin "$1" ;;
|
||||
status) shift; [ $# -ge 1 ] || { err "status requires an app id"; exit 1; }; plugin_status_cmd "$1" ;;
|
||||
update) shift; [ $# -ge 1 ] || { err "update requires an app id"; exit 1; }; update_plugin "$1" ;;
|
||||
validate) shift; if ! validate_manifests_cmd; then exit 1; fi ;;
|
||||
help|--help|-h|'') usage ;;
|
||||
*) err "Unknown command: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
{
|
||||
"id": "auth-guardian",
|
||||
"name": "Auth Guardian",
|
||||
"category": "security",
|
||||
"runtime": "native",
|
||||
"maturity": "stable",
|
||||
"description": "Captive portal and authentication layer with OAuth, vouchers, and bypass policies.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-auth-guardian",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-auth-guardian"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-auth-guardian"
|
||||
],
|
||||
"capabilities": [
|
||||
"captive-portal",
|
||||
"oauth",
|
||||
"voucher-policy"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 256,
|
||||
"min_storage_mb": 60
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [
|
||||
80,
|
||||
443
|
||||
],
|
||||
"protocols": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"outbound_only": false
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": true
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"portal_branding",
|
||||
"oauth_setup",
|
||||
"voucher_policy"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"guest_wifi",
|
||||
"hospitality",
|
||||
"campus"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": "bandwidth-manager",
|
||||
"name": "Bandwidth Manager",
|
||||
"category": "networking",
|
||||
"runtime": "native",
|
||||
"maturity": "stable",
|
||||
"description": "Traffic shaping and quota manager with HTB/CAKE classes and schedules.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-bandwidth-manager",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-bandwidth-manager"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-bandwidth-manager"
|
||||
],
|
||||
"capabilities": [
|
||||
"qos",
|
||||
"quota",
|
||||
"scheduling"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 128,
|
||||
"min_storage_mb": 40
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [],
|
||||
"protocols": [
|
||||
"http"
|
||||
],
|
||||
"outbound_only": true
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": true
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"class_defaults",
|
||||
"quota_setup",
|
||||
"schedule_window"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"home",
|
||||
"smb",
|
||||
"isp-lite"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
{
|
||||
"id": "cdn-cache",
|
||||
"name": "CDN Cache",
|
||||
"category": "networking",
|
||||
"runtime": "native",
|
||||
"maturity": "stable",
|
||||
"description": "Local CDN proxy cache with policies, stats, purging, and bandwidth savings analytics.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-cdn-cache",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-cdn-cache"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-cdn-cache"
|
||||
],
|
||||
"capabilities": [
|
||||
"caching",
|
||||
"policy-control",
|
||||
"bandwidth-savings"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 256,
|
||||
"min_storage_mb": 1024
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [
|
||||
80,
|
||||
443
|
||||
],
|
||||
"protocols": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"outbound_only": false
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": true
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"cache_path",
|
||||
"policy_matrix",
|
||||
"purge_schedule"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"campus",
|
||||
"isp-lite",
|
||||
"lab"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
{
|
||||
"id": "client-guardian",
|
||||
"name": "Client Guardian",
|
||||
"category": "security",
|
||||
"runtime": "native",
|
||||
"maturity": "stable",
|
||||
"description": "Network access control with zones, parental policies, captive portal, and alerting.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-client-guardian",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-client-guardian"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-client-guardian"
|
||||
],
|
||||
"capabilities": [
|
||||
"nac",
|
||||
"zone-control",
|
||||
"parental-control"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 256,
|
||||
"min_storage_mb": 80
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [
|
||||
80,
|
||||
443
|
||||
],
|
||||
"protocols": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"outbound_only": false
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": true
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"zone_definition",
|
||||
"policy_rules",
|
||||
"notification_channels"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"home",
|
||||
"smb",
|
||||
"education"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": "crowdsec-dashboard",
|
||||
"name": "CrowdSec Dashboard",
|
||||
"category": "security",
|
||||
"runtime": "native",
|
||||
"maturity": "stable",
|
||||
"description": "CrowdSec intrusion prevention console featuring bans, metrics, and geo insights.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-crowdsec-dashboard",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-crowdsec-dashboard"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-crowdsec-dashboard"
|
||||
],
|
||||
"capabilities": [
|
||||
"threat-intel",
|
||||
"ban-management",
|
||||
"metrics"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 512,
|
||||
"min_storage_mb": 300
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [],
|
||||
"protocols": [
|
||||
"http"
|
||||
],
|
||||
"outbound_only": true
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": true
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"engine_detection",
|
||||
"bouncer_binding",
|
||||
"alert_policy"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"gateway",
|
||||
"smb",
|
||||
"lab"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
{
|
||||
"id": "domoticz",
|
||||
"name": "Domoticz",
|
||||
"category": "home-automation",
|
||||
"runtime": "docker",
|
||||
"maturity": "stable",
|
||||
"description": "Domoticz home-automation stack bundled with CLI installer, data mounts, and LuCI tie-ins.",
|
||||
"source": {
|
||||
"homepage": "https://www.domoticz.com/",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/secubox-app-domoticz"
|
||||
},
|
||||
"packages": [
|
||||
"secubox-app-domoticz",
|
||||
"luci-app-vhost-manager"
|
||||
],
|
||||
"capabilities": [
|
||||
"home-automation",
|
||||
"docker-runner",
|
||||
"vhost-publish"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"x86_64"
|
||||
],
|
||||
"min_ram_mb": 512,
|
||||
"min_storage_mb": 1024
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [
|
||||
8080
|
||||
],
|
||||
"protocols": [
|
||||
"http"
|
||||
],
|
||||
"outbound_only": false
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": false
|
||||
},
|
||||
"update": {
|
||||
"strategy": "docker_pull"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"data_path",
|
||||
"device_mounts",
|
||||
"vhost_binding"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"home",
|
||||
"lab",
|
||||
"iot"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": "ksm-manager",
|
||||
"name": "KSM Manager",
|
||||
"category": "security",
|
||||
"runtime": "native",
|
||||
"maturity": "stable",
|
||||
"description": "Cryptographic key/secret manager with CSR tooling, SSH/HSM workflows, and audits.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-ksm-manager",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-ksm-manager"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-ksm-manager"
|
||||
],
|
||||
"capabilities": [
|
||||
"key-management",
|
||||
"certificates",
|
||||
"hsm"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 256,
|
||||
"min_storage_mb": 100
|
||||
},
|
||||
"hardware": {
|
||||
"usb": true,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [],
|
||||
"protocols": [
|
||||
"http"
|
||||
],
|
||||
"outbound_only": true
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": true,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": false
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"hsm_registration",
|
||||
"key_policy",
|
||||
"audit_targets"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"smb",
|
||||
"enterprise",
|
||||
"lab"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
{
|
||||
"id": "lyrion",
|
||||
"name": "Lyrion Media Server",
|
||||
"category": "media",
|
||||
"runtime": "docker",
|
||||
"maturity": "stable",
|
||||
"description": "Self-hosted Lyrion media server container with configurable storage and HTTPS publishing.",
|
||||
"source": {
|
||||
"homepage": "https://lyrion.org/",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/secubox-app-lyrion"
|
||||
},
|
||||
"packages": [
|
||||
"secubox-app-lyrion",
|
||||
"luci-app-vhost-manager"
|
||||
],
|
||||
"capabilities": [
|
||||
"media-server",
|
||||
"docker-runner",
|
||||
"vhost-publish"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"x86_64"
|
||||
],
|
||||
"min_ram_mb": 1024,
|
||||
"min_storage_mb": 2048
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [
|
||||
8096,
|
||||
8920
|
||||
],
|
||||
"protocols": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"outbound_only": false
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": false
|
||||
},
|
||||
"update": {
|
||||
"strategy": "docker_pull"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"library_path",
|
||||
"media_mounts",
|
||||
"https_publish"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"home",
|
||||
"media-gateway",
|
||||
"lab"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": "media-flow",
|
||||
"name": "Media Flow",
|
||||
"category": "media",
|
||||
"runtime": "native",
|
||||
"maturity": "stable",
|
||||
"description": "Streaming/VoIP detection dashboard with QoE counters, histories, and alerts.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-media-flow",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-media-flow"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-media-flow"
|
||||
],
|
||||
"capabilities": [
|
||||
"stream-detection",
|
||||
"qoemetrics",
|
||||
"alerts"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 256,
|
||||
"min_storage_mb": 60
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [],
|
||||
"protocols": [
|
||||
"http"
|
||||
],
|
||||
"outbound_only": true
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": true
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"service_filters",
|
||||
"alert_rules",
|
||||
"integration_checkpoint"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"home",
|
||||
"campus",
|
||||
"isp-lite"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
{
|
||||
"id": "mqtt-bridge",
|
||||
"name": "MQTT Bridge",
|
||||
"category": "iot",
|
||||
"runtime": "native",
|
||||
"maturity": "experimental",
|
||||
"description": "USB-aware MQTT bridge with adapter presets, local broker controls, and automation rules.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-mqtt-bridge",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-mqtt-bridge"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-mqtt-bridge"
|
||||
],
|
||||
"capabilities": [
|
||||
"mqtt-broker",
|
||||
"usb-bridging",
|
||||
"automation-rules"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 128,
|
||||
"min_storage_mb": 60
|
||||
},
|
||||
"hardware": {
|
||||
"usb": true,
|
||||
"serial": true
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [
|
||||
1883
|
||||
],
|
||||
"protocols": [
|
||||
"mqtt",
|
||||
"http"
|
||||
],
|
||||
"outbound_only": false
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": true,
|
||||
"needs_serial": true,
|
||||
"needs_net_admin": false
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"broker_setup",
|
||||
"adapter_discovery",
|
||||
"rule_binding"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"home",
|
||||
"iot",
|
||||
"lab"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
{
|
||||
"id": "netdata-dashboard",
|
||||
"name": "Netdata Dashboard",
|
||||
"category": "monitoring",
|
||||
"runtime": "native",
|
||||
"maturity": "stable",
|
||||
"description": "LuCI front-end for Netdata-like system metrics with realtime charts and process views.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-netdata-dashboard",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-netdata-dashboard"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-netdata-dashboard"
|
||||
],
|
||||
"capabilities": [
|
||||
"system-monitoring",
|
||||
"process-analytics"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 512,
|
||||
"min_storage_mb": 200
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [],
|
||||
"protocols": [
|
||||
"http"
|
||||
],
|
||||
"outbound_only": true
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": false
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"data_refresh",
|
||||
"sensor_selection",
|
||||
"thresholds"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"lab",
|
||||
"home",
|
||||
"smb"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": "netifyd-dashboard",
|
||||
"name": "Netifyd Dashboard",
|
||||
"category": "monitoring",
|
||||
"runtime": "native",
|
||||
"maturity": "stable",
|
||||
"description": "Deep packet inspection dashboard covering applications, devices, flows, and risks.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-netifyd-dashboard",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-netifyd-dashboard"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-netifyd-dashboard"
|
||||
],
|
||||
"capabilities": [
|
||||
"dpi",
|
||||
"device-insights",
|
||||
"traffic-analysis"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 512,
|
||||
"min_storage_mb": 200
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [],
|
||||
"protocols": [
|
||||
"http"
|
||||
],
|
||||
"outbound_only": true
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": true
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"dpi_enable",
|
||||
"device_tags",
|
||||
"risk_rules"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"gateway",
|
||||
"lab",
|
||||
"isp-lite"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": "network-modes",
|
||||
"name": "Network Modes",
|
||||
"category": "networking",
|
||||
"runtime": "native",
|
||||
"maturity": "stable",
|
||||
"description": "Mode-switching wizard covering router, relay, AP, and sniffer profiles with automatic backups.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-network-modes",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-network-modes"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-network-modes"
|
||||
],
|
||||
"capabilities": [
|
||||
"mode-switching",
|
||||
"wifi-automation",
|
||||
"firewall-profiles"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 128,
|
||||
"min_storage_mb": 30
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [],
|
||||
"protocols": [
|
||||
"http"
|
||||
],
|
||||
"outbound_only": true
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": true
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"mode_selection",
|
||||
"interface_mapping",
|
||||
"rollback_plan"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"gateway",
|
||||
"lab",
|
||||
"iot"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": "secubox-hub",
|
||||
"name": "SecuBox Hub",
|
||||
"category": "system",
|
||||
"runtime": "native",
|
||||
"maturity": "mature",
|
||||
"description": "Central SecuBox dashboard that discovers modules, unifies monitoring, and manages package-driven installs.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-secubox",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-secubox"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-secubox"
|
||||
],
|
||||
"capabilities": [
|
||||
"module-orchestration",
|
||||
"monitoring",
|
||||
"alerts"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 128,
|
||||
"min_storage_mb": 20
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [],
|
||||
"protocols": [
|
||||
"http"
|
||||
],
|
||||
"outbound_only": true
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": true
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"module_selection",
|
||||
"alert_routing",
|
||||
"profile_binding"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"home",
|
||||
"lab",
|
||||
"gateway"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": "system-hub",
|
||||
"name": "System Hub",
|
||||
"category": "system",
|
||||
"runtime": "native",
|
||||
"maturity": "mature",
|
||||
"description": "System health, service control, diagnostics, and remote assistance center for SecuBox routers.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-system-hub",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-system-hub"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-system-hub"
|
||||
],
|
||||
"capabilities": [
|
||||
"health-monitoring",
|
||||
"service-control",
|
||||
"remote-assist"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 256,
|
||||
"min_storage_mb": 40
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [],
|
||||
"protocols": [
|
||||
"http"
|
||||
],
|
||||
"outbound_only": true
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": true
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"health_scoring",
|
||||
"service_checks",
|
||||
"backup_targets"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"home",
|
||||
"smb",
|
||||
"lab"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": "traffic-shaper",
|
||||
"name": "Traffic Shaper",
|
||||
"category": "networking",
|
||||
"runtime": "native",
|
||||
"maturity": "stable",
|
||||
"description": "Preset-friendly CAKE/HTB shaping with latency-focused rules and stats.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-traffic-shaper",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-traffic-shaper"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-traffic-shaper"
|
||||
],
|
||||
"capabilities": [
|
||||
"qos",
|
||||
"presets",
|
||||
"latency-optimization"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 128,
|
||||
"min_storage_mb": 40
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [],
|
||||
"protocols": [
|
||||
"http"
|
||||
],
|
||||
"outbound_only": true
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": true
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"preset_selection",
|
||||
"class_mapping",
|
||||
"verification"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"gaming",
|
||||
"wfh",
|
||||
"lab"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
{
|
||||
"id": "vhost-manager",
|
||||
"name": "Vhost Manager",
|
||||
"category": "networking",
|
||||
"runtime": "native",
|
||||
"maturity": "stable",
|
||||
"description": "Nginx reverse proxy/vhost orchestrator with certificates, redirects, and SaaS publishing.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-vhost-manager",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-vhost-manager"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-vhost-manager"
|
||||
],
|
||||
"capabilities": [
|
||||
"reverse-proxy",
|
||||
"cert-automation",
|
||||
"saas-publishing"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 256,
|
||||
"min_storage_mb": 200
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [
|
||||
80,
|
||||
443
|
||||
],
|
||||
"protocols": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"outbound_only": false
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": true
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"hostname_pool",
|
||||
"certificate_mode",
|
||||
"publish_targets"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"smb",
|
||||
"lab",
|
||||
"homelab"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
{
|
||||
"id": "wireguard-dashboard",
|
||||
"name": "WireGuard Dashboard",
|
||||
"category": "security",
|
||||
"runtime": "native",
|
||||
"maturity": "stable",
|
||||
"description": "WireGuard tunnel orchestration with peer tracking, QR onboarding, and traffic views.",
|
||||
"source": {
|
||||
"homepage": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-wireguard-dashboard",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/luci-app-wireguard-dashboard"
|
||||
},
|
||||
"packages": [
|
||||
"luci-app-wireguard-dashboard"
|
||||
],
|
||||
"capabilities": [
|
||||
"vpn-management",
|
||||
"peer-onboarding"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64",
|
||||
"armv7",
|
||||
"x86_64",
|
||||
"mipsel"
|
||||
],
|
||||
"min_ram_mb": 128,
|
||||
"min_storage_mb": 40
|
||||
},
|
||||
"hardware": {
|
||||
"usb": false,
|
||||
"serial": false
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [
|
||||
51820
|
||||
],
|
||||
"protocols": [
|
||||
"udp"
|
||||
],
|
||||
"outbound_only": false
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": false,
|
||||
"needs_serial": false,
|
||||
"needs_net_admin": true
|
||||
},
|
||||
"update": {
|
||||
"strategy": "opkg"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"tunnel_basics",
|
||||
"peer_enrollment",
|
||||
"qrcode_export"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"home",
|
||||
"smb",
|
||||
"remote-workers"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
{
|
||||
"id": "zigbee2mqtt",
|
||||
"name": "Zigbee2MQTT",
|
||||
"category": "home-automation",
|
||||
"runtime": "docker",
|
||||
"maturity": "stable",
|
||||
"description": "Dockerized Zigbee coordinator with MQTT bridge, serial setup, and LuCI management UI.",
|
||||
"source": {
|
||||
"homepage": "https://www.zigbee2mqtt.io",
|
||||
"github": "https://github.com/gkerma/secubox-openwrt/tree/main/secubox-app-zigbee2mqtt"
|
||||
},
|
||||
"packages": [
|
||||
"secubox-app-zigbee2mqtt",
|
||||
"luci-app-zigbee2mqtt"
|
||||
],
|
||||
"capabilities": [
|
||||
"zigbee-gateway",
|
||||
"mqtt",
|
||||
"docker-runner"
|
||||
],
|
||||
"requirements": {
|
||||
"arch": [
|
||||
"arm64"
|
||||
],
|
||||
"min_ram_mb": 256,
|
||||
"min_storage_mb": 512
|
||||
},
|
||||
"hardware": {
|
||||
"usb": true,
|
||||
"serial": true
|
||||
},
|
||||
"network": {
|
||||
"inbound_ports": [
|
||||
8080
|
||||
],
|
||||
"protocols": [
|
||||
"http",
|
||||
"mqtt"
|
||||
],
|
||||
"outbound_only": false
|
||||
},
|
||||
"privileges": {
|
||||
"needs_usb": true,
|
||||
"needs_serial": true,
|
||||
"needs_net_admin": false
|
||||
},
|
||||
"update": {
|
||||
"strategy": "docker_pull"
|
||||
},
|
||||
"wizard": {
|
||||
"steps": [
|
||||
"serial_port",
|
||||
"mqtt_broker",
|
||||
"frontend_port",
|
||||
"docker_check"
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"recommended": [
|
||||
"home",
|
||||
"lab",
|
||||
"iot"
|
||||
]
|
||||
}
|
||||
}
|
||||
130
scripts/refresh-manifest-specs.py
Executable file
130
scripts/refresh-manifest-specs.py
Executable file
@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Refresh requirements (architectures + min specs) for SecuBox App Store manifests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
MANIFEST_DIR = REPO_ROOT / "plugins" / "catalog"
|
||||
|
||||
RUNTIME_DEFAULTS: Dict[str, Dict[str, int]] = {
|
||||
"native": {"min_ram_mb": 128, "min_storage_mb": 30},
|
||||
"docker": {"min_ram_mb": 512, "min_storage_mb": 512},
|
||||
"lxc": {"min_ram_mb": 256, "min_storage_mb": 256},
|
||||
"hybrid": {"min_ram_mb": 256, "min_storage_mb": 256},
|
||||
}
|
||||
|
||||
CATEGORY_DEFAULTS: Dict[str, Dict[str, int]] = {
|
||||
"system": {"min_ram_mb": 256, "min_storage_mb": 40},
|
||||
"security": {"min_ram_mb": 256, "min_storage_mb": 60},
|
||||
"monitoring": {"min_ram_mb": 512, "min_storage_mb": 200},
|
||||
"media": {"min_ram_mb": 256, "min_storage_mb": 60},
|
||||
"networking": {"min_ram_mb": 128, "min_storage_mb": 40},
|
||||
"iot": {"min_ram_mb": 128, "min_storage_mb": 50},
|
||||
"storage": {"min_ram_mb": 256, "min_storage_mb": 200},
|
||||
}
|
||||
|
||||
SPEC_OVERRIDES: Dict[str, Dict[str, int]] = {
|
||||
"secubox-hub": {"min_ram_mb": 128, "min_storage_mb": 20},
|
||||
"network-modes": {"min_storage_mb": 30},
|
||||
"wireguard-dashboard": {"min_ram_mb": 128, "min_storage_mb": 40},
|
||||
"mqtt-bridge": {"min_storage_mb": 60},
|
||||
"client-guardian": {"min_storage_mb": 80},
|
||||
"ksm-manager": {"min_storage_mb": 100},
|
||||
"crowdsec-dashboard": {"min_ram_mb": 512, "min_storage_mb": 300},
|
||||
"vhost-manager": {"min_ram_mb": 256, "min_storage_mb": 200},
|
||||
"cdn-cache": {"min_ram_mb": 256, "min_storage_mb": 1024},
|
||||
"domoticz": {"min_ram_mb": 512, "min_storage_mb": 1024},
|
||||
"zigbee2mqtt": {"min_ram_mb": 256, "min_storage_mb": 512},
|
||||
"lyrion": {"min_ram_mb": 1024, "min_storage_mb": 2048},
|
||||
}
|
||||
|
||||
ARCH_DEFAULTS: Dict[str, List[str]] = {
|
||||
"native": ["arm64", "armv7", "x86_64", "mipsel"],
|
||||
"docker": ["arm64", "x86_64"],
|
||||
"lxc": ["arm64", "x86_64"],
|
||||
"hybrid": ["arm64", "x86_64"],
|
||||
}
|
||||
|
||||
ARCH_OVERRIDES: Dict[str, List[str]] = {
|
||||
"zigbee2mqtt": ["arm64"],
|
||||
}
|
||||
|
||||
|
||||
def compute_specs(manifest_id: str, manifest: Dict) -> Dict[str, int]:
|
||||
runtime = (manifest.get("runtime") or manifest.get("type") or "native").lower()
|
||||
specs = dict(RUNTIME_DEFAULTS.get(runtime, RUNTIME_DEFAULTS["native"]))
|
||||
category = (manifest.get("category") or "").lower()
|
||||
if category in CATEGORY_DEFAULTS:
|
||||
for key, value in CATEGORY_DEFAULTS[category].items():
|
||||
specs[key] = max(specs.get(key, 0), value)
|
||||
if manifest_id in SPEC_OVERRIDES:
|
||||
specs.update(SPEC_OVERRIDES[manifest_id])
|
||||
return specs
|
||||
|
||||
|
||||
def compute_arch(manifest_id: str, runtime: str) -> List[str]:
|
||||
if manifest_id in ARCH_OVERRIDES:
|
||||
return ARCH_OVERRIDES[manifest_id]
|
||||
return ARCH_DEFAULTS.get(runtime, ARCH_DEFAULTS["native"])
|
||||
|
||||
|
||||
def apply_updates(path: Path) -> bool:
|
||||
data = json.loads(path.read_text())
|
||||
manifest_id = data.get("id") or path.stem
|
||||
runtime = (data.get("runtime") or data.get("type") or "native").lower()
|
||||
specs = compute_specs(manifest_id, data)
|
||||
requirements = data.setdefault("requirements", {})
|
||||
changed = False
|
||||
|
||||
for key, value in specs.items():
|
||||
if requirements.get(key) != value:
|
||||
requirements[key] = value
|
||||
changed = True
|
||||
|
||||
arch = compute_arch(manifest_id, runtime)
|
||||
if requirements.get("arch") != arch:
|
||||
requirements["arch"] = arch
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
return changed
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Refresh manifest requirement metadata.")
|
||||
parser.add_argument("--check", action="store_true", help="Only report drifts.")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not MANIFEST_DIR.is_dir():
|
||||
raise SystemExit(f"Manifest directory not found: {MANIFEST_DIR}")
|
||||
|
||||
dirty = []
|
||||
for manifest_path in sorted(MANIFEST_DIR.glob("*.json")):
|
||||
if args.check:
|
||||
original = manifest_path.read_text()
|
||||
changed = apply_updates(manifest_path)
|
||||
if changed:
|
||||
dirty.append(manifest_path)
|
||||
manifest_path.write_text(original)
|
||||
else:
|
||||
if apply_updates(manifest_path):
|
||||
print(f"[updated] {manifest_path.relative_to(REPO_ROOT)}")
|
||||
|
||||
if args.check:
|
||||
if dirty:
|
||||
for path in dirty:
|
||||
print(f"[drift] {path.relative_to(REPO_ROOT)}")
|
||||
raise SystemExit(1)
|
||||
print("All manifest specs look good.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user