feat(fax): add fax routing, job tracking, inbox management, and T.38/UDPTL media support

This commit is contained in:
2026-04-20 20:43:42 +00:00
parent 3c010a3b1b
commit d2c18a4ebb
27 changed files with 4247 additions and 280 deletions
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: 'siprouter',
version: '1.25.2',
version: '1.26.0',
description: 'undefined'
}
+346 -65
View File
@@ -175,36 +175,240 @@ export class SipproxyViewCalls extends DeesElement {
.call-body {
padding: 12px 16px 16px;
display: grid;
gap: 16px;
}
.legs-table {
width: 100%;
border-collapse: collapse;
font-size: 0.75rem;
margin-bottom: 12px;
.call-overview {
display: grid;
grid-template-columns: minmax(0, 1.6fr) minmax(240px, 0.9fr);
gap: 14px;
}
.legs-table th {
text-align: left;
color: #64748b;
font-weight: 500;
font-size: 0.65rem;
.call-route-card,
.call-facts-card,
.legs-section {
border-radius: 14px;
border: 1px solid rgba(51, 65, 85, 0.75);
background:
linear-gradient(180deg, rgba(15, 23, 42, 0.92) 0%, rgba(8, 15, 31, 0.88) 100%);
box-shadow: inset 0 1px 0 rgba(148, 163, 184, 0.08);
}
.call-route-card,
.call-facts-card {
padding: 14px;
}
.section-kicker {
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 6px 8px;
border-bottom: 1px solid var(--dees-color-border-default, #334155);
color: #64748b;
}
.legs-table td {
padding: 8px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
border-bottom: 1px solid var(--dees-color-border-subtle, rgba(51, 65, 85, 0.5));
vertical-align: middle;
.route-line {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 12px;
margin-top: 12px;
}
.legs-table tr:last-child td {
.route-party {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px;
border-radius: 12px;
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(71, 85, 105, 0.45);
}
.route-party.align-end {
text-align: right;
align-items: flex-end;
}
.route-party-label {
font-size: 0.64rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #64748b;
}
.route-party-value {
min-width: 0;
font-size: 0.95rem;
font-weight: 600;
color: #e2e8f0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.route-arrow {
width: 34px;
height: 34px;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
color: #93c5fd;
background: rgba(30, 41, 59, 0.8);
border: 1px solid rgba(59, 130, 246, 0.35);
}
.call-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.subtle-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 9px;
border-radius: 999px;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.03em;
color: #cbd5e1;
background: rgba(30, 41, 59, 0.9);
border: 1px solid rgba(71, 85, 105, 0.45);
}
.call-facts-card {
display: grid;
gap: 8px;
}
.fact-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
padding: 7px 0;
border-bottom: 1px solid rgba(51, 65, 85, 0.55);
}
.fact-row:last-child {
border-bottom: none;
padding-bottom: 0;
}
.fact-label {
font-size: 0.65rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #64748b;
}
.fact-value {
font-family: 'JetBrains Mono', monospace;
font-size: 0.76rem;
text-align: right;
color: #e2e8f0;
word-break: break-word;
}
.legs-section {
padding: 14px;
display: grid;
gap: 12px;
}
.legs-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.legs-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 12px;
}
.leg-card {
display: grid;
gap: 12px;
padding: 12px;
border-radius: 12px;
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(71, 85, 105, 0.4);
}
.leg-card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
}
.leg-card-badges {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.leg-card-id {
font-family: 'JetBrains Mono', monospace;
font-size: 0.64rem;
color: #64748b;
word-break: break-all;
text-align: right;
}
.leg-facts {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 12px;
}
.leg-fact {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.leg-fact-wide {
grid-column: 1 / -1;
}
.leg-fact-label {
font-size: 0.62rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #64748b;
}
.leg-fact-value {
font-family: 'JetBrains Mono', monospace;
font-size: 0.76rem;
color: #e2e8f0;
word-break: break-word;
}
.leg-actions {
display: flex;
justify-content: flex-end;
}
.no-legs {
padding: 16px;
border-radius: 12px;
border: 1px dashed rgba(71, 85, 105, 0.55);
color: #64748b;
font-size: 0.75rem;
text-align: center;
}
.card-actions {
@@ -247,6 +451,34 @@ export class SipproxyViewCalls extends DeesElement {
.empty-state-icon { font-size: 2.5rem; margin-bottom: 12px; opacity: 0.4; }
.empty-state-text { font-size: 0.9rem; font-weight: 500; }
.empty-state-sub { font-size: 0.75rem; margin-top: 4px; }
@media (max-width: 820px) {
.call-overview {
grid-template-columns: 1fr;
}
.route-line {
grid-template-columns: 1fr;
}
.route-arrow {
justify-self: center;
transform: rotate(90deg);
}
.route-party.align-end {
text-align: left;
align-items: flex-start;
}
.leg-card-top {
flex-direction: column;
}
.leg-card-id {
text-align: left;
}
}
`,
];
@@ -550,61 +782,110 @@ export class SipproxyViewCalls extends DeesElement {
</div>
<div class="call-id">${call.id}</div>
<div class="call-body">
${call.legs.length
? html`
<table class="legs-table">
<thead>
<tr>
<th>Type</th>
<th>State</th>
<th>Remote</th>
<th>Port</th>
<th>Codec</th>
<th>Pkts In</th>
<th>Pkts Out</th>
<th></th>
</tr>
</thead>
<tbody>
<div class="call-overview">
<div class="call-route-card">
<div class="section-kicker">Call Route</div>
<div class="route-line">
<div class="route-party">
<div class="route-party-label">From</div>
<div class="route-party-value">${call.callerNumber || 'Unknown caller'}</div>
</div>
<div class="route-arrow">${directionIcon(call.direction)}</div>
<div class="route-party align-end">
<div class="route-party-label">To</div>
<div class="route-party-value">${call.calleeNumber || 'System'}</div>
</div>
</div>
<div class="call-tags">
<span class="subtle-badge">${call.legs.length} ${call.legs.length === 1 ? 'leg' : 'legs'}</span>
<span class="subtle-badge">${call.providerUsed || 'system handled'}</span>
<span class="subtle-badge">started ${fmtTime(call.startedAt)}</span>
</div>
</div>
<div class="call-facts-card">
<div class="section-kicker">Session</div>
<div class="fact-row">
<span class="fact-label">State</span>
<span class="fact-value">${STATE_LABELS[call.state] || call.state}</span>
</div>
<div class="fact-row">
<span class="fact-label">Direction</span>
<span class="fact-value">${call.direction}</span>
</div>
<div class="fact-row">
<span class="fact-label">Duration</span>
<span class="fact-value">${fmtDuration(call.duration)}</span>
</div>
<div class="fact-row">
<span class="fact-label">Provider</span>
<span class="fact-value">${call.providerUsed || '--'}</span>
</div>
</div>
</div>
<div class="legs-section">
<div class="legs-header">
<div class="section-kicker">Active Legs</div>
<span class="subtle-badge">${call.legs.length}</span>
</div>
${call.legs.length
? html`
<div class="legs-grid">
${call.legs.map(
(leg) => html`
<tr>
<td>
<span class="badge" style="${legTypeBadgeStyle(leg.type)}">
${LEG_TYPE_LABELS[leg.type] || leg.type}
</span>
</td>
<td>
<span class="badge" style="${stateBadgeStyle(leg.state)}">
${leg.state}
</span>
</td>
<td>
${leg.remoteMedia || '--'}
</td>
<td>${leg.rtpPort ?? '--'}</td>
<td>
${leg.codec || '--'}${leg.transcoding ? ' (transcode)' : ''}
</td>
<td>${leg.pktReceived}</td>
<td>${leg.pktSent}</td>
<td>
<div class="leg-card">
<div class="leg-card-top">
<div class="leg-card-badges">
<span class="badge" style="${legTypeBadgeStyle(leg.type)}">
${LEG_TYPE_LABELS[leg.type] || leg.type}
</span>
<span class="badge" style="${stateBadgeStyle(leg.state)}">
${STATE_LABELS[leg.state] || leg.state}
</span>
</div>
<div class="leg-card-id">${leg.id}</div>
</div>
<div class="leg-facts">
<div class="leg-fact">
<span class="leg-fact-label">Codec</span>
<span class="leg-fact-value">${leg.codec || '--'}${leg.transcoding ? ' (transcode)' : ''}</span>
</div>
<div class="leg-fact">
<span class="leg-fact-label">RTP Port</span>
<span class="leg-fact-value">${leg.rtpPort ?? '--'}</span>
</div>
<div class="leg-fact leg-fact-wide">
<span class="leg-fact-label">Remote Media</span>
<span class="leg-fact-value">${leg.remoteMedia || '--'}</span>
</div>
<div class="leg-fact">
<span class="leg-fact-label">Packets In</span>
<span class="leg-fact-value">${leg.pktReceived}</span>
</div>
<div class="leg-fact">
<span class="leg-fact-label">Packets Out</span>
<span class="leg-fact-value">${leg.pktSent}</span>
</div>
</div>
<div class="leg-actions">
<button
class="btn btn-remove"
@click=${() => this.handleRemoveLeg(call, leg)}
>
Remove
</button>
</td>
</tr>
</div>
</div>
`,
)}
</tbody>
</table>
`
: html`<div style="color:#64748b;font-size:.75rem;font-style:italic;margin-bottom:8px;">
No legs
</div>`}
</div>
`
: html`<div class="no-legs">No legs reported yet. SIP/system legs should appear here as soon as the call is wired.</div>`}
</div>
<div class="card-actions">
<button class="btn btn-primary" @click=${() => this.handleAddParticipant(call)}>