feat(appstore): add normalized catalog manifests

This commit is contained in:
CyberMind-FR 2025-12-30 08:48:41 +01:00
parent 7179d71a6c
commit e4c9ec0237
24 changed files with 2013 additions and 29 deletions

View File

@ -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

View File

@ -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
});

View File

@ -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"

View File

@ -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

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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"
]
}
}

View File

@ -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
View 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()