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.
208 lines
6.6 KiB
TypeScript
208 lines
6.6 KiB
TypeScript
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>
|
|
`;
|
|
}
|
|
}
|