secubox-openwrt/package/secubox/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/adapters.js
CyberMind-FR db3a41928e fix(luci): Fix require syntax in all LuCI views - use slashes instead of dots
All 'require module.submodule' directives changed to 'require module/submodule'
to match LuCI's module loading convention.

Affected packages:
- luci-app-auth-guardian
- luci-app-glances
- luci-app-localai
- luci-app-magicmirror2
- luci-app-mitmproxy
- luci-app-mmpm
- luci-app-mqtt-bridge
- luci-app-ndpid
- luci-app-network-modes
- luci-app-secubox-admin
- luci-app-secubox-portal
- luci-app-wireguard-dashboard

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 17:15:21 +01:00

474 lines
15 KiB
JavaScript

'use strict';
'require view';
'require form';
'require ui';
'require uci';
'require rpc';
'require mqtt-bridge/api as API';
'require secubox-theme/theme as Theme';
return view.extend({
load: function() {
return Promise.all([
API.getAdapterStatus().catch(function(err) {
console.warn('MQTT Bridge backend not available:', err);
return { adapters: [], backend_available: false };
}),
API.detectIoTAdapters().catch(function(err) {
console.warn('MQTT Bridge backend not available:', err);
return { zigbee: [], zwave: [], modbus: [], backend_available: false };
}),
L.resolveDefault(uci.load('mqtt-bridge'))
]);
},
render: function(data) {
Theme.init({ language: 'en' });
var adapterStatus = data[0] || { adapters: [] };
var detectedAdapters = data[1] || { zigbee: [], zwave: [], modbus: [] };
// Import theme CSS
document.head.appendChild(E('link', {
'rel': 'stylesheet',
'href': L.resource('secubox-theme/core/variables.css')
}));
document.head.appendChild(E('link', {
'rel': 'stylesheet',
'href': L.resource('secubox-theme/themes/cyberpunk.css')
}));
document.head.appendChild(E('link', {
'rel': 'stylesheet',
'href': L.resource('secubox-theme/components/cards.css')
}));
document.head.appendChild(E('link', {
'rel': 'stylesheet',
'href': L.resource('secubox-theme/components/buttons.css')
}));
var backendMissing = (adapterStatus.backend_available === false || detectedAdapters.backend_available === false);
var containerContent = [
E('style', {}, `
.mqtt-adapters-container {
padding: var(--spacing-lg);
}
.adapters-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
}
.adapters-title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-semibold);
color: var(--sh-text-primary);
}
.adapters-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
}
.adapter-card {
background: var(--sh-bg-card);
border: 1px solid var(--sh-border);
border-radius: var(--radius-md);
padding: var(--spacing-md);
transition: all 0.2s;
}
.adapter-card:hover {
box-shadow: var(--sh-hover-shadow);
border-color: var(--sh-primary);
}
[data-secubox-theme="cyberpunk"] .adapter-card:hover {
border-color: var(--cyber-accent-primary);
box-shadow: 0 0 20px rgba(102, 126, 234, 0.2);
}
.adapter-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-sm);
}
.adapter-title {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--sh-text-primary);
}
.adapter-type {
display: inline-block;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
text-transform: uppercase;
background: var(--sh-bg-secondary);
color: var(--sh-text-secondary);
}
.adapter-type.zigbee { background: rgba(99, 102, 241, 0.1); color: #6366f1; }
.adapter-type.zwave { background: rgba(139, 92, 246, 0.1); color: #8b5cf6; }
.adapter-type.modbus { background: rgba(245, 158, 11, 0.1); color: #f59e0b; }
.adapter-type.serial { background: rgba(156, 163, 175, 0.1); color: #9ca3af; }
.adapter-info {
margin: var(--spacing-sm) 0;
color: var(--sh-text-secondary);
font-size: var(--font-size-sm);
}
.adapter-info-row {
display: flex;
justify-content: space-between;
margin: 4px 0;
}
.adapter-status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
}
.adapter-status.online {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
}
.adapter-status.error {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.adapter-status.missing {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
.adapter-status.unknown {
background: rgba(156, 163, 175, 0.1);
color: #9ca3af;
}
.adapter-actions {
display: flex;
gap: var(--spacing-xs);
margin-top: var(--spacing-md);
padding-top: var(--spacing-md);
border-top: 1px solid var(--sh-border);
}
.detected-section {
margin-top: var(--spacing-xl);
}
.detected-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
}
.detected-title {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
color: var(--sh-text-primary);
}
.detected-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--spacing-md);
}
.detected-card {
background: var(--sh-bg-card);
border: 1px solid var(--sh-border);
border-radius: var(--radius-md);
padding: var(--spacing-sm);
}
.detected-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xs);
}
.empty-state {
text-align: center;
padding: var(--spacing-xl);
color: var(--sh-text-secondary);
}
.empty-state-icon {
font-size: 48px;
margin-bottom: var(--spacing-md);
opacity: 0.5;
}
`),
// Page Header
E('div', { 'class': 'adapters-header' }, [
E('h2', { 'class': 'adapters-title' }, _('USB IoT Adapters')),
E('button', {
'class': 'cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, this.handleScanUSB)
}, _('Scan USB Devices'))
]),
// Configured Adapters Section
E('div', {}, [
E('h3', { 'class': 'detected-title' }, _('Configured Adapters')),
this.renderConfiguredAdapters(adapterStatus.adapters)
]),
// Detected Devices Section
E('div', { 'class': 'detected-section' }, [
E('div', { 'class': 'detected-header' }, [
E('h3', { 'class': 'detected-title' }, _('Detected USB Devices')),
E('div', {}, [
E('span', { 'style': 'color: var(--sh-text-secondary); font-size: var(--font-size-sm);' },
_('Found: %d Zigbee, %d Z-Wave, %d ModBus').format(
detectedAdapters.zigbee.length,
detectedAdapters.zwave.length,
detectedAdapters.modbus.length
)
)
])
]),
this.renderDetectedDevices(detectedAdapters)
])
];
// Insert warning banner if backend is not available
if (backendMissing) {
containerContent.splice(1, 0, E('div', {
'style': 'background: #fef2f2; border-left: 4px solid #ef4444; border-radius: var(--radius-md); padding: var(--spacing-md); margin-bottom: var(--spacing-lg);'
}, [
E('h3', { 'style': 'color: #991b1b; margin: 0 0 8px 0; font-size: var(--font-size-lg);' },
'⚠️ ' + _('Backend Not Installed')),
E('p', { 'style': 'color: #991b1b; margin: 0;' },
_('The MQTT Bridge backend (RPCD script) is not installed. USB detection and adapter management require the backend to be deployed.'))
]));
}
var container = E('div', { 'class': 'mqtt-adapters-container' }, containerContent);
return container;
},
renderConfiguredAdapters: function(adapters) {
if (!adapters || adapters.length === 0) {
return E('div', { 'class': 'empty-state' }, [
E('div', { 'class': 'empty-state-icon' }, '🔌'),
E('p', {}, _('No adapters configured yet.')),
E('p', { 'style': 'font-size: var(--font-size-sm);' },
_('Scan for USB devices and import them to get started.'))
]);
}
var grid = E('div', { 'class': 'adapters-grid' });
adapters.forEach(function(adapter) {
var statusDot = E('span', {
'style': 'display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; background: ' +
(adapter.health === 'online' ? '#22c55e' :
adapter.health === 'error' ? '#ef4444' :
adapter.health === 'missing' ? '#f59e0b' : '#9ca3af')
});
var card = E('div', { 'class': 'adapter-card' }, [
E('div', { 'class': 'adapter-header' }, [
E('div', { 'class': 'adapter-title' }, adapter.title || adapter.id),
E('span', { 'class': 'adapter-type ' + adapter.type }, adapter.type)
]),
E('div', { 'class': 'adapter-info' }, [
E('div', { 'class': 'adapter-info-row' }, [
E('span', {}, _('Port:')),
E('span', { 'style': 'font-family: monospace;' }, adapter.port || _('Not detected'))
]),
adapter.vid && E('div', { 'class': 'adapter-info-row' }, [
E('span', {}, _('VID:PID:')),
E('span', { 'style': 'font-family: monospace;' },
'%s:%s'.format(adapter.vid || '—', adapter.pid || '—'))
]),
E('div', { 'class': 'adapter-info-row' }, [
E('span', {}, _('Status:')),
E('span', { 'class': 'adapter-status ' + adapter.health }, [
statusDot,
_(adapter.health.charAt(0).toUpperCase() + adapter.health.slice(1))
])
]),
adapter.usb_present !== undefined && E('div', { 'class': 'adapter-info-row' }, [
E('span', {}, _('USB Present:')),
E('span', {}, adapter.usb_present ? _('Yes') : _('No'))
])
]),
E('div', { 'class': 'adapter-actions' }, [
E('button', {
'class': 'cbi-button cbi-button-neutral',
'click': ui.createHandlerFn(this, this.handleTestAdapter, adapter)
}, _('Test')),
E('button', {
'class': 'cbi-button cbi-button-neutral',
'click': ui.createHandlerFn(this, this.handleConfigureAdapter, adapter)
}, _('Configure')),
E('button', {
'class': 'cbi-button cbi-button-negative',
'click': ui.createHandlerFn(this, this.handleRemoveAdapter, adapter)
}, _('Remove'))
])
]);
grid.appendChild(card);
}.bind(this));
return grid;
},
renderDetectedDevices: function(detected) {
var allDevices = [
...(detected.zigbee || []).map(d => ({ ...d, type: 'zigbee' })),
...(detected.zwave || []).map(d => ({ ...d, type: 'zwave' })),
...(detected.modbus || []).map(d => ({ ...d, type: 'modbus' }))
];
if (allDevices.length === 0) {
return E('div', { 'class': 'empty-state' }, [
E('div', { 'class': 'empty-state-icon' }, '🔍'),
E('p', {}, _('No IoT USB devices detected.')),
E('p', { 'style': 'font-size: var(--font-size-sm);' },
_('Click "Scan USB Devices" to refresh detection.'))
]);
}
var grid = E('div', { 'class': 'detected-grid' });
allDevices.forEach(function(device) {
var card = E('div', { 'class': 'detected-card' }, [
E('div', { 'class': 'detected-card-header' }, [
E('strong', {}, device.name),
E('span', { 'class': 'adapter-type ' + device.type }, device.type)
]),
E('div', { 'style': 'font-size: var(--font-size-xs); color: var(--sh-text-secondary); margin: 4px 0;' }, [
E('div', {}, 'VID:PID: ' + device.vid + ':' + device.pid),
device.port && E('div', {}, 'Port: ' + device.port),
device.bus && E('div', {}, 'Bus: ' + device.bus + ', Device: ' + device.device)
]),
E('button', {
'class': 'cbi-button cbi-button-action',
'style': 'margin-top: 8px; width: 100%;',
'click': ui.createHandlerFn(this, this.handleImportDevice, device)
}, _('Import'))
]);
grid.appendChild(card);
}.bind(this));
return grid;
},
handleScanUSB: function() {
ui.showModal(_('Scanning USB Devices'), [
E('p', { 'class': 'spinning' }, _('Scanning for IoT USB adapters...'))
]);
return API.detectIoTAdapters().then(function(result) {
ui.hideModal();
var totalFound = (result.zigbee || []).length +
(result.zwave || []).length +
(result.modbus || []).length;
ui.addNotification(null,
E('p', _('Found %d IoT USB device(s)').format(totalFound)),
'info'
);
// Reload page to show detected devices
window.location.reload();
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Scan failed: %s').format(err.message)), 'error');
});
},
handleImportDevice: function(device) {
var adapterId = device.type + '_' + device.vid + device.pid;
ui.showModal(_('Import Device'), [
E('p', _('Import %s as a configured adapter?').format(device.name)),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button cbi-button-neutral',
'click': ui.hideModal
}, _('Cancel')),
E('button', {
'class': 'cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, function() {
return API.configureAdapter(
adapterId,
true,
device.type,
device.name,
device.vid,
device.pid,
device.port || ''
).then(function() {
ui.hideModal();
ui.addNotification(null, E('p', _('Adapter imported successfully')), 'info');
window.location.reload();
}).catch(function(err) {
ui.addNotification(null, E('p', _('Import failed: %s').format(err.message)), 'error');
});
})
}, _('Import'))
])
]);
},
handleTestAdapter: function(adapter) {
if (!adapter.port) {
ui.addNotification(null, E('p', _('No port configured for this adapter')), 'warning');
return;
}
ui.showModal(_('Testing Connection'), [
E('p', { 'class': 'spinning' }, _('Testing %s...').format(adapter.port))
]);
return API.testConnection(adapter.port).then(function(result) {
ui.hideModal();
if (result.accessible) {
ui.addNotification(null, E('p', _('Port %s is accessible').format(adapter.port)), 'info');
} else {
ui.addNotification(null, E('p', _('Port %s is not accessible').format(adapter.port)), 'warning');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Test failed: %s').format(err.message)), 'error');
});
},
handleConfigureAdapter: function(adapter) {
ui.addNotification(null, E('p', _('Configuration dialog not yet implemented')), 'info');
// TODO: Open configuration modal
},
handleRemoveAdapter: function(adapter) {
ui.showModal(_('Remove Adapter'), [
E('p', _('Remove adapter "%s"?').format(adapter.title || adapter.id)),
E('p', { 'style': 'color: var(--sh-text-secondary); font-size: var(--font-size-sm);' },
_('This will remove the adapter configuration from UCI.')),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button cbi-button-neutral',
'click': ui.hideModal
}, _('Cancel')),
E('button', {
'class': 'cbi-button cbi-button-negative',
'click': ui.createHandlerFn(this, function() {
return API.resetAdapter(adapter.id).then(function() {
ui.hideModal();
ui.addNotification(null, E('p', _('Adapter removed successfully')), 'info');
window.location.reload();
}).catch(function(err) {
ui.addNotification(null, E('p', _('Remove failed: %s').format(err.message)), 'error');
});
})
}, _('Remove'))
])
]);
},
handleSaveOrder: null,
handleSave: null,
handleReset: null
});