'use strict'; 'require view'; 'require form'; 'require ui'; 'require uci'; 'require rpc'; 'require mqtt-bridge/api as API'; 'require secubox-theme/theme as Theme'; 'require secubox/kiss-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': 'kiss-btn kiss-btn-blue', '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 KissTheme.wrap([container], 'admin/services/mqtt-bridge/adapters'); }, 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': 'kiss-btn', 'click': ui.createHandlerFn(this, this.handleTestAdapter, adapter) }, _('Test')), E('button', { 'class': 'kiss-btn', 'click': ui.createHandlerFn(this, this.handleConfigureAdapter, adapter) }, _('Configure')), E('button', { 'class': 'kiss-btn kiss-btn-red', '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': 'kiss-btn kiss-btn-blue', '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': 'kiss-btn', 'click': ui.hideModal }, _('Cancel')), E('button', { 'class': 'kiss-btn kiss-btn-blue', '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': 'kiss-btn', 'click': ui.hideModal }, _('Cancel')), E('button', { 'class': 'kiss-btn kiss-btn-red', '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 });