feat: add support for inbound trunk registration (Receive mode) for FXO gateways like Grandstream HT813 and analog Livebox integration

This commit is contained in:
sorcier committed 2026-11-03 19:07:19 +01:00
1 parent 981c650c7a
commit 8a4d7a0504
5 files changed
+285 -37

No files matched your search

+110 -5
View File
@@ -1579,14 +1579,25 @@ function renderTrunksTable() {
tbody.innerHTML = rows.map(t => {
const slug = `trunk-${slugifyTrunkName(t.name)}`;
const regInfo = regs[slug];
const regMode = t.registration_mode || (Number(t.register) === 1 ? 'send' : 'none');
const isIpAuth = t.auth_mode === 'ip';
const wantsReg = !isIpAuth && Number(t.register) === 1;
let statusBadge = '';
if (Number(t.enabled ?? 1) === 0) {
statusBadge = `<span class="px-2.5 py-0.5 rounded-full text-xs font-medium bg-anthracite-950 text-slate-400 border border-anthracite-700">Désactivé</span>`;
} else if (isIpAuth || !wantsReg) {
statusBadge = `<span class="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-cyan-500/15 text-cyan-300 border border-cyan-500/30">Peering / Sans Reg.</span>`;
} else if (regMode === 'receive') {
const contacts = appState.metrics?.asterisk?.contacts || [];
const isOnline = contacts.some(c =>
(c.extension === slug || c.extension === t.username || (t.username && c.uri && c.uri.includes(t.username)))
&& String(c.status || '').toLowerCase().includes('avail')
);
if (isOnline) {
statusBadge = `<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30"><i class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></i>Passerelle En ligne</span>`;
} else {
statusBadge = `<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-amber-500/20 text-amber-300 border border-amber-500/30">En attente passerelle (Receive)</span>`;
}
} else if (isIpAuth || regMode === 'none') {
statusBadge = `<span class="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-purple-500/15 text-purple-300 border border-purple-500/30">Peering IP direct</span>`;
} else if (regInfo && /Registered/i.test(regInfo.status)) {
statusBadge = `<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30"><i class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></i>Registered</span>`;
} else if (regInfo) {
@@ -1618,7 +1629,9 @@ function renderTrunksTable() {
</td>
<td class="py-3.5 px-4">${statusBadge}</td>
<td class="py-3.5 px-4">
<div class="font-mono text-xs text-indigo-300">${escapeHtml(t.host)}:${Number(t.port)}</div>
<div class="font-mono text-xs ${t.host ? 'text-indigo-300' : 'text-emerald-300'}">
${t.host ? escapeHtml(t.host) + ':' + Number(t.port || 5060) : 'Passerelle locale (Dynamique)'}
</div>
${t.outbound_proxy ? `<div class="text-[11px] font-mono text-slate-400 truncate max-w-[180px]">Proxy: ${escapeHtml(t.outbound_proxy)}</div>` : ''}
</td>
<td class="py-3.5 px-4">
@@ -2085,6 +2098,57 @@ function parseFreePbxConfig() {
}
}
function onTrunkRegistrationModeChange(mode) {
const form = document.getElementById('form-trunk');
if (!form) return;
const hostInput = form.elements['host'];
const hostLabel = document.getElementById('trunk-host-label');
const hostHint = document.getElementById('trunk-host-hint');
const receiveGuide = document.getElementById('trunk-receive-guide');
const regNotice = document.getElementById('trunk-reg-outbound-notice');
if (mode === 'receive') {
if (hostInput) {
hostInput.required = false;
hostInput.placeholder = 'Optionnel (vide = IP découverte dynamiquement au REGISTER du HT813)';
}
if (hostLabel) {
hostLabel.innerHTML = 'Serveur SIP / IP Passerelle <span class="text-emerald-400 font-semibold">(Optionnel en mode Réception)</span>';
}
hostHint?.classList.remove('hidden');
receiveGuide?.classList.remove('hidden');
regNotice?.classList.remove('hidden');
if (form.elements['register']) form.elements['register'].checked = false;
if (form.elements['line_support']) form.elements['line_support'].checked = false;
} else if (mode === 'none') {
if (hostInput) {
hostInput.required = true;
hostInput.placeholder = 'ex: 192.168.1.250 ou sbc.direct.net';
}
if (hostLabel) {
hostLabel.innerHTML = 'Serveur SIP / SBC / Passerelle * <span class="text-slate-400">(Host / IP)</span>';
}
hostHint?.classList.add('hidden');
receiveGuide?.classList.add('hidden');
regNotice?.classList.remove('hidden');
if (form.elements['register']) form.elements['register'].checked = false;
} else {
// mode === 'send'
if (hostInput) {
hostInput.required = true;
hostInput.placeholder = 'ex: sbc.operateur.net ou siptrunk.ovh.net';
}
if (hostLabel) {
hostLabel.innerHTML = 'Serveur SIP / SBC / Passerelle * <span class="text-slate-400">(Host / IP)</span>';
}
hostHint?.classList.add('hidden');
receiveGuide?.classList.add('hidden');
regNotice?.classList.add('hidden');
if (form.elements['register']) form.elements['register'].checked = true;
}
}
function applyTrunkArchitecture(arch) {
const form = document.getElementById('form-trunk');
if (!form) return;
@@ -2095,17 +2159,31 @@ function applyTrunkArchitecture(arch) {
port: 5060,
transport: 'udp',
auth_mode: 'userpass',
registration_mode: 'send',
register: true,
line_support: true,
media_encryption: 'no',
nat_symmetric: true,
qualify_frequency: 60
},
gateway_receive: {
label: 'Passerelle FXO / Analogique (Grandstream HT813, Livebox, ATA)',
port: 5060,
transport: 'udp',
auth_mode: 'userpass',
registration_mode: 'receive',
register: false,
line_support: false,
media_encryption: 'no',
nat_symmetric: true,
qualify_frequency: 60
},
ip_peering: {
label: 'Peering IP / SBC / Passerelle (Sans REGISTER)',
port: 5060,
transport: 'udp',
auth_mode: 'ip',
registration_mode: 'none',
register: false,
line_support: false,
media_encryption: 'no',
@@ -2117,6 +2195,7 @@ function applyTrunkArchitecture(arch) {
port: 5061,
transport: 'tls',
auth_mode: 'userpass',
registration_mode: 'send',
register: true,
line_support: true,
media_encryption: 'sdes',
@@ -2134,6 +2213,13 @@ function applyTrunkArchitecture(arch) {
form.querySelectorAll('input[name="auth_mode"]').forEach(r => { r.checked = (r.value === a.auth_mode); });
toggleTrunkAuthFields(a.auth_mode);
if (a.registration_mode) {
form.querySelectorAll('input[name="registration_mode"]').forEach(r => {
r.checked = (r.value === a.registration_mode);
});
onTrunkRegistrationModeChange(a.registration_mode);
}
form.elements['register'].checked = a.register;
form.elements['line_support'].checked = a.line_support;
form.elements['media_encryption'].value = a.media_encryption;
@@ -2152,11 +2238,18 @@ function openTrunkModal(trunk = null) {
form.elements['id'].value = trunk ? trunk.id : '';
form.elements['name'].value = trunk ? trunk.name : '';
form.elements['host'].value = trunk ? trunk.host : '';
form.elements['host'].value = trunk ? (trunk.host ?? '') : '';
form.elements['port'].value = trunk ? (trunk.port ?? 5060) : 5060;
form.elements['outbound_proxy'].value = trunk ? (trunk.outbound_proxy ?? '') : '';
form.elements['transport'].value = trunk ? (trunk.transport ?? 'udp') : 'udp';
const regMode = trunk ? (trunk.registration_mode || (Number(trunk.register) === 1 ? 'send' : 'none')) : 'send';
form.querySelectorAll('input[name="registration_mode"]').forEach(r => {
r.checked = (r.value === regMode);
});
onTrunkRegistrationModeChange(regMode);
form.elements['transport'].value = trunk ? (trunk.transport ?? 'udp') : 'udp';
const authMode = trunk ? (trunk.auth_mode || 'userpass') : 'userpass';
form.querySelectorAll('input[name="auth_mode"]').forEach(r => {
r.checked = (r.value === authMode);
@@ -2168,6 +2261,17 @@ function openTrunkModal(trunk = null) {
form.elements['secret'].value = trunk ? (trunk.secret ?? '') : '';
form.elements['enabled'].checked = trunk ? Number(trunk.enabled ?? 1) === 1 : true;
const uInput = form.elements['username'];
const updateGuide = () => {
const val = uInput.value.trim() || 'Même valeur que Username';
const gUser = document.getElementById('ht813-guide-user');
const gAuth = document.getElementById('ht813-guide-auth');
if (gUser) gUser.textContent = val;
if (gAuth) gAuth.textContent = val;
};
uInput.oninput = updateGuide;
updateGuide();
// Onglet 2 : Registration & Identité SIP
form.elements['register'].checked = trunk ? Number(trunk.register ?? 1) === 1 : true;
form.elements['line_support'].checked = trunk ? Number(trunk.line_support ?? 1) === 1 : true;
@@ -3583,6 +3687,7 @@ document.addEventListener('DOMContentLoaded', () => {
const fd = new FormData(form);
const payload = Object.fromEntries(fd.entries());
payload.registration_mode = form.querySelector('input[name="registration_mode"]:checked')?.value || 'send';
payload.enabled = form.elements['enabled'].checked ? 1 : 0;
payload.register = form.elements['register'].checked ? 1 : 0;
payload.line_support = form.elements['line_support'].checked ? 1 : 0;