secubox-openwrt/package/secubox/luci-app-voip/htdocs/luci-static/resources/view/voip/extensions.js
CyberMind-FR 4ca46b61e2 feat(voip): Add VoIP packages with OVH provisioning and Jabber integration
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>
2026-02-19 09:22:06 +01:00

185 lines
4.7 KiB
JavaScript

'use strict';
'require view';
'require ui';
'require dom';
'require voip.api as api';
return view.extend({
load: function() {
return Promise.all([
api.getStatus(),
api.listExtensions()
]);
},
parseExtensions: function(extString) {
if (!extString) return [];
return extString.split(',').filter(function(e) { return e; }).map(function(entry) {
var parts = entry.split(':');
return {
ext: parts[0],
name: parts[1] || ''
};
});
},
renderExtensionTable: function(extensions) {
var self = this;
if (!extensions || extensions.length === 0) {
return E('p', { 'class': 'alert-message' }, 'No extensions configured');
}
var rows = extensions.map(function(ext) {
return E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, ext.ext),
E('td', { 'class': 'td' }, ext.name),
E('td', { 'class': 'td' }, [
E('button', {
'class': 'btn cbi-button cbi-button-remove',
'click': ui.createHandlerFn(self, function() {
return self.handleDelete(ext.ext);
})
}, 'Delete')
])
]);
});
return E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, 'Extension'),
E('th', { 'class': 'th' }, 'Name'),
E('th', { 'class': 'th' }, 'Actions')
])
].concat(rows));
},
renderAddForm: function() {
var self = this;
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Add Extension'),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Extension Number'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'new-ext',
'placeholder': '100',
'class': 'cbi-input-text'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Display Name'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'new-name',
'placeholder': 'John Doe',
'class': 'cbi-input-text'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Password'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'password',
'id': 'new-password',
'placeholder': '(auto-generated if empty)',
'class': 'cbi-input-text'
})
])
]),
E('div', { 'class': 'cbi-page-actions' }, [
E('button', {
'class': 'btn cbi-button cbi-button-positive',
'click': ui.createHandlerFn(this, function() {
return this.handleAdd();
})
}, 'Add Extension')
])
]);
},
handleAdd: function() {
var ext = document.getElementById('new-ext').value.trim();
var name = document.getElementById('new-name').value.trim();
var password = document.getElementById('new-password').value;
if (!ext) {
ui.addNotification(null, E('p', 'Extension number is required'), 'error');
return Promise.resolve();
}
if (!name) {
ui.addNotification(null, E('p', 'Display name is required'), 'error');
return Promise.resolve();
}
if (!/^\d+$/.test(ext)) {
ui.addNotification(null, E('p', 'Extension must be numeric'), 'error');
return Promise.resolve();
}
return api.addExtension(ext, name, password).then(function(res) {
if (res.success) {
var msg = 'Extension ' + ext + ' created';
if (res.password) {
msg += '. Password: ' + res.password;
}
ui.addNotification(null, E('p', msg), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
},
handleDelete: function(ext) {
if (!confirm('Delete extension ' + ext + '?')) {
return Promise.resolve();
}
return api.deleteExtension(ext).then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', 'Extension deleted'), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'Unknown error')), 'error');
}
});
},
render: function(data) {
var status = data[0];
var extData = data[1];
var running = status.running === 'true';
if (!running) {
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'Extensions'),
E('p', { 'class': 'alert-message warning' },
'VoIP service is not running. Start it from the Overview page.')
]);
}
var extensions = this.parseExtensions(extData.extensions);
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'Extensions'),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Configured Extensions'),
E('div', { 'id': 'ext-table' }, this.renderExtensionTable(extensions))
]),
this.renderAddForm()
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});