New packages: - secubox-app-voip: Asterisk PBX in LXC container - luci-app-voip: Dashboard with extensions, trunks, click-to-call VoIP features: - voipctl CLI for container, extensions, trunks, calls, voicemail - OVH Telephony API auto-provisioning for SIP trunks - Click-to-call web interface with quick dial - RPCD backend with 15 methods Jabber VoIP integration: - Jingle VoIP support (STUN/TURN via mod_external_services) - SMS relay via OVH (messages to sms@domain) - Voicemail notifications via Asterisk AMI → XMPP - 9 new RPCD methods for VoIP features Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
151 lines
4.2 KiB
JavaScript
151 lines
4.2 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require ui';
|
|
'require uci';
|
|
'require form';
|
|
'require voip.api as api';
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
api.getStatus(),
|
|
api.getTrunkStatus(),
|
|
uci.load('voip')
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var status = data[0];
|
|
var trunkStatus = data[1];
|
|
var running = status.running === 'true';
|
|
|
|
var view = E('div', { 'class': 'cbi-map' }, [
|
|
E('h2', {}, 'SIP Trunks')
|
|
]);
|
|
|
|
// Trunk status section
|
|
var registered = trunkStatus.registered === 'true';
|
|
view.appendChild(E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, 'Trunk Status'),
|
|
E('table', { 'class': 'table' }, [
|
|
E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, 'Registration'),
|
|
E('td', { 'class': 'td' }, [
|
|
E('span', { 'class': 'badge ' + (registered ? 'success' : 'warning') },
|
|
registered ? 'Registered' : 'Not Registered')
|
|
])
|
|
])
|
|
]),
|
|
trunkStatus.status ? E('pre', {
|
|
'style': 'background: #f5f5f5; padding: 10px; font-size: 12px; overflow-x: auto;'
|
|
}, trunkStatus.status) : ''
|
|
]));
|
|
|
|
// OVH Auto-provision section
|
|
var ovhEnabled = uci.get('voip', 'ovh_telephony', 'enabled') === '1';
|
|
var ovhAppKey = uci.get('voip', 'ovh_telephony', 'app_key') || '';
|
|
|
|
view.appendChild(E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, 'OVH Telephony'),
|
|
E('p', {}, 'Auto-provision SIP trunk from OVH Telephony API'),
|
|
E('table', { 'class': 'table' }, [
|
|
E('tr', { 'class': 'tr' }, [
|
|
E('td', { 'class': 'td' }, 'API Credentials'),
|
|
E('td', { 'class': 'td' }, ovhAppKey ? 'Configured' : 'Not configured')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cbi-page-actions' }, [
|
|
E('button', {
|
|
'class': 'btn cbi-button cbi-button-positive',
|
|
'disabled': !ovhAppKey || !running,
|
|
'click': ui.createHandlerFn(this, function() {
|
|
return this.handleOvhProvision();
|
|
})
|
|
}, 'Auto-Provision OVH Trunk')
|
|
])
|
|
]));
|
|
|
|
// UCI configuration form
|
|
var m, s, o;
|
|
|
|
m = new form.Map('voip', null, null);
|
|
|
|
s = m.section(form.NamedSection, 'ovh_telephony', 'ovh', 'OVH API Settings');
|
|
s.anonymous = true;
|
|
|
|
o = s.option(form.Flag, 'enabled', 'Enable OVH Integration');
|
|
o.default = '0';
|
|
|
|
o = s.option(form.ListValue, 'endpoint', 'API Endpoint');
|
|
o.value('ovh-eu', 'OVH Europe');
|
|
o.value('ovh-ca', 'OVH Canada');
|
|
o.value('ovh-us', 'OVH US');
|
|
o.default = 'ovh-eu';
|
|
|
|
o = s.option(form.Value, 'app_key', 'Application Key');
|
|
o.password = false;
|
|
o.rmempty = true;
|
|
|
|
o = s.option(form.Value, 'app_secret', 'Application Secret');
|
|
o.password = true;
|
|
o.rmempty = true;
|
|
|
|
o = s.option(form.Value, 'consumer_key', 'Consumer Key');
|
|
o.password = true;
|
|
o.rmempty = true;
|
|
|
|
o = s.option(form.Value, 'billing_account', 'Billing Account');
|
|
o.rmempty = true;
|
|
o.placeholder = '(auto-detected)';
|
|
|
|
o = s.option(form.Value, 'service_name', 'Service Name (SIP Line)');
|
|
o.rmempty = true;
|
|
o.placeholder = '(auto-detected)';
|
|
|
|
// Manual trunk section
|
|
s = m.section(form.NamedSection, 'sip_trunk', 'trunk', 'Manual Trunk Configuration');
|
|
s.anonymous = true;
|
|
|
|
o = s.option(form.Flag, 'enabled', 'Enable Trunk');
|
|
o.default = '0';
|
|
|
|
o = s.option(form.Value, 'host', 'SIP Server');
|
|
o.placeholder = 'sip.provider.com';
|
|
|
|
o = s.option(form.Value, 'username', 'Username');
|
|
|
|
o = s.option(form.Value, 'password', 'Password');
|
|
o.password = true;
|
|
|
|
o = s.option(form.Value, 'outbound_proxy', 'Outbound Proxy');
|
|
o.rmempty = true;
|
|
|
|
o = s.option(form.Value, 'codecs', 'Codecs');
|
|
o.default = 'ulaw,alaw,g729';
|
|
|
|
return m.render().then(function(formEl) {
|
|
view.appendChild(formEl);
|
|
return view;
|
|
});
|
|
},
|
|
|
|
handleOvhProvision: function() {
|
|
ui.showModal('Provisioning OVH Trunk', [
|
|
E('p', { 'class': 'spinning' }, 'Connecting to OVH API and configuring SIP trunk...')
|
|
]);
|
|
|
|
return api.addOvhTrunk().then(function(res) {
|
|
ui.hideModal();
|
|
if (res.success) {
|
|
ui.addNotification(null, E('p', res.message || 'OVH trunk provisioned'), 'success');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'Unknown error')), 'error');
|
|
}
|
|
}).catch(function(e) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', 'Provisioning failed: ' + e.message), 'error');
|
|
});
|
|
}
|
|
});
|