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
|
```json
|
||||||
{
|
{
|
||||||
"id": "zigbee2mqtt",
|
"id": "zigbee2mqtt",
|
||||||
"name": "Zigbee2MQTT",
|
"name": "Zigbee2MQTT",
|
||||||
"type": "docker",
|
"category": "home-automation",
|
||||||
"description": "Dockerized Zigbee gateway",
|
"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"],
|
"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 }],
|
"ports": [{ "name": "frontend", "protocol": "http", "port": 8080 }],
|
||||||
"volumes": ["/srv/zigbee2mqtt"],
|
"volumes": ["/srv/zigbee2mqtt"],
|
||||||
"network": { "default_mode": "lan", "dmz_supported": true },
|
|
||||||
"wizard": {
|
"wizard": {
|
||||||
"uci": { "config": "zigbee2mqtt", "section": "main" },
|
"uci": { "config": "zigbee2mqtt", "section": "main" },
|
||||||
"fields": [
|
"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" }
|
{ "id": "frontend_port", "label": "Frontend Port", "type": "number", "uci_option": "frontend_port" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"profiles": ["home", "lab"],
|
"profiles": { "recommended": ["home", "lab", "iot"] },
|
||||||
"actions": {
|
"actions": {
|
||||||
"install": "zigbee2mqttctl install",
|
"install": "zigbee2mqttctl install",
|
||||||
"check": "zigbee2mqttctl check",
|
"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>`). |
|
| `id` | Unique identifier used by the CLI (`secubox-app install <id>`). |
|
||||||
| `name` / `description` | Display metadata. |
|
| `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. |
|
| `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. |
|
| `actions.install/update/check/status` | Optional shell commands executed after opkg operations. |
|
||||||
|
|
||||||
**Optional keys**
|
**Optional keys**
|
||||||
|
|
||||||
- `ports`: Document exposed services for the App Store UI.
|
- `ports`: Document exposed services for the App Store UI.
|
||||||
- `volumes`: Persistent directories (e.g., `/srv/zigbee2mqtt`).
|
- `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.
|
- `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
|
# Update or remove
|
||||||
secubox-app update zigbee2mqtt
|
secubox-app update zigbee2mqtt
|
||||||
secubox-app remove zigbee2mqtt
|
secubox-app remove zigbee2mqtt
|
||||||
|
|
||||||
|
# Validate manifests (schema + requirements)
|
||||||
|
secubox-app validate
|
||||||
```
|
```
|
||||||
|
|
||||||
Environment variables:
|
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.
|
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
|
## 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"
|
SECOBOX_APP="/usr/sbin/secubox-app"
|
||||||
OPKG_UPDATED=0
|
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/
|
# Module registry - auto-detected from /usr/libexec/rpcd/
|
||||||
detect_modules() {
|
detect_modules() {
|
||||||
local modules=""
|
local modules=""
|
||||||
@ -1259,30 +1299,44 @@ list_apps() {
|
|||||||
ensure_directory "$PLUGIN_DIR"
|
ensure_directory "$PLUGIN_DIR"
|
||||||
json_init
|
json_init
|
||||||
json_add_array "apps"
|
json_add_array "apps"
|
||||||
local manifest_file
|
local manifest_file manifest id name runtime type version description state wizard_field category maturity seen_ids=""
|
||||||
for manifest_file in "$PLUGIN_DIR"/*/manifest.json; do
|
while IFS= read -r manifest_file; do
|
||||||
[ -f "$manifest_file" ] || continue
|
[ -f "$manifest_file" ] || continue
|
||||||
local manifest
|
|
||||||
manifest=$(cat "$manifest_file")
|
manifest=$(cat "$manifest_file")
|
||||||
local id name type version description state wizard_field
|
|
||||||
id=$(jsonfilter -s "$manifest" -e '@.id' 2>/dev/null)
|
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)
|
name=$(jsonfilter -s "$manifest" -e '@.name' 2>/dev/null)
|
||||||
type=$(jsonfilter -s "$manifest" -e '@.type' 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)
|
version=$(jsonfilter -s "$manifest" -e '@.version' 2>/dev/null)
|
||||||
description=$(jsonfilter -s "$manifest" -e '@.description' 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")
|
state=$(packages_state "$manifest")
|
||||||
wizard_field=$(jsonfilter -s "$manifest" -e '@.wizard.fields[0].id' 2>/dev/null)
|
wizard_field=$(jsonfilter -s "$manifest" -e '@.wizard.fields[0].id' 2>/dev/null)
|
||||||
json_add_object
|
json_add_object
|
||||||
json_add_string "id" "$id"
|
json_add_string "id" "$id"
|
||||||
json_add_string "name" "$name"
|
json_add_string "name" "$name"
|
||||||
|
json_add_string "runtime" "$runtime"
|
||||||
json_add_string "type" "$type"
|
json_add_string "type" "$type"
|
||||||
json_add_string "version" "$version"
|
json_add_string "version" "$version"
|
||||||
json_add_string "description" "$description"
|
json_add_string "description" "$description"
|
||||||
|
json_add_string "category" "$category"
|
||||||
|
json_add_string "maturity" "$maturity"
|
||||||
json_add_string "state" "$state"
|
json_add_string "state" "$state"
|
||||||
json_add_boolean "has_wizard" "$([ -n "$wizard_field" ] && echo 1 || echo 0)"
|
json_add_boolean "has_wizard" "$([ -n "$wizard_field" ] && echo 1 || echo 0)"
|
||||||
json_close_object
|
json_close_object
|
||||||
done
|
done <<EOF
|
||||||
|
$(manifest_files | sort)
|
||||||
|
EOF
|
||||||
json_close_array
|
json_close_array
|
||||||
json_dump
|
json_dump
|
||||||
}
|
}
|
||||||
@ -1298,8 +1352,8 @@ get_app_manifest() {
|
|||||||
json_dump
|
json_dump
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
file="$PLUGIN_DIR/$app_id/manifest.json"
|
file=$(manifest_file_for_id "$app_id")
|
||||||
if [ ! -f "$file" ]; then
|
if [ -z "$file" ] || [ ! -f "$file" ]; then
|
||||||
json_init
|
json_init
|
||||||
json_add_boolean "success" 0
|
json_add_boolean "success" 0
|
||||||
json_add_string "error" "Manifest not found"
|
json_add_string "error" "Manifest not found"
|
||||||
@ -1324,8 +1378,8 @@ apply_app_wizard() {
|
|||||||
json_dump
|
json_dump
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
file="$PLUGIN_DIR/$app_id/manifest.json"
|
file=$(manifest_file_for_id "$app_id")
|
||||||
if [ ! -f "$file" ]; then
|
if [ -z "$file" ] || [ ! -f "$file" ]; then
|
||||||
json_init
|
json_init
|
||||||
json_add_boolean "success" 0
|
json_add_boolean "success" 0
|
||||||
json_add_string "error" "Manifest not found"
|
json_add_string "error" "Manifest not found"
|
||||||
|
|||||||
@ -32,6 +32,7 @@ Commands:
|
|||||||
remove <app-id> Remove packages listed in manifest
|
remove <app-id> Remove packages listed in manifest
|
||||||
status <app-id> Show install state and run status action if defined
|
status <app-id> Show install state and run status action if defined
|
||||||
update <app-id> Run plugin update action or opkg upgrade
|
update <app-id> Run plugin update action or opkg upgrade
|
||||||
|
validate Verify manifest schema/metadata
|
||||||
|
|
||||||
Environment:
|
Environment:
|
||||||
SECUBOX_PLUGINS_DIR Override manifest directory (default: /usr/share/secubox/plugins)
|
SECUBOX_PLUGINS_DIR Override manifest directory (default: /usr/share/secubox/plugins)
|
||||||
@ -110,11 +111,33 @@ pkg_upgrade() {
|
|||||||
esac
|
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() {
|
manifest_path() {
|
||||||
local id="$1"
|
local id="$1"
|
||||||
local file="$PLUGINS_DIR/$id/manifest.json"
|
local catalog="$PLUGINS_DIR/catalog/$id.json"
|
||||||
[ -f "$file" ] || { err "Manifest not found for '$id' ($file)"; exit 1; }
|
local legacy="$PLUGINS_DIR/$id/manifest.json"
|
||||||
printf '%s' "$file"
|
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() {
|
manifest_field() {
|
||||||
@ -132,6 +155,38 @@ manifest_action() {
|
|||||||
jsonfilter -f "$file" -e "@.actions.$1" 2>/dev/null || true
|
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() {
|
plugin_state() {
|
||||||
local file="$1"
|
local file="$1"
|
||||||
local pkgs pkg missing=0 installed=0 total=0
|
local pkgs pkg missing=0 installed=0 total=0
|
||||||
@ -158,16 +213,28 @@ plugin_state() {
|
|||||||
list_plugins() {
|
list_plugins() {
|
||||||
require_jsonfilter
|
require_jsonfilter
|
||||||
ensure_pkg_mgr
|
ensure_pkg_mgr
|
||||||
printf '%-16s %-22s %-10s %-10s\n' "ID" "Name" "Type" "State"
|
local seen_ids=""
|
||||||
printf '%-16s %-22s %-10s %-10s\n' "--" "----" "----" "-----"
|
printf '%-18s %-24s %-10s %-10s %-10s\n' "ID" "Name" "Runtime" "Category" "State"
|
||||||
find "$PLUGINS_DIR" -mindepth 2 -maxdepth 2 -name manifest.json | sort | while read -r file; do
|
printf '%-18s %-24s %-10s %-10s %-10s\n' "--" "----" "-------" "--------" "-----"
|
||||||
local id name type state
|
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')
|
id=$(manifest_field "$file" '@.id')
|
||||||
name=$(manifest_field "$file" '@.name')
|
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")
|
state=$(plugin_state "$file")
|
||||||
printf '%-16s %-22s %-10s %-10s\n' "$id" "${name:-Unknown}" "${type:-?}" "$state"
|
printf '%-18s %-24s %-10s %-10s %-10s\n' "$id" "${name:-Unknown}" "${runtime:-unknown}" "${category:-?}" "$state"
|
||||||
done
|
done <<EOF
|
||||||
|
$(manifest_files | sort)
|
||||||
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
show_manifest() {
|
show_manifest() {
|
||||||
@ -241,6 +308,27 @@ update_plugin() {
|
|||||||
fi
|
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
|
case "${1:-}" in
|
||||||
list) shift; list_plugins ;;
|
list) shift; list_plugins ;;
|
||||||
show) shift; [ $# -ge 1 ] || { err "show requires an app id"; exit 1; }; show_manifest "$1" ;;
|
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" ;;
|
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" ;;
|
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" ;;
|
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 ;;
|
help|--help|-h|'') usage ;;
|
||||||
*) err "Unknown command: $1"; usage; exit 1 ;;
|
*) err "Unknown command: $1"; usage; exit 1 ;;
|
||||||
esac
|
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