'use strict'; 'require view'; 'require dom'; 'require poll'; 'require ui'; 'require uci'; 'require form'; 'require jabber.api as api'; return view.extend({ handleAction: function(action, args) { var self = this; var btn = document.activeElement; ui.showModal(_('Please wait...'), [ E('p', { 'class': 'spinning' }, _('Processing request...')) ]); var promise; switch(action) { case 'start': promise = api.start(); break; case 'stop': promise = api.stop(); break; case 'install': promise = api.install(); break; case 'uninstall': if (!confirm(_('This will remove the Jabber container. User data will be preserved. Continue?'))) return ui.hideModal(); promise = api.uninstall(); break; case 'update': promise = api.update(); break; case 'configure_haproxy': promise = api.configureHaproxy(); break; case 'emancipate': var domain = args; if (!domain) { ui.hideModal(); ui.addNotification(null, E('p', _('Domain is required')), 'error'); return; } promise = api.emancipate(domain); break; case 'user_add': var jid = args.jid; var password = args.password; if (!jid) { ui.hideModal(); ui.addNotification(null, E('p', _('JID is required')), 'error'); return; } promise = api.userAdd(jid, password); break; case 'user_del': if (!confirm(_('Delete user ') + args + '?')) return ui.hideModal(); promise = api.userDel(args); break; case 'jingle_enable': var stunServer = args || 'stun.l.google.com:19302'; promise = api.jingleEnable(stunServer); break; case 'jingle_disable': promise = api.jingleDisable(); break; case 'sms_config': var sender = args; if (!sender) { ui.hideModal(); ui.addNotification(null, E('p', _('Sender name is required')), 'error'); return; } promise = api.smsConfig(sender); break; case 'sms_send': if (!args.to || !args.message) { ui.hideModal(); ui.addNotification(null, E('p', _('Phone number and message are required')), 'error'); return; } promise = api.smsSend(args.to, args.message); break; case 'voicemail_config': var notifyJid = args; if (!notifyJid) { ui.hideModal(); ui.addNotification(null, E('p', _('Notification JID is required')), 'error'); return; } promise = api.voicemailConfig(notifyJid); break; default: ui.hideModal(); return; } promise.then(function(res) { ui.hideModal(); if (res && res.success) { var msg = res.message || _('Action completed'); if (res.password) { msg += '\n' + _('Password: ') + res.password; } ui.addNotification(null, E('p', { 'style': 'white-space: pre-wrap;' }, msg), 'success'); self.load().then(function(data) { dom.content(document.querySelector('#jabber-content'), self.renderContent(data)); }); } else { ui.addNotification(null, E('p', res.error || _('Action failed')), 'error'); } }).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', _('Error: ') + e.message), 'error'); }); }, load: function() { return Promise.all([ api.status(), api.userList(), uci.load('jabber'), api.jingleStatus(), api.smsStatus(), api.voicemailStatus() ]); }, renderInstallWizard: function() { var self = this; return E('div', { 'class': 'cbi-section' }, [ E('h3', {}, _('Jabber/XMPP Server')), E('p', {}, _('Prosody is a modern XMPP server written in Lua. It aims to be easy to set up and configure, and efficient with system resources.')), E('div', { 'class': 'cbi-value' }, [ E('h4', {}, _('Features')), E('ul', {}, [ E('li', {}, _('Secure messaging with end-to-end encryption (OMEMO)')), E('li', {}, _('Multi-user chat rooms (MUC)')), E('li', {}, _('File sharing with HTTP upload')), E('li', {}, _('Server-to-server federation (S2S)')), E('li', {}, _('BOSH and WebSocket for web clients')), E('li', {}, _('Message archiving (MAM)')) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('h4', {}, _('Compatible Clients')), E('ul', {}, [ E('li', {}, _('Conversations (Android)')), E('li', {}, _('Monal (iOS/macOS)')), E('li', {}, _('Gajim (Windows/Linux)')), E('li', {}, _('Dino (Linux)')), E('li', {}, _('Converse.js (Web)')) ]) ]), E('div', { 'class': 'cbi-page-actions' }, [ E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': function() { self.handleAction('install'); } }, _('Install Jabber/XMPP')) ]) ]); }, renderStatusBadge: function(running) { var color = running === 'true' ? '#4CAF50' : '#f44336'; var text = running === 'true' ? _('Running') : _('Stopped'); return E('span', { 'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:' + color }, text); }, renderContent: function(data) { var self = this; var status = data[0] || {}; var userListData = data[1] || {}; var jingleStatus = data[3] || {}; var smsStatus = data[4] || {}; var voicemailStatus = data[5] || {}; if (status.container_state === 'not_installed') { return this.renderInstallWizard(); } var running = status.running === 'true'; var haproxyConfigured = status.haproxy === '1'; var domain = status.domain || ''; var hostname = status.hostname || 'jabber.local'; // Parse user list var users = []; if (userListData.users) { users = userListData.users.split(',').filter(function(u) { return u.length > 0; }); } var accessUrl = ''; if (running) { if (domain && haproxyConfigured) { accessUrl = 'https://' + domain; } else { accessUrl = 'http://192.168.255.1:' + (status.http_port || '5280'); } } return E('div', { 'class': 'cbi-section' }, [ E('h3', {}, _('Jabber/XMPP Server (Prosody)')), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Status')), E('div', { 'class': 'cbi-value-field' }, this.renderStatusBadge(status.running)) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Hostname')), E('div', { 'class': 'cbi-value-field' }, hostname) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('C2S Port')), E('div', { 'class': 'cbi-value-field' }, status.c2s_port || '5222') ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('S2S Port')), E('div', { 'class': 'cbi-value-field' }, [ status.s2s_port || '5269', ' ', E('span', { 'style': 'display:inline-block;padding:2px 8px;border-radius:3px;color:#fff;background:' + (status.s2s_enabled === '1' ? '#4CAF50' : '#9e9e9e') }, status.s2s_enabled === '1' ? _('Federation ON') : _('Federation OFF')) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('HTTP/BOSH Port')), E('div', { 'class': 'cbi-value-field' }, status.http_port || '5280') ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Users')), E('div', { 'class': 'cbi-value-field' }, status.user_count || '0') ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('MUC (Chat Rooms)')), E('div', { 'class': 'cbi-value-field' }, [ E('span', { 'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:' + (status.muc_enabled === '1' ? '#4CAF50' : '#9e9e9e') }, status.muc_enabled === '1' ? _('Enabled') : _('Disabled')) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('HAProxy')), E('div', { 'class': 'cbi-value-field' }, [ E('span', { 'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:' + (haproxyConfigured ? '#4CAF50' : '#9e9e9e') }, haproxyConfigured ? _('Configured') : _('Not configured')), ' ', !haproxyConfigured ? E('button', { 'class': 'btn cbi-button', 'click': function() { self.handleAction('configure_haproxy'); } }, _('Configure')) : '' ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Domain')), E('div', { 'class': 'cbi-value-field' }, domain || _('Not configured')) ]), E('hr'), E('h4', {}, _('Service Controls')), E('div', { 'class': 'cbi-page-actions', 'style': 'margin-bottom: 20px;' }, [ running ? E('button', { 'class': 'btn cbi-button cbi-button-negative', 'click': function() { self.handleAction('stop'); } }, _('Stop')) : E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': function() { self.handleAction('start'); } }, _('Start')), ' ', E('button', { 'class': 'btn cbi-button', 'click': function() { self.handleAction('update'); } }, _('Update')), ' ', E('button', { 'class': 'btn cbi-button cbi-button-negative', 'click': function() { self.handleAction('uninstall'); } }, _('Uninstall')) ]), E('hr'), E('h4', {}, _('User Management')), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('New User')), E('div', { 'class': 'cbi-value-field' }, [ E('input', { 'type': 'text', 'id': 'new-user-jid', 'class': 'cbi-input-text', 'placeholder': 'user@' + hostname, 'style': 'width: 200px;' }), ' ', E('input', { 'type': 'password', 'id': 'new-user-password', 'class': 'cbi-input-text', 'placeholder': _('Password (auto-generate if empty)'), 'style': 'width: 200px;' }), ' ', E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': function() { var jid = document.getElementById('new-user-jid').value; var password = document.getElementById('new-user-password').value; self.handleAction('user_add', { jid: jid, password: password }); } }, _('Add User')) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Registered Users')), E('div', { 'class': 'cbi-value-field' }, [ users.length > 0 ? E('table', { 'class': 'table', 'style': 'width: auto;' }, [ E('tr', { 'class': 'tr table-titles' }, [ E('th', { 'class': 'th' }, _('JID')), E('th', { 'class': 'th' }, _('Actions')) ]) ].concat(users.map(function(user) { return E('tr', { 'class': 'tr' }, [ E('td', { 'class': 'td' }, user), E('td', { 'class': 'td' }, [ E('button', { 'class': 'btn cbi-button cbi-button-remove', 'click': function() { self.handleAction('user_del', user); } }, _('Delete')) ]) ]); }))) : E('em', {}, _('No users registered')) ]) ]), E('hr'), E('h4', {}, _('Emancipate (Public Exposure)')), E('p', {}, _('Make Jabber publicly accessible with SSL certificate, DNS records, and S2S federation.')), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Domain')), E('div', { 'class': 'cbi-value-field' }, [ E('input', { 'type': 'text', 'id': 'emancipate-domain', 'class': 'cbi-input-text', 'placeholder': 'xmpp.example.com', 'value': domain }) ]) ]), E('div', { 'class': 'cbi-page-actions' }, [ E('button', { 'class': 'btn cbi-button cbi-button-action', 'click': function() { var domainInput = document.getElementById('emancipate-domain'); self.handleAction('emancipate', domainInput.value); } }, _('Emancipate')) ]), E('p', { 'style': 'font-size: 12px; color: #666;' }, [ _('DNS records needed: A record for domain, SRV records for _xmpp-client._tcp and _xmpp-server._tcp') ]), E('hr'), E('h4', {}, _('Connection Info')), E('div', { 'style': 'background: #f5f5f5; padding: 15px; border-radius: 4px; font-family: monospace;' }, [ E('p', {}, [ E('strong', {}, _('XMPP Server: ')), hostname + ':' + (status.c2s_port || '5222') ]), E('p', {}, [ E('strong', {}, _('BOSH URL: ')), accessUrl + '/http-bind' ]), E('p', {}, [ E('strong', {}, _('WebSocket: ')), (domain && haproxyConfigured ? 'wss://' + domain : 'ws://192.168.255.1:' + (status.http_port || '5280')) + '/xmpp-websocket' ]), E('p', {}, [ E('strong', {}, _('Admin JID: ')), (status.admin_user || 'admin') + '@' + hostname ]) ]), E('hr'), // VoIP Integration - Jingle E('h4', {}, _('VoIP Integration - Jingle')), E('p', {}, _('Enable Jingle for voice/video calls between XMPP clients (Conversations, Dino, etc.)')), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Status')), E('div', { 'class': 'cbi-value-field' }, [ E('span', { 'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:' + (jingleStatus.enabled === '1' ? '#4CAF50' : '#9e9e9e') }, jingleStatus.enabled === '1' ? _('Enabled') : _('Disabled')), ' ', jingleStatus.enabled === '1' ? E('button', { 'class': 'btn cbi-button cbi-button-negative', 'click': function() { self.handleAction('jingle_disable'); } }, _('Disable')) : E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': function() { var stunServer = document.getElementById('jingle-stun').value || 'stun.l.google.com:19302'; self.handleAction('jingle_enable', stunServer); } }, _('Enable')) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('STUN Server')), E('div', { 'class': 'cbi-value-field' }, [ E('input', { 'type': 'text', 'id': 'jingle-stun', 'class': 'cbi-input-text', 'placeholder': 'stun.l.google.com:19302', 'value': jingleStatus.stun_server || 'stun.l.google.com:19302', 'style': 'width: 250px;' }) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('TURN Server')), E('div', { 'class': 'cbi-value-field' }, [ jingleStatus.turn_server || E('em', {}, _('Not configured')), E('p', { 'style': 'font-size: 12px; color: #666; margin: 5px 0 0;' }, _('TURN server for NAT traversal. Configure in /etc/config/jabber')) ]) ]), E('hr'), // VoIP Integration - SMS Relay E('h4', {}, _('VoIP Integration - SMS Relay')), E('p', {}, _('Send SMS messages via OVH API through XMPP. Requires OVH API credentials in VoIP settings.')), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Status')), E('div', { 'class': 'cbi-value-field' }, [ E('span', { 'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:' + (smsStatus.enabled === '1' ? '#4CAF50' : '#9e9e9e') }, smsStatus.enabled === '1' ? _('Enabled') : _('Disabled')), ' ', smsStatus.ovh_configured === '1' ? E('span', { 'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:#2196F3' }, _('OVH API Configured')) : E('span', { 'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:#ff9800' }, _('OVH API Not Configured')) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Sender Name')), E('div', { 'class': 'cbi-value-field' }, [ E('input', { 'type': 'text', 'id': 'sms-sender', 'class': 'cbi-input-text', 'placeholder': 'SecuBox', 'value': smsStatus.sender || 'SecuBox', 'style': 'width: 200px;' }), ' ', E('button', { 'class': 'btn cbi-button', 'click': function() { var sender = document.getElementById('sms-sender').value; self.handleAction('sms_config', sender); } }, _('Save')) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Test SMS')), E('div', { 'class': 'cbi-value-field' }, [ E('input', { 'type': 'tel', 'id': 'sms-test-to', 'class': 'cbi-input-text', 'placeholder': '+33612345678', 'style': 'width: 150px;' }), ' ', E('input', { 'type': 'text', 'id': 'sms-test-msg', 'class': 'cbi-input-text', 'placeholder': _('Test message'), 'style': 'width: 200px;' }), ' ', E('button', { 'class': 'btn cbi-button cbi-button-action', 'click': function() { var to = document.getElementById('sms-test-to').value; var message = document.getElementById('sms-test-msg').value; self.handleAction('sms_send', { to: to, message: message }); } }, _('Send Test SMS')) ]) ]), E('p', { 'style': 'font-size: 12px; color: #666;' }, _('To send SMS via XMPP, message sms@[domain] with format: +33612345678 Your message')), E('hr'), // VoIP Integration - Voicemail Notifications E('h4', {}, _('VoIP Integration - Voicemail Notifications')), E('p', {}, _('Receive XMPP notifications when new voicemails arrive in Asterisk PBX.')), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Status')), E('div', { 'class': 'cbi-value-field' }, [ E('span', { 'style': 'display:inline-block;padding:3px 10px;border-radius:3px;color:#fff;background:' + (voicemailStatus.enabled === '1' ? '#4CAF50' : '#9e9e9e') }, voicemailStatus.enabled === '1' ? _('Enabled') : _('Disabled')) ]) ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('AMI Host')), E('div', { 'class': 'cbi-value-field' }, voicemailStatus.ami_host || '127.0.0.1') ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('AMI Port')), E('div', { 'class': 'cbi-value-field' }, voicemailStatus.ami_port || '5038') ]), E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Notification JID')), E('div', { 'class': 'cbi-value-field' }, [ E('input', { 'type': 'text', 'id': 'voicemail-jid', 'class': 'cbi-input-text', 'placeholder': 'admin@' + hostname, 'value': voicemailStatus.notify_jid || '', 'style': 'width: 250px;' }), ' ', E('button', { 'class': 'btn cbi-button cbi-button-action', 'click': function() { var notifyJid = document.getElementById('voicemail-jid').value; self.handleAction('voicemail_config', notifyJid); } }, _('Configure')) ]) ]), E('p', { 'style': 'font-size: 12px; color: #666;' }, _('Requires VoIP container running with Asterisk AMI enabled.')), E('hr'), E('h4', {}, _('Logs')), E('div', { 'id': 'jabber-logs' }, [ E('pre', { 'style': 'background:#1e1e1e;color:#d4d4d4;padding:10px;max-height:300px;overflow:auto;font-size:12px;border-radius:4px;' }, _('Loading logs...')) ]), E('div', { 'class': 'cbi-page-actions' }, [ E('button', { 'class': 'btn cbi-button', 'click': function() { api.logs(100).then(function(res) { var logsEl = document.querySelector('#jabber-logs pre'); if (logsEl) { logsEl.textContent = res.logs || _('No logs available'); } }); } }, _('Refresh Logs')) ]) ]); }, render: function(data) { var self = this; var content = E('div', { 'id': 'jabber-content' }, this.renderContent(data)); // Load logs initially api.logs(50).then(function(res) { var logsEl = document.querySelector('#jabber-logs pre'); if (logsEl) { logsEl.textContent = res.logs || _('No logs available'); } }); // Poll for status updates poll.add(function() { return api.status().then(function(status) { // Update status badge if needed }); }, 10); return content; }, handleSaveApply: null, handleSave: null, handleReset: null });