feat: adapter preferences editing

This commit is contained in:
CyberMind-FR 2025-12-29 14:59:43 +01:00
parent 790719e2a1
commit 6c3c96a70b
5 changed files with 159 additions and 9 deletions

View File

@ -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`. - Managed with the standard init script: `service mqtt-bridge start|stop|status`.
- Writes state transitions to the system log (`logread -e mqtt-bridge-monitor`). - 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. - 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. 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.

View File

@ -9,7 +9,7 @@ USB-aware MQTT orchestrator for SecuBox routers. The application discovers USB s
- `overview.js` broker status, metrics, quick actions. - `overview.js` broker status, metrics, quick actions.
- `devices.js` USB/tasmota sensor list with pairing wizard. - `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 ## RPC Methods

View File

@ -692,6 +692,43 @@ pre {
overflow-x: auto; 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) { @media (max-width: 768px) {
.mqtt-bridge-dashboard { .mqtt-bridge-dashboard {
padding: 16px; padding: 16px;

View File

@ -13,17 +13,20 @@ Theme.init({ language: lang });
return view.extend({ return view.extend({
load: function() { load: function() {
return API.getStatus().then(function(status) { return API.getStatus();
return status.settings || {};
});
}, },
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' }, [ 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('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('mqtt-bridge/common.css') }), E('link', { 'rel': 'stylesheet', 'href': L.resource('mqtt-bridge/common.css') }),
Nav.renderTabs('settings'), Nav.renderTabs('settings'),
this.renderSettingsCard(settings || {}) this.renderSettingsCard(settings || {}),
this.renderAdapterCard(adapters || [])
]); ]);
return container; return container;
}, },
@ -42,11 +45,51 @@ return view.extend({
this.input('retention', _('Retention (days)'), settings.retention || 7, 'number') this.input('retention', _('Retention (days)'), settings.retention || 7, 'number')
]), ]),
E('div', { 'style': 'margin-top:16px;' }, [ 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) { input: function(id, label, value, type) {
return E('div', { 'class': 'mb-input-group' }, [ return E('div', { 'class': 'mb-input-group' }, [
E('label', { 'class': 'mb-stat-label', 'for': id }, label), E('label', { 'class': 'mb-stat-label', 'for': id }, label),
@ -59,8 +102,12 @@ return view.extend({
]); ]);
}, },
saveSettings: function() { makeAdapterInputId: function(id, field) {
var payload = { return 'adapter-' + (id || 'x').replace(/[^a-z0-9_-]/ig, '_') + '-' + field;
},
collectSettings: function() {
return {
host: document.getElementById('broker-host').value, host: document.getElementById('broker-host').value,
port: parseInt(document.getElementById('broker-port').value, 10) || 1883, port: parseInt(document.getElementById('broker-port').value, 10) || 1883,
username: document.getElementById('username').value, username: document.getElementById('username').value,
@ -68,6 +115,33 @@ return view.extend({
base_topic: document.getElementById('base-topic').value, base_topic: document.getElementById('base-topic').value,
retention: parseInt(document.getElementById('retention').value, 10) || 7 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'), [ ui.showModal(_('Saving MQTT settings'), [
E('p', {}, _('Applying broker configuration…')), E('p', {}, _('Applying broker configuration…')),

View File

@ -113,6 +113,38 @@ append_configured_adapters() {
json_close_array 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() { status() {
json_init json_init
json_add_string "broker" "$(uci -q get mqtt-bridge.broker.host || echo 'localhost')" 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 password password
json_get_var base_topic base_topic json_get_var base_topic base_topic
json_get_var retention retention 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 json_cleanup
[ -n "$host" ] && uci set mqtt-bridge.broker.host="$host" [ -n "$host" ] && uci set mqtt-bridge.broker.host="$host"