fix(proxy-engine): improve inbound SIP routing diagnostics and enrich leg media state reporting

This commit is contained in:
2026-04-14 20:19:34 +00:00
parent 0d82a626b5
commit 88768f0586
46 changed files with 555689 additions and 107 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: 'siprouter',
version: '1.25.1',
version: '1.25.2',
description: 'undefined'
}

View File

@@ -32,8 +32,32 @@ const LEG_TYPE_LABELS: Record<string, string> = {
'sip-device': 'SIP Device',
'sip-provider': 'SIP Provider',
'webrtc': 'WebRTC',
'tool': 'Tool',
};
function renderHistoryLegs(legs: ICallHistoryEntry['legs']): TemplateResult {
if (!legs.length) {
return html`<span style="color:#64748b">-</span>`;
}
return html`
<div style="display:flex;flex-direction:column;gap:6px;font-size:.72rem;line-height:1.35;">
${legs.map(
(leg) => html`
<div>
<span class="badge" style="${legTypeBadgeStyle(leg.type)}">${LEG_TYPE_LABELS[leg.type] || leg.type}</span>
<span style="margin-left:6px;font-family:'JetBrains Mono',monospace;">${leg.codec || '--'}</span>
<span style="margin-left:6px;color:#94a3b8;">${STATE_LABELS[leg.state] || leg.state}</span>
${leg.remoteMedia
? html`<span style="display:block;color:#64748b;font-family:'JetBrains Mono',monospace;">${leg.remoteMedia}</span>`
: ''}
</div>
`,
)}
</div>
`;
}
function directionIcon(dir: string): string {
if (dir === 'inbound') return '\u2199';
if (dir === 'outbound') return '\u2197';
@@ -226,8 +250,8 @@ export class SipproxyViewCalls extends DeesElement {
`,
];
connectedCallback() {
super.connectedCallback();
async connectedCallback(): Promise<void> {
await super.connectedCallback();
this.rxSubscriptions.push({
unsubscribe: appState.subscribe((s) => {
this.appData = s;
@@ -490,6 +514,11 @@ export class SipproxyViewCalls extends DeesElement {
renderer: (val: number) =>
html`<span style="font-family:'JetBrains Mono',monospace;font-size:.75rem">${fmtDuration(val)}</span>`,
},
{
key: 'legs',
header: 'Legs',
renderer: (val: ICallHistoryEntry['legs']) => renderHistoryLegs(val),
},
];
}
@@ -551,9 +580,7 @@ export class SipproxyViewCalls extends DeesElement {
</span>
</td>
<td>
${leg.remoteMedia
? `${leg.remoteMedia.address}:${leg.remoteMedia.port}`
: '--'}
${leg.remoteMedia || '--'}
</td>
<td>${leg.rtpPort ?? '--'}</td>
<td>

View File

@@ -18,6 +18,12 @@ interface IVoicemailMessage {
heard: boolean;
}
interface IVoiceboxRow {
id: string;
unheardCount: number;
selected: boolean;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@@ -61,19 +67,6 @@ export class SipproxyViewVoicemail extends DeesElement {
.view-section {
margin-bottom: 24px;
}
.box-selector {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.box-selector label {
font-size: 0.85rem;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.audio-player {
display: flex;
align-items: center;
@@ -135,10 +128,11 @@ export class SipproxyViewVoicemail extends DeesElement {
const cfg = await appState.apiGetConfig();
const boxes: { id: string }[] = cfg.voiceboxes || [];
this.voiceboxIds = boxes.map((b) => b.id);
if (this.voiceboxIds.length > 0 && !this.selectedBoxId) {
this.selectedBoxId = this.voiceboxIds[0];
await this.loadMessages();
}
const nextSelectedBoxId = this.voiceboxIds.includes(this.selectedBoxId)
? this.selectedBoxId
: (this.voiceboxIds[0] || '');
this.selectedBoxId = nextSelectedBoxId;
await this.loadMessages();
} catch {
// Config unavailable.
}
@@ -161,11 +155,22 @@ export class SipproxyViewVoicemail extends DeesElement {
}
private async selectBox(boxId: string) {
if (boxId === this.selectedBoxId) {
return;
}
this.selectedBoxId = boxId;
this.stopAudio();
await this.loadMessages();
}
private getVoiceboxRows(): IVoiceboxRow[] {
return this.voiceboxIds.map((id) => ({
id,
unheardCount: this.appData.voicemailCounts[id] || 0,
selected: id === this.selectedBoxId,
}));
}
// ---- audio playback ------------------------------------------------------
private playMessage(msg: IVoicemailMessage) {
@@ -341,6 +346,43 @@ export class SipproxyViewVoicemail extends DeesElement {
];
}
private getVoiceboxColumns() {
return [
{
key: 'id',
header: 'Voicebox',
sortable: true,
renderer: (val: string, row: IVoiceboxRow) => html`
<div style="display:flex;align-items:center;gap:10px;">
<span style="font-family:'JetBrains Mono',monospace;font-size:.85rem;">${val}</span>
${row.selected ? html`
<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:.7rem;font-weight:600;text-transform:uppercase;background:#1e3a5f;color:#60a5fa">Viewing</span>
` : ''}
</div>
`,
},
{
key: 'unheardCount',
header: 'Unheard',
sortable: true,
renderer: (val: number) => {
const hasUnheard = val > 0;
return html`
<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:.75rem;font-weight:600;background:${hasUnheard ? '#422006' : '#1f2937'};color:${hasUnheard ? '#f59e0b' : '#94a3b8'}">${val}</span>
`;
},
},
{
key: 'selected',
header: 'Status',
value: (row: IVoiceboxRow) => (row.selected ? 'Open' : 'Available'),
renderer: (val: string, row: IVoiceboxRow) => html`
<span style="color:${row.selected ? '#60a5fa' : '#94a3b8'};font-size:.8rem;">${val}</span>
`,
},
];
}
// ---- table actions -------------------------------------------------------
private getDataActions() {
@@ -390,21 +432,43 @@ export class SipproxyViewVoicemail extends DeesElement {
];
}
private getVoiceboxActions() {
return [
{
name: 'View Messages',
iconName: 'lucide:folder-open',
type: ['inRow'] as any,
actionFunc: async ({ item }: { item: IVoiceboxRow }) => {
await this.selectBox(item.id);
},
},
{
name: 'Refresh Boxes',
iconName: 'lucide:refreshCw',
type: ['header'] as any,
actionFunc: async () => {
await this.loadVoiceboxes();
deesCatalog.DeesToast.success('Voiceboxes refreshed');
},
},
];
}
// ---- render --------------------------------------------------------------
public render(): TemplateResult {
return html`
${this.voiceboxIds.length > 1 ? html`
<div class="box-selector">
<label>Voicebox</label>
<dees-input-dropdown
.key=${'voicebox'}
.selectedOption=${{ option: this.selectedBoxId, key: this.selectedBoxId }}
.options=${this.voiceboxIds.map((id) => ({ option: id, key: id }))}
@selectedOption=${(e: CustomEvent) => { this.selectBox(e.detail.key); }}
></dees-input-dropdown>
</div>
` : ''}
<div class="view-section">
<dees-table
heading1="Voiceboxes"
heading2="${this.voiceboxIds.length} configured"
dataName="voiceboxes"
.data=${this.getVoiceboxRows()}
.rowKey=${'id'}
.columns=${this.getVoiceboxColumns()}
.dataActions=${this.getVoiceboxActions()}
></dees-table>
</div>
<div class="view-section">
<dees-statsgrid