Files
siprouter/ts_web/elements/sipproxy-view-overview.ts
Juergen Kunz f3e1c96872 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.
2026-04-09 23:03:55 +00:00

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>
`;
}
}