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

@@ -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