From 16e16a6180b77ca58cdcfefdf28ea6c0d3cb3085 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 29 Dec 2025 15:02:15 +0100 Subject: [PATCH] feat: import presets into adapter prefs --- .../resources/view/mqtt-bridge/settings.js | 122 +++++++++++++++++- .../root/usr/libexec/rpcd/luci.mqtt-bridge | 4 + 2 files changed, 121 insertions(+), 5 deletions(-) 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 e414f23f..fe58e59c 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 @@ -5,6 +5,7 @@ 'require mqtt-bridge/nav as Nav'; 'require ui'; 'require form'; +'require dom'; var lang = (typeof L !== 'undefined' && L.env && L.env.lang) || (document.documentElement && document.documentElement.getAttribute('lang')) || @@ -19,14 +20,17 @@ return view.extend({ render: function(payload) { var settings = (payload && payload.settings) || {}; var adapters = (payload && payload.adapters) || []; - this.currentAdapters = adapters; + var profiles = (payload && payload.profiles) || []; + this.currentAdapters = this.cloneAdapters(adapters || []); + this.liveProfiles = profiles || []; 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.renderAdapterCard(adapters || []) + this.renderAdapterCard(this.currentAdapters), + this.renderPresetCard(this.liveProfiles) ]); return container; }, @@ -51,16 +55,28 @@ return view.extend({ }, 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.'))]; + var grid = E('div', { 'class': 'mb-adapter-grid' }, this.renderAdapterRows(adapters)); + this.adapterGrid = grid; + 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) + grid ]); }, + renderAdapterRows: function(adapters) { + var self = this; + if (!adapters || !adapters.length) { + return [E('p', { 'style': 'color:var(--mb-muted);' }, + _('No adapters configured yet. Existing `config adapter` entries will appear once the monitor updates.'))]; + } + return adapters.map(function(adapter) { + return self.renderAdapterRow(adapter); + }); + }, + renderAdapterRow: function(adapter) { var id = adapter.id || adapter.section || adapter.preset || adapter.vendor + ':' + adapter.product; var inputId = this.makeAdapterInputId(id, 'label'); @@ -90,6 +106,48 @@ return view.extend({ ]); }, + renderPresetCard: function(profiles) { + var self = this; + var rows = (profiles && profiles.length) ? profiles.map(function(profile) { + return self.renderPresetRow(profile); + }) : [ + E('p', { 'style': 'color:var(--mb-muted);' }, + _('When USB presets are detected (VID/PID), they will be displayed here so you can import them as adapters.')) + ]; + + return E('div', { 'class': 'mb-card' }, [ + E('div', { 'class': 'mb-card-header' }, [ + E('div', { 'class': 'mb-card-title' }, [E('span', {}, '🛰️'), _('Detected presets')]) + ]), + E('div', { 'class': 'mb-profile-grid' }, rows) + ]); + }, + + renderPresetRow: function(profile) { + var self = this; + var meta = [ + (profile.vendor && profile.product) ? _('VID:PID ') + profile.vendor + ':' + profile.product : null, + profile.bus ? _('Bus ') + profile.bus : null, + profile.device ? _('Device ') + profile.device : null, + profile.port ? _('Port ') + profile.port : null + ].filter(Boolean); + return E('div', { 'class': 'mb-profile-card' }, [ + E('div', { 'class': 'mb-profile-header' }, [ + E('div', {}, [ + E('strong', {}, profile.label || profile.title || _('USB profile')), + E('div', { 'class': 'mb-profile-meta' }, meta.map(function(entry) { + return E('span', {}, entry); + })) + ]), + E('button', { + 'class': 'mb-btn mb-btn-secondary', + 'click': function() { self.importProfile(profile); } + }, ['➕ ', _('Import preset')]) + ]), + profile.notes ? E('p', { 'class': 'mb-profile-notes' }, profile.notes) : null + ]); + }, + input: function(id, label, value, type) { return E('div', { 'class': 'mb-input-group' }, [ E('label', { 'class': 'mb-stat-label', 'for': id }, label), @@ -106,6 +164,24 @@ return view.extend({ return 'adapter-' + (id || 'x').replace(/[^a-z0-9_-]/ig, '_') + '-' + field; }, + normalizeAdapterId: function(id) { + return (id || '').replace(/[^a-z0-9_-]/ig, '_') || 'adapter_' + Math.random().toString(36).slice(2, 7); + }, + + cloneAdapters: function(list) { + var cloned = []; + (list || []).forEach(function(item) { + var copy = {}; + if (item) { + Object.keys(item).forEach(function(key) { + copy[key] = item[key]; + }); + } + cloned.push(copy); + }); + return cloned; + }, + collectSettings: function() { return { host: document.getElementById('broker-host').value, @@ -139,6 +215,42 @@ return view.extend({ return adapters; }, + refreshAdapterGrid: function() { + if (!this.adapterGrid) + return; + dom.content(this.adapterGrid, this.renderAdapterRows(this.currentAdapters)); + }, + + importProfile: function(profile) { + if (!profile) + return; + var idSource = profile.id || profile.preset || (profile.vendor ? profile.vendor + '_' + profile.product : null); + var id = this.normalizeAdapterId(idSource); + + if (!this.currentAdapters) + this.currentAdapters = []; + + var exists = this.currentAdapters.some(function(entry) { + return (entry.id || entry.section) === id; + }); + if (exists) { + ui.addNotification(null, E('p', {}, _('Preset already imported. Adjust preferences below.')), 'info'); + return; + } + + this.currentAdapters.push({ + id: id, + label: profile.label || profile.title || id, + vendor: profile.vendor || '', + product: profile.product || '', + port: profile.port || '', + enabled: profile.detected ? 1 : 0, + preset: profile.id || profile.preset || '' + }); + this.refreshAdapterGrid(); + ui.addNotification(null, E('p', {}, _('Preset added. Remember to save preferences.')), 'info'); + }, + savePreferences: function() { var payload = this.collectSettings(); payload.adapters = this.collectAdapters(); 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 a63ccf0f..1a51ab84 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 @@ -128,6 +128,10 @@ apply_adapter_settings() { json_select .. [ -n "$adapter" ] || continue + if ! uci -q get mqtt-bridge.adapter."$adapter" >/dev/null 2>&1; then + uci set mqtt-bridge.adapter."$adapter"="adapter" + fi + [ -n "$enabled" ] && uci set mqtt-bridge.adapter."$adapter".enabled="$enabled" if [ -n "$label" ]; then uci set mqtt-bridge.adapter."$adapter".title="$label"