feat(routing): require explicit inbound DID routes and normalize SIP identities for provider-based number matching

This commit is contained in:
2026-04-14 16:35:54 +00:00
parent cff70ab179
commit 06c86d7e81
29 changed files with 1476 additions and 549 deletions

View File

@@ -20,6 +20,9 @@ interface ISipRoute {
action: {
targets?: string[];
ringBrowsers?: boolean;
voicemailBox?: string;
ivrMenuId?: string;
noAnswerTimeout?: number;
provider?: string;
failoverProviders?: string[];
stripPrefix?: string;
@@ -40,10 +43,10 @@ export class SipproxyViewRoutes extends DeesElement {
`,
];
connectedCallback() {
super.connectedCallback();
appState.subscribe((_k, s) => { this.appData = s; });
this.loadConfig();
async connectedCallback(): Promise<void> {
await super.connectedCallback();
appState.subscribe((s) => { this.appData = s; });
await this.loadConfig();
}
private async loadConfig() {
@@ -157,9 +160,15 @@ export class SipproxyViewRoutes extends DeesElement {
return html`<span style="font-family:'JetBrains Mono',monospace;font-size:.82rem">${parts.join(' ')}</span>`;
} else {
const parts: string[] = [];
if (a.targets?.length) parts.push(`ring: ${a.targets.join(', ')}`);
else parts.push('ring: all devices');
if (a.ringBrowsers) parts.push('+ browsers');
if (a.ivrMenuId) {
parts.push(`ivr: ${a.ivrMenuId}`);
} else {
if (a.targets?.length) parts.push(`ring: ${a.targets.join(', ')}`);
else parts.push('ring: all devices');
if (a.ringBrowsers) parts.push('+ browsers');
}
if (a.voicemailBox) parts.push(`vm: ${a.voicemailBox}`);
if (a.noAnswerTimeout) parts.push(`timeout: ${a.noAnswerTimeout}s`);
return html`<span style="font-family:'JetBrains Mono',monospace;font-size:.82rem">${parts.join(' ')}</span>`;
}
},
@@ -231,6 +240,8 @@ export class SipproxyViewRoutes extends DeesElement {
const cfg = this.config;
const providers = cfg?.providers || [];
const devices = cfg?.devices || [];
const voiceboxes = cfg?.voiceboxes || [];
const ivrMenus = cfg?.ivr?.menus || [];
const formData: ISipRoute = existing
? JSON.parse(JSON.stringify(existing))
@@ -284,7 +295,7 @@ export class SipproxyViewRoutes extends DeesElement {
<dees-input-text
.key=${'numberPattern'}
.label=${'Number Pattern'}
.description=${'Exact, prefix with * (e.g. +49*), or /regex/'}
.description=${'Inbound: DID/called number. Outbound: dialed number. Exact, prefix with * (e.g. +49*), or /regex/'}
.value=${formData.match.numberPattern || ''}
@input=${(e: Event) => { formData.match.numberPattern = (e.target as any).value || undefined; }}
></dees-input-text>
@@ -328,7 +339,7 @@ export class SipproxyViewRoutes extends DeesElement {
<dees-input-text
.key=${'targets'}
.label=${'Ring Devices (comma-separated IDs)'}
.description=${'Leave empty to ring all devices'}
.description=${'Leave empty to ring all devices for matched inbound numbers'}
.value=${(formData.action.targets || []).join(', ')}
@input=${(e: Event) => {
const v = (e.target as any).value.trim();
@@ -342,6 +353,30 @@ export class SipproxyViewRoutes extends DeesElement {
@newValue=${(e: CustomEvent) => { formData.action.ringBrowsers = e.detail; }}
></dees-input-checkbox>
<dees-input-dropdown
.key=${'voicemailBox'} .label=${'Voicemail Box (inbound fallback)'}
.selectedOption=${formData.action.voicemailBox
? { option: formData.action.voicemailBox, key: formData.action.voicemailBox }
: { option: '(none)', key: '' }}
.options=${[
{ option: '(none)', key: '' },
...voiceboxes.map((vb: any) => ({ option: vb.id, key: vb.id })),
]}
@selectedOption=${(e: CustomEvent) => { formData.action.voicemailBox = e.detail.key || undefined; }}
></dees-input-dropdown>
<dees-input-dropdown
.key=${'ivrMenuId'} .label=${'IVR Menu (inbound)'}
.selectedOption=${formData.action.ivrMenuId
? { option: formData.action.ivrMenuId, key: formData.action.ivrMenuId }
: { option: '(none)', key: '' }}
.options=${[
{ option: '(none)', key: '' },
...ivrMenus.map((menu: any) => ({ option: menu.name || menu.id, key: menu.id })),
]}
@selectedOption=${(e: CustomEvent) => { formData.action.ivrMenuId = e.detail.key || undefined; }}
></dees-input-dropdown>
<dees-input-text
.key=${'stripPrefix'} .label=${'Strip Prefix (outbound)'}
.value=${formData.action.stripPrefix || ''}
@@ -380,6 +415,9 @@ export class SipproxyViewRoutes extends DeesElement {
if (!formData.action.prependPrefix) delete formData.action.prependPrefix;
if (!formData.action.targets?.length) delete formData.action.targets;
if (!formData.action.ringBrowsers) delete formData.action.ringBrowsers;
if (!formData.action.voicemailBox) delete formData.action.voicemailBox;
if (!formData.action.ivrMenuId) delete formData.action.ivrMenuId;
if (!formData.action.noAnswerTimeout) delete formData.action.noAnswerTimeout;
const currentRoutes = [...(cfg?.routing?.routes || [])];
const idx = currentRoutes.findIndex((r: any) => r.id === formData.id);