From 6c3c96a70b8bbbe46a4e2c67331eb1b34112909d Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 29 Dec 2025 14:59:43 +0100 Subject: [PATCH] feat: adapter preferences editing --- DOCS/MQTT_BRIDGE.md | 1 + luci-app-mqtt-bridge/README.md | 2 +- .../resources/mqtt-bridge/common.css | 37 ++++++++ .../resources/view/mqtt-bridge/settings.js | 90 +++++++++++++++++-- .../root/usr/libexec/rpcd/luci.mqtt-bridge | 38 ++++++++ 5 files changed, 159 insertions(+), 9 deletions(-) diff --git a/DOCS/MQTT_BRIDGE.md b/DOCS/MQTT_BRIDGE.md index 48a36bf5..f7d398fe 100644 --- a/DOCS/MQTT_BRIDGE.md +++ b/DOCS/MQTT_BRIDGE.md @@ -75,6 +75,7 @@ The package now installs a lightweight watcher (`/usr/sbin/mqtt-bridge-monitor`) - Managed with the standard init script: `service mqtt-bridge start|stop|status`. - Writes state transitions to the system log (`logread -e mqtt-bridge-monitor`). - Updates each adapter section with `detected`, `port`, `bus`, `device`, `health`, and `last_seen`, which the LuCI Devices tab now surfaces. +- The MQTT Settings view exposes the same adapter entries so you can enable/disable presets, rename labels, or override `/dev/tty*` assignments without leaving the UI. Use `uci show mqtt-bridge.adapter` to inspect the persisted metadata, or `ubus call luci.mqtt-bridge status` to see the JSON payload consumed by the UI. diff --git a/luci-app-mqtt-bridge/README.md b/luci-app-mqtt-bridge/README.md index 4d941c67..a4c2069d 100644 --- a/luci-app-mqtt-bridge/README.md +++ b/luci-app-mqtt-bridge/README.md @@ -9,7 +9,7 @@ USB-aware MQTT orchestrator for SecuBox routers. The application discovers USB s - `overview.js` – broker status, metrics, quick actions. - `devices.js` – USB/tasmota sensor list with pairing wizard. -- `settings.js` – broker credentials, topic templates, retention options. +- `settings.js` – broker credentials, topic templates, retention options, adapter preferences (enable/label/tty overrides). ## RPC Methods diff --git a/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/common.css b/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/common.css index 07ee65cc..470ba88b 100644 --- a/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/common.css +++ b/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/common.css @@ -692,6 +692,43 @@ pre { overflow-x: auto; } +.mb-adapter-grid { + display: flex; + flex-direction: column; + gap: 18px; +} + +.mb-adapter-row { + border: 1px solid var(--mb-border); + border-radius: 16px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + background: var(--mb-card); +} + +.mb-adapter-header { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: center; +} + +.mb-switch { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: var(--mb-text); +} + +.mb-switch input { + accent-color: var(--mb-accent); + width: 16px; + height: 16px; +} + @media (max-width: 768px) { .mqtt-bridge-dashboard { padding: 16px; diff --git a/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/settings.js b/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/settings.js index 4691a558..e414f23f 100644 --- a/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/settings.js +++ b/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/settings.js @@ -13,17 +13,20 @@ Theme.init({ language: lang }); return view.extend({ load: function() { - return API.getStatus().then(function(status) { - return status.settings || {}; - }); + return API.getStatus(); }, - render: function(settings) { + render: function(payload) { + var settings = (payload && payload.settings) || {}; + var adapters = (payload && payload.adapters) || []; + this.currentAdapters = adapters; + var container = E('div', { 'class': 'mqtt-bridge-dashboard' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), E('link', { 'rel': 'stylesheet', 'href': L.resource('mqtt-bridge/common.css') }), Nav.renderTabs('settings'), - this.renderSettingsCard(settings || {}) + this.renderSettingsCard(settings || {}), + this.renderAdapterCard(adapters || []) ]); return container; }, @@ -42,11 +45,51 @@ return view.extend({ this.input('retention', _('Retention (days)'), settings.retention || 7, 'number') ]), E('div', { 'style': 'margin-top:16px;' }, [ - E('button', { 'class': 'mb-btn mb-btn-primary', 'click': ui.createHandlerFn(this, 'saveSettings') }, ['💾 ', _('Save settings')]) + E('button', { 'class': 'mb-btn mb-btn-primary', 'click': ui.createHandlerFn(this, 'savePreferences') }, ['💾 ', _('Save preferences')]) ]) ]); }, + renderAdapterCard: function(adapters) { + var items = adapters && adapters.length ? adapters.map(this.renderAdapterRow.bind(this)) : + [E('p', { 'style': 'color:var(--mb-muted);' }, _('No adapters configured yet. UCI sections named `config adapter` will appear here.'))]; + return E('div', { 'class': 'mb-card' }, [ + E('div', { 'class': 'mb-card-header' }, [ + E('div', { 'class': 'mb-card-title' }, [E('span', {}, '🧩'), _('Adapter preferences')]) + ]), + E('div', { 'class': 'mb-adapter-grid' }, items) + ]); + }, + + renderAdapterRow: function(adapter) { + var id = adapter.id || adapter.section || adapter.preset || adapter.vendor + ':' + adapter.product; + var inputId = this.makeAdapterInputId(id, 'label'); + return E('div', { 'class': 'mb-adapter-row' }, [ + E('div', { 'class': 'mb-adapter-header' }, [ + E('div', {}, [ + E('strong', {}, adapter.label || id || _('Adapter')), + E('div', { 'class': 'mb-profile-meta' }, [ + adapter.vendor && adapter.product ? _('VID:PID ') + adapter.vendor + ':' + adapter.product : null, + adapter.port ? _('Port ') + adapter.port : null + ].filter(Boolean).map(function(entry) { + return E('span', {}, entry); + })) + ]), + E('label', { 'class': 'mb-switch' }, [ + E('input', { + 'type': 'checkbox', + 'id': this.makeAdapterInputId(id, 'enabled'), + 'checked': adapter.enabled !== false && adapter.enabled !== '0' + }), + E('span', {}, _('Enabled')) + ]) + ]), + this.input(this.makeAdapterInputId(id, 'custom-label'), _('Display label'), adapter.label || id), + this.input(this.makeAdapterInputId(id, 'custom-port'), _('Preferred /dev/tty*'), adapter.port || '', 'text'), + adapter.notes ? E('p', { 'class': 'mb-profile-notes' }, adapter.notes) : null + ]); + }, + input: function(id, label, value, type) { return E('div', { 'class': 'mb-input-group' }, [ E('label', { 'class': 'mb-stat-label', 'for': id }, label), @@ -59,8 +102,12 @@ return view.extend({ ]); }, - saveSettings: function() { - var payload = { + makeAdapterInputId: function(id, field) { + return 'adapter-' + (id || 'x').replace(/[^a-z0-9_-]/ig, '_') + '-' + field; + }, + + collectSettings: function() { + return { host: document.getElementById('broker-host').value, port: parseInt(document.getElementById('broker-port').value, 10) || 1883, username: document.getElementById('username').value, @@ -68,6 +115,33 @@ return view.extend({ base_topic: document.getElementById('base-topic').value, retention: parseInt(document.getElementById('retention').value, 10) || 7 }; + }, + + collectAdapters: function() { + var adapters = {}; + var list = this.currentAdapters || []; + list.forEach(function(adapter) { + var id = adapter.id || adapter.section; + if (!id) + return; + var enabledEl = document.getElementById(this.makeAdapterInputId(id, 'enabled')); + var labelEl = document.getElementById(this.makeAdapterInputId(id, 'custom-label')); + var portEl = document.getElementById(this.makeAdapterInputId(id, 'custom-port')); + adapters[id] = { + enabled: enabledEl ? (enabledEl.checked ? 1 : 0) : 1, + label: labelEl ? labelEl.value : (adapter.label || ''), + port: portEl ? portEl.value : (adapter.port || ''), + preset: adapter.preset || '', + vendor: adapter.vendor || '', + product: adapter.product || '' + }; + }, this); + return adapters; + }, + + savePreferences: function() { + var payload = this.collectSettings(); + payload.adapters = this.collectAdapters(); ui.showModal(_('Saving MQTT settings'), [ E('p', {}, _('Applying broker configuration…')), diff --git a/luci-app-mqtt-bridge/root/usr/libexec/rpcd/luci.mqtt-bridge b/luci-app-mqtt-bridge/root/usr/libexec/rpcd/luci.mqtt-bridge index 934fad01..a63ccf0f 100755 --- a/luci-app-mqtt-bridge/root/usr/libexec/rpcd/luci.mqtt-bridge +++ b/luci-app-mqtt-bridge/root/usr/libexec/rpcd/luci.mqtt-bridge @@ -113,6 +113,38 @@ append_configured_adapters() { json_close_array } +apply_adapter_settings() { + local adapter_keys + json_get_keys adapter_keys + for adapter in $adapter_keys; do + json_select "$adapter" || continue + local enabled label port vendor product preset + json_get_var enabled enabled + json_get_var label label + json_get_var port port + json_get_var vendor vendor + json_get_var product product + json_get_var preset preset + json_select .. + [ -n "$adapter" ] || continue + + [ -n "$enabled" ] && uci set mqtt-bridge.adapter."$adapter".enabled="$enabled" + if [ -n "$label" ]; then + uci set mqtt-bridge.adapter."$adapter".title="$label" + else + uci delete mqtt-bridge.adapter."$adapter".title >/dev/null 2>&1 + fi + if [ -n "$port" ]; then + uci set mqtt-bridge.adapter."$adapter".port="$port" + else + uci delete mqtt-bridge.adapter."$adapter".port >/dev/null 2>&1 + fi + [ -n "$vendor" ] && uci set mqtt-bridge.adapter."$adapter".vendor="$vendor" + [ -n "$product" ] && uci set mqtt-bridge.adapter."$adapter".product="$product" + [ -n "$preset" ] && uci set mqtt-bridge.adapter."$adapter".preset="$preset" + done +} + status() { json_init json_add_string "broker" "$(uci -q get mqtt-bridge.broker.host || echo 'localhost')" @@ -208,6 +240,12 @@ apply_settings() { json_get_var password password json_get_var base_topic base_topic json_get_var retention retention + json_get_type adapters_type adapters + if [ "$adapters_type" = "object" ]; then + json_select adapters + apply_adapter_settings + json_select .. + fi json_cleanup [ -n "$host" ] && uci set mqtt-bridge.broker.host="$host"