initial commit — SIP B2BUA + WebRTC bridge with Rust codec engine
Full-featured SIP router with multi-provider trunking, browser softphone via WebRTC, real-time Opus/G.722/PCM transcoding in Rust, RNNoise ML noise suppression, Kokoro neural TTS announcements, and a Lit-based web dashboard with live call monitoring and REST API.
This commit is contained in:
207
ts_web/elements/sipproxy-view-overview.ts
Normal file
207
ts_web/elements/sipproxy-view-overview.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { DeesElement, customElement, html, css, cssManager, state, type TemplateResult } from '../plugins.js';
|
||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
import { appState, type IAppState } from '../state/appstate.js';
|
||||
import { viewHostCss } from './shared/index.js';
|
||||
|
||||
@customElement('sipproxy-view-overview')
|
||||
export class SipproxyViewOverview extends DeesElement {
|
||||
@state() accessor appData: IAppState = appState.getState();
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
viewHostCss,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: #64748b;
|
||||
margin: 32px 0 12px;
|
||||
}
|
||||
|
||||
.section-heading:first-of-type {
|
||||
margin-top: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.rxSubscriptions.push({
|
||||
unsubscribe: appState.subscribe((s) => {
|
||||
this.appData = s;
|
||||
}),
|
||||
} as any);
|
||||
}
|
||||
|
||||
private fmtUptime(sec: number): string {
|
||||
const d = Math.floor(sec / 86400);
|
||||
const h = Math.floor((sec % 86400) / 3600);
|
||||
const m = Math.floor((sec % 3600) / 60);
|
||||
const s = sec % 60;
|
||||
if (d > 0) return `${d}d ${h}h ${m}m`;
|
||||
return `${h}h ${String(m).padStart(2, '0')}m ${String(s).padStart(2, '0')}s`;
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const { appData } = this;
|
||||
const activeCalls = appData.calls?.filter((c) => c.state !== 'terminated') || [];
|
||||
const activeCount = activeCalls.length;
|
||||
const registeredProviders = appData.providers?.filter((p) => p.registered).length || 0;
|
||||
const connectedDevices = appData.devices?.filter((d) => d.connected).length || 0;
|
||||
const inboundCalls = activeCalls.filter((c) => c.direction === 'inbound').length;
|
||||
const outboundCalls = activeCalls.filter((c) => c.direction === 'outbound').length;
|
||||
const webrtcSessions = activeCalls.reduce(
|
||||
(sum, c) => sum + c.legs.filter((l) => l.type === 'webrtc').length,
|
||||
0,
|
||||
);
|
||||
const totalRtpPackets = activeCalls.reduce(
|
||||
(sum, c) => sum + c.legs.reduce((lsum, l) => lsum + l.pktSent + l.pktReceived, 0),
|
||||
0,
|
||||
);
|
||||
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'active-calls',
|
||||
title: 'Active Calls',
|
||||
value: activeCount,
|
||||
type: 'number',
|
||||
icon: 'lucide:phone',
|
||||
color: 'hsl(142.1 76.2% 36.3%)',
|
||||
description: activeCount === 1 ? '1 call in progress' : `${activeCount} calls in progress`,
|
||||
},
|
||||
{
|
||||
id: 'providers',
|
||||
title: 'Registered Providers',
|
||||
value: registeredProviders,
|
||||
type: 'number',
|
||||
icon: 'lucide:server',
|
||||
color: 'hsl(217.2 91.2% 59.8%)',
|
||||
description: `${appData.providers?.length || 0} configured`,
|
||||
},
|
||||
{
|
||||
id: 'devices',
|
||||
title: 'Connected Devices',
|
||||
value: connectedDevices,
|
||||
type: 'number',
|
||||
icon: 'lucide:wifi',
|
||||
color: 'hsl(270 70% 60%)',
|
||||
description: `${appData.devices?.length || 0} total`,
|
||||
},
|
||||
{
|
||||
id: 'uptime',
|
||||
title: 'Uptime',
|
||||
value: this.fmtUptime(appData.uptime),
|
||||
type: 'text',
|
||||
icon: 'lucide:clock',
|
||||
description: 'Since last restart',
|
||||
},
|
||||
{
|
||||
id: 'inbound',
|
||||
title: 'Inbound Calls',
|
||||
value: inboundCalls,
|
||||
type: 'number',
|
||||
icon: 'lucide:phone-incoming',
|
||||
description: 'Currently active',
|
||||
},
|
||||
{
|
||||
id: 'outbound',
|
||||
title: 'Outbound Calls',
|
||||
value: outboundCalls,
|
||||
type: 'number',
|
||||
icon: 'lucide:phone-outgoing',
|
||||
description: 'Currently active',
|
||||
},
|
||||
{
|
||||
id: 'webrtc',
|
||||
title: 'WebRTC Sessions',
|
||||
value: webrtcSessions,
|
||||
type: 'number',
|
||||
icon: 'lucide:globe',
|
||||
color: 'hsl(166 72% 40%)',
|
||||
description: 'Browser connections',
|
||||
},
|
||||
{
|
||||
id: 'rtp-packets',
|
||||
title: 'Total RTP Packets',
|
||||
value: totalRtpPackets,
|
||||
type: 'number',
|
||||
icon: 'lucide:radio',
|
||||
description: 'Sent + received across legs',
|
||||
},
|
||||
];
|
||||
|
||||
const allDevices = appData.devices || [];
|
||||
const onlineCount = allDevices.filter((d) => d.connected).length;
|
||||
|
||||
return html`
|
||||
<dees-statsgrid
|
||||
.tiles=${tiles}
|
||||
.minTileWidth=${220}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
|
||||
<div class="section-heading">Devices</div>
|
||||
<dees-table
|
||||
heading1="Devices"
|
||||
heading2="${onlineCount} of ${allDevices.length} online"
|
||||
dataName="devices"
|
||||
.data=${allDevices}
|
||||
.rowKey=${'id'}
|
||||
.searchable=${false}
|
||||
.columns=${[
|
||||
{
|
||||
key: 'connected',
|
||||
header: 'Status',
|
||||
renderer: (val: boolean) => {
|
||||
const on = val === true;
|
||||
return html`
|
||||
<span style="display:inline-flex;align-items:center;gap:6px">
|
||||
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;${on ? 'background:#4ade80;box-shadow:0 0 6px #4ade80' : 'background:#f87171;box-shadow:0 0 6px #f87171'}"></span>
|
||||
<span style="font-size:.7rem;font-weight:600;text-transform:uppercase;color:${on ? '#4ade80' : '#f87171'}">${on ? 'Online' : 'Offline'}</span>
|
||||
</span>
|
||||
`;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'displayName',
|
||||
header: 'Device',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
header: 'Type',
|
||||
value: (row: any) => (row.isBrowser ? 'Browser' : 'SIP Device'),
|
||||
renderer: (val: string, row: any) => {
|
||||
const isBrowser = row.isBrowser;
|
||||
const bg = isBrowser ? '#065f46' : '#1e3a5f';
|
||||
const fg = isBrowser ? '#34d399' : '#38bdf8';
|
||||
return html`<span style="display:inline-block;font-size:.6rem;padding:2px 6px;border-radius:3px;font-weight:600;text-transform:uppercase;background:${bg};color:${fg}">${val}</span>`;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'contact',
|
||||
header: 'Contact',
|
||||
renderer: (_val: any, row: any) => {
|
||||
const c = row.contact;
|
||||
const text = c ? (c.port ? `${c.address}:${c.port}` : c.address) : '--';
|
||||
return html`<span style="font-family:'JetBrains Mono',monospace;font-size:.75rem">${text}</span>`;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'aor',
|
||||
header: 'AOR',
|
||||
renderer: (val: any) =>
|
||||
html`<span style="font-family:'JetBrains Mono',monospace;font-size:.75rem">${val || '--'}</span>`,
|
||||
},
|
||||
]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user