diff --git a/changelog.md b/changelog.md index 64f3e6c..b6dbc85 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-04-13 - 13.14.0 - feat(network) +add bandwidth-ranked IP and domain activity metrics to network monitoring + +- Expose top IPs by bandwidth and aggregated domain activity from route metrics. +- Replace estimated per-connection values with real per-IP throughput data in ops handlers and stats responses. +- Update the network UI to show bandwidth-ranked IPs and domain activity while removing the recent request table. + ## 2026-04-13 - 13.13.0 - feat(dns) add domain migration between dcrouter and provider-managed DNS with unified ACME managed-domain handling diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index cc0c19c..8ea8e75 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '13.13.0', + version: '13.14.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/monitoring/classes.metricsmanager.ts b/ts/monitoring/classes.metricsmanager.ts index b257d99..ac3e30c 100644 --- a/ts/monitoring/classes.metricsmanager.ts +++ b/ts/monitoring/classes.metricsmanager.ts @@ -553,12 +553,14 @@ export class MetricsManager { connectionsByIP: new Map(), throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, topIPs: [] as Array<{ ip: string; count: number }>, + topIPsByBandwidth: [] as Array<{ ip: string; count: number; bwIn: number; bwOut: number }>, totalDataTransferred: { bytesIn: 0, bytesOut: 0 }, throughputHistory: [] as Array<{ timestamp: number; in: number; out: number }>, throughputByIP: new Map(), requestsPerSecond: 0, requestsTotal: 0, backends: [] as Array, + domainActivity: [] as Array<{ domain: string; bytesInPerSecond: number; bytesOutPerSecond: number; activeConnections: number; routeCount: number }>, }; } @@ -572,7 +574,7 @@ export class MetricsManager { bytesOutPerSecond: instantThroughput.out }; - // Get top IPs + // Get top IPs by connection count const topIPs = proxyMetrics.connections.topIPs(10); // Get total data transferred @@ -699,10 +701,83 @@ export class MetricsManager { } } + // Build top 10 IPs by bandwidth (sorted by total throughput desc) + const allIPData = new Map(); + for (const [ip, count] of connectionsByIP) { + allIPData.set(ip, { count, bwIn: 0, bwOut: 0 }); + } + for (const [ip, tp] of throughputByIP) { + const existing = allIPData.get(ip); + if (existing) { + existing.bwIn = tp.in; + existing.bwOut = tp.out; + } else { + allIPData.set(ip, { count: 0, bwIn: tp.in, bwOut: tp.out }); + } + } + const topIPsByBandwidth = Array.from(allIPData.entries()) + .sort((a, b) => (b[1].bwIn + b[1].bwOut) - (a[1].bwIn + a[1].bwOut)) + .slice(0, 10) + .map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut })); + + // Build domain activity from per-route metrics + const connectionsByRoute = proxyMetrics.connections.byRoute(); + const throughputByRoute = proxyMetrics.throughput.byRoute(); + + // Map route name → primary domain using dcrouter's route configs + const routeToDomain = new Map(); + if (this.dcRouter.smartProxy) { + for (const route of this.dcRouter.smartProxy.routeManager.getRoutes()) { + if (!route.name || !route.match.domains) continue; + const domains = Array.isArray(route.match.domains) + ? route.match.domains + : [route.match.domains]; + if (domains.length > 0) { + routeToDomain.set(route.name, domains[0]); + } + } + } + + // Aggregate metrics by domain + const domainAgg = new Map(); + for (const [routeName, activeConns] of connectionsByRoute) { + const domain = routeToDomain.get(routeName) || routeName; + const tp = throughputByRoute.get(routeName) || { in: 0, out: 0 }; + const existing = domainAgg.get(domain); + if (existing) { + existing.activeConnections += activeConns; + existing.bytesInPerSec += tp.in; + existing.bytesOutPerSec += tp.out; + existing.routeCount++; + } else { + domainAgg.set(domain, { + activeConnections: activeConns, + bytesInPerSec: tp.in, + bytesOutPerSec: tp.out, + routeCount: 1, + }); + } + } + const domainActivity = Array.from(domainAgg.entries()) + .map(([domain, data]) => ({ + domain, + bytesInPerSecond: data.bytesInPerSec, + bytesOutPerSecond: data.bytesOutPerSec, + activeConnections: data.activeConnections, + routeCount: data.routeCount, + })) + .sort((a, b) => (b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond)); + return { connectionsByIP, throughputRate, topIPs, + topIPsByBandwidth, totalDataTransferred, throughputHistory, throughputByIP, @@ -711,6 +786,7 @@ export class MetricsManager { backends, frontendProtocols, backendProtocols, + domainActivity, }; }, 1000); // 1s cache — matches typical dashboard poll interval } diff --git a/ts/opsserver/handlers/security.handler.ts b/ts/opsserver/handlers/security.handler.ts index aaa8cdb..f48b2bf 100644 --- a/ts/opsserver/handlers/security.handler.ts +++ b/ts/opsserver/handlers/security.handler.ts @@ -51,8 +51,8 @@ export class SecurityHandler { startTime: conn.startTime, protocol: conn.type === 'http' ? 'https' : conn.type as any, state: conn.status as any, - bytesReceived: Math.floor(conn.bytesTransferred / 2), - bytesSent: Math.floor(conn.bytesTransferred / 2), + bytesReceived: (conn as any)._throughputIn || 0, + bytesSent: (conn as any)._throughputOut || 0, })); const summary = { @@ -96,9 +96,11 @@ export class SecurityHandler { connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })), throughputRate: networkStats.throughputRate, topIPs: networkStats.topIPs, + topIPsByBandwidth: networkStats.topIPsByBandwidth, totalDataTransferred: networkStats.totalDataTransferred, throughputHistory: networkStats.throughputHistory || [], throughputByIP, + domainActivity: networkStats.domainActivity || [], requestsPerSecond: networkStats.requestsPerSecond || 0, requestsTotal: networkStats.requestsTotal || 0, backends: networkStats.backends || [], @@ -110,9 +112,11 @@ export class SecurityHandler { connectionsByIP: [], throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, topIPs: [], + topIPsByBandwidth: [], totalDataTransferred: { bytesIn: 0, bytesOut: 0 }, throughputHistory: [], throughputByIP: [], + domainActivity: [], requestsPerSecond: 0, requestsTotal: 0, backends: [], @@ -251,31 +255,31 @@ export class SecurityHandler { const connectionInfo = await this.opsServerRef.dcRouterRef.metricsManager.getConnectionInfo(); const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats(); - // Use IP-based connection data from the new metrics API + // One aggregate row per IP with real throughput data if (networkStats.connectionsByIP && networkStats.connectionsByIP.size > 0) { let connIndex = 0; const publicIp = this.opsServerRef.dcRouterRef.options.publicIp || 'server'; - + for (const [ip, count] of networkStats.connectionsByIP) { - // Create a connection entry for each active IP connection - for (let i = 0; i < Math.min(count, 5); i++) { // Limit to 5 connections per IP for UI performance - connections.push({ - id: `conn-${connIndex++}`, - type: 'http', - source: { - ip: ip, - port: Math.floor(Math.random() * 50000) + 10000, // High port range - }, - destination: { - ip: publicIp, - port: 443, - service: 'proxy', - }, - startTime: Date.now() - Math.floor(Math.random() * 3600000), // Within last hour - bytesTransferred: Math.floor(networkStats.totalDataTransferred.bytesIn / networkStats.connectionsByIP.size), - status: 'active', - }); - } + const tp = networkStats.throughputByIP?.get(ip); + connections.push({ + id: `ip-${connIndex++}`, + type: 'http', + source: { + ip: ip, + port: 0, + }, + destination: { + ip: publicIp, + port: 443, + service: 'proxy', + }, + startTime: 0, + bytesTransferred: count, // Store connection count here + status: 'active', + // Attach real throughput for the handler mapping + ...(tp ? { _throughputIn: tp.in, _throughputOut: tp.out } : {}), + } as any); } } else if (connectionInfo.length > 0) { // Fallback to route-based connection info if no IP data available diff --git a/ts/opsserver/handlers/stats.handler.ts b/ts/opsserver/handlers/stats.handler.ts index 052eeb9..bdaa524 100644 --- a/ts/opsserver/handlers/stats.handler.ts +++ b/ts/opsserver/handlers/stats.handler.ts @@ -291,6 +291,20 @@ export class StatsHandler { } } + // Build connectionDetails from real per-IP data + const connectionDetails: interfaces.data.IConnectionDetails[] = []; + for (const [ip, count] of stats.connectionsByIP) { + const tp = stats.throughputByIP?.get(ip); + connectionDetails.push({ + remoteAddress: ip, + protocol: 'https', + state: 'connected', + startTime: 0, + bytesIn: tp?.in || 0, + bytesOut: tp?.out || 0, + }); + } + metrics.network = { totalBandwidth: { in: stats.throughputRate.bytesInPerSecond, @@ -301,12 +315,18 @@ export class StatsHandler { out: stats.totalDataTransferred.bytesOut, }, activeConnections: serverStats.activeConnections, - connectionDetails: [], + connectionDetails, topEndpoints: stats.topIPs.map(ip => ({ endpoint: ip.ip, - requests: ip.count, + connections: ip.count, bandwidth: ipBandwidth.get(ip.ip) || { in: 0, out: 0 }, })), + topEndpointsByBandwidth: stats.topIPsByBandwidth.map(ip => ({ + endpoint: ip.ip, + connections: ip.count, + bandwidth: { in: ip.bwIn, out: ip.bwOut }, + })), + domainActivity: stats.domainActivity || [], throughputHistory: stats.throughputHistory || [], requestsPerSecond: stats.requestsPerSecond || 0, requestsTotal: stats.requestsTotal || 0, diff --git a/ts_interfaces/data/stats.ts b/ts_interfaces/data/stats.ts index 469ecf4..b09a998 100644 --- a/ts_interfaces/data/stats.ts +++ b/ts_interfaces/data/stats.ts @@ -143,6 +143,14 @@ export interface IHealthStatus { version?: string; } +export interface IDomainActivity { + domain: string; + bytesInPerSecond: number; + bytesOutPerSecond: number; + activeConnections: number; + routeCount: number; +} + export interface INetworkMetrics { totalBandwidth: { in: number; @@ -156,12 +164,21 @@ export interface INetworkMetrics { connectionDetails: IConnectionDetails[]; topEndpoints: Array<{ endpoint: string; - requests: number; + connections: number; bandwidth: { in: number; out: number; }; }>; + topEndpointsByBandwidth: Array<{ + endpoint: string; + connections: number; + bandwidth: { + in: number; + out: number; + }; + }>; + domainActivity: IDomainActivity[]; throughputHistory?: Array<{ timestamp: number; in: number; out: number }>; requestsPerSecond?: number; requestsTotal?: number; diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index cc0c19c..8ea8e75 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '13.13.0', + version: '13.14.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index 3a359be..c89ed73 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -52,7 +52,9 @@ export interface INetworkState { throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number }; totalBytes: { in: number; out: number }; topIPs: Array<{ ip: string; count: number }>; + topIPsByBandwidth: Array<{ ip: string; count: number; bwIn: number; bwOut: number }>; throughputByIP: Array<{ ip: string; in: number; out: number }>; + domainActivity: interfaces.data.IDomainActivity[]; throughputHistory: Array<{ timestamp: number; in: number; out: number }>; requestsPerSecond: number; requestsTotal: number; @@ -160,7 +162,9 @@ export const networkStatePart = await appState.getStatePart( throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, totalBytes: { in: 0, out: 0 }, topIPs: [], + topIPsByBandwidth: [], throughputByIP: [], + domainActivity: [], throughputHistory: [], requestsPerSecond: 0, requestsTotal: 0, @@ -552,7 +556,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat ? { in: networkStatsResponse.totalDataTransferred.bytesIn, out: networkStatsResponse.totalDataTransferred.bytesOut } : { in: 0, out: 0 }, topIPs: networkStatsResponse.topIPs || [], + topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [], throughputByIP: networkStatsResponse.throughputByIP || [], + domainActivity: networkStatsResponse.domainActivity || [], throughputHistory: networkStatsResponse.throughputHistory || [], requestsPerSecond: networkStatsResponse.requestsPerSecond || 0, requestsTotal: networkStatsResponse.requestsTotal || 0, @@ -2649,67 +2655,52 @@ async function dispatchCombinedRefreshActionInner() { if (combinedResponse.metrics.network && currentView === 'network') { const network = combinedResponse.metrics.network; const connectionsByIP: { [ip: string]: number } = {}; - - // Convert connection details to IP counts + + // Build connectionsByIP from connectionDetails (now populated with real per-IP data) network.connectionDetails.forEach(conn => { connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + 1; }); - // Fetch detailed connections for the network view - try { - const connectionsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< - interfaces.requests.IReq_GetActiveConnections - >('/typedrequest', 'getActiveConnections'); - - const connectionsResponse = await connectionsRequest.fire({ - identity: context.identity, - }); + // Build connections from connectionDetails (real per-IP aggregates) + const connections: interfaces.data.IConnectionInfo[] = network.connectionDetails.map((conn, i) => ({ + id: `ip-${conn.remoteAddress}`, + remoteAddress: conn.remoteAddress, + localAddress: 'server', + startTime: conn.startTime, + protocol: conn.protocol as any, + state: conn.state as any, + bytesReceived: conn.bytesIn, + bytesSent: conn.bytesOut, + })); - networkStatePart.setState({ - ...networkStatePart.getState()!, - connections: connectionsResponse.connections, - connectionsByIP, - throughputRate: { - bytesInPerSecond: network.totalBandwidth.in, - bytesOutPerSecond: network.totalBandwidth.out - }, - totalBytes: network.totalBytes || { in: 0, out: 0 }, - topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })), - throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })), - throughputHistory: network.throughputHistory || [], - requestsPerSecond: network.requestsPerSecond || 0, - requestsTotal: network.requestsTotal || 0, - backends: network.backends || [], - frontendProtocols: network.frontendProtocols || null, - backendProtocols: network.backendProtocols || null, - lastUpdated: Date.now(), - isLoading: false, - error: null, - }); - } catch (error: unknown) { - console.error('Failed to fetch connections:', error); - networkStatePart.setState({ - ...networkStatePart.getState()!, - connections: [], - connectionsByIP, - throughputRate: { - bytesInPerSecond: network.totalBandwidth.in, - bytesOutPerSecond: network.totalBandwidth.out - }, - totalBytes: network.totalBytes || { in: 0, out: 0 }, - topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })), - throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })), - throughputHistory: network.throughputHistory || [], - requestsPerSecond: network.requestsPerSecond || 0, - requestsTotal: network.requestsTotal || 0, - backends: network.backends || [], - frontendProtocols: network.frontendProtocols || null, - backendProtocols: network.backendProtocols || null, - lastUpdated: Date.now(), - isLoading: false, - error: null, - }); - } + networkStatePart.setState({ + ...networkStatePart.getState()!, + connections, + connectionsByIP, + throughputRate: { + bytesInPerSecond: network.totalBandwidth.in, + bytesOutPerSecond: network.totalBandwidth.out, + }, + totalBytes: network.totalBytes || { in: 0, out: 0 }, + topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.connections })), + topIPsByBandwidth: (network.topEndpointsByBandwidth || []).map(e => ({ + ip: e.endpoint, + count: e.connections, + bwIn: e.bandwidth?.in || 0, + bwOut: e.bandwidth?.out || 0, + })), + throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })), + domainActivity: network.domainActivity || [], + throughputHistory: network.throughputHistory || [], + requestsPerSecond: network.requestsPerSecond || 0, + requestsTotal: network.requestsTotal || 0, + backends: network.backends || [], + frontendProtocols: network.frontendProtocols || null, + backendProtocols: network.backendProtocols || null, + lastUpdated: Date.now(), + isLoading: false, + error: null, + }); } // Refresh certificate data if on Domains > Certificates subview diff --git a/ts_web/elements/domains/ops-view-domains.ts b/ts_web/elements/domains/ops-view-domains.ts index 8644fb2..d90fa14 100644 --- a/ts_web/elements/domains/ops-view-domains.ts +++ b/ts_web/elements/domains/ops-view-domains.ts @@ -323,16 +323,14 @@ export class OpsViewDomains extends DeesElement { // Build target options based on current source const targetOptions: { option: string; key: string }[] = []; - if (domain.source === 'provider') { - targetOptions.push({ option: 'DcRouter (authoritative)', key: 'dcrouter' }); - } - // Add all providers (except the current one if already provider-managed) for (const p of providers) { - if (domain.source === 'provider' && domain.providerId === p.id) continue; - targetOptions.push({ option: `${p.name} (${p.type})`, key: `provider:${p.id}` }); - } - if (domain.source === 'dcrouter') { - targetOptions.unshift({ option: 'DcRouter (authoritative)', key: 'dcrouter' }); + // Skip current source + if (p.builtIn && domain.source === 'dcrouter') continue; + if (!p.builtIn && domain.source === 'provider' && domain.providerId === p.id) continue; + + const label = p.builtIn ? 'DcRouter (self)' : `${p.name} (${p.type})`; + const key = p.builtIn ? 'dcrouter' : `provider:${p.id}`; + targetOptions.push({ option: label, key }); } if (targetOptions.length === 0) { @@ -345,7 +343,7 @@ export class OpsViewDomains extends DeesElement { } const currentLabel = domain.source === 'dcrouter' - ? 'DcRouter (authoritative)' + ? 'DcRouter (self)' : providers.find((p) => p.id === domain.providerId)?.name || 'Provider'; DeesModal.createAndShow({ diff --git a/ts_web/elements/network/ops-view-network-activity.ts b/ts_web/elements/network/ops-view-network-activity.ts index 657dfbe..1984838 100644 --- a/ts_web/elements/network/ops-view-network-activity.ts +++ b/ts_web/elements/network/ops-view-network-activity.ts @@ -10,22 +10,6 @@ declare global { } } -interface INetworkRequest { - id: string; - timestamp: number; - method: string; - url: string; - hostname: string; - port: number; - protocol: 'http' | 'https' | 'tcp' | 'udp'; - statusCode?: number; - duration: number; - bytesIn: number; - bytesOut: number; - remoteIp: string; - route?: string; -} - @customElement('ops-view-network-activity') export class OpsViewNetworkActivity extends DeesElement { /** How far back the traffic chart shows */ @@ -42,9 +26,6 @@ export class OpsViewNetworkActivity extends DeesElement { accessor networkState = appstate.networkStatePart.getState()!; - @state() - accessor networkRequests: INetworkRequest[] = []; - @state() accessor trafficDataIn: Array<{ x: string | number; y: number }> = []; @@ -314,108 +295,21 @@ export class OpsViewNetworkActivity extends DeesElement { ${this.renderProtocolCharts()} - + ${this.renderTopIPs()} + + ${this.renderTopIPsByBandwidth()} + + + ${this.renderDomainActivity()} + ${this.renderBackendProtocols()} - - - ({ - Time: new Date(req.timestamp).toLocaleTimeString(), - Protocol: html`${req.protocol.toUpperCase()}`, - Method: req.method, - 'Host:Port': `${req.hostname}:${req.port}`, - Path: this.truncateUrl(req.url), - Status: this.renderStatus(req.statusCode), - Duration: `${req.duration}ms`, - 'In/Out': `${this.formatBytes(req.bytesIn)} / ${this.formatBytes(req.bytesOut)}`, - 'Remote IP': req.remoteIp, - })} - .dataActions=${[ - { - name: 'View Details', - iconName: 'fa:magnifyingGlass', - type: ['inRow', 'doubleClick', 'contextmenu'], - actionFunc: async (actionData) => { - await this.showRequestDetails(actionData.item); - } - } - ]} - heading1="Recent Network Activity" - heading2="Recent network requests" - searchable - .showColumnFilters=${true} - .pagination=${true} - .paginationSize=${50} - dataName="request" - > `; } - private async showRequestDetails(request: INetworkRequest) { - const { DeesModal } = await import('@design.estate/dees-catalog'); - - await DeesModal.createAndShow({ - heading: 'Request Details', - content: html` -
- -
- `, - menuOptions: [ - { - name: 'Copy Request ID', - iconName: 'lucide:Copy', - action: async () => { - await navigator.clipboard.writeText(request.id); - } - } - ] - }); - } - - - private renderStatus(statusCode?: number): TemplateResult { - if (!statusCode) { - return html`N/A`; - } - - const statusClass = statusCode >= 200 && statusCode < 300 ? 'success' : - statusCode >= 400 ? 'error' : 'warning'; - - return html`${statusCode}`; - } - - private truncateUrl(url: string, maxLength = 50): string { - if (url.length <= maxLength) return url; - return url.substring(0, maxLength - 3) + '...'; - } - - private formatNumber(num: number): string { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; @@ -619,6 +513,66 @@ export class OpsViewNetworkActivity extends DeesElement { `; } + private renderTopIPsByBandwidth(): TemplateResult { + if (!this.networkState.topIPsByBandwidth || this.networkState.topIPsByBandwidth.length === 0) { + return html``; + } + + return html` + { + return { + 'IP Address': ipData.ip, + 'Bandwidth In': this.formatBitsPerSecond(ipData.bwIn), + 'Bandwidth Out': this.formatBitsPerSecond(ipData.bwOut), + 'Total Bandwidth': this.formatBitsPerSecond(ipData.bwIn + ipData.bwOut), + 'Connections': ipData.count, + }; + }} + heading1="Top IPs by Bandwidth" + heading2="IPs with highest throughput" + searchable + .showColumnFilters=${true} + .pagination=${false} + dataName="ip" + > + `; + } + + private renderDomainActivity(): TemplateResult { + if (!this.networkState.domainActivity || this.networkState.domainActivity.length === 0) { + return html``; + } + + return html` + { + const totalBytesPerMin = (item.bytesInPerSecond + item.bytesOutPerSecond) * 60; + return { + 'Domain': item.domain, + 'Throughput In': this.formatBitsPerSecond(item.bytesInPerSecond), + 'Throughput Out': this.formatBitsPerSecond(item.bytesOutPerSecond), + 'Transferred / min': this.formatBytes(totalBytesPerMin), + 'Connections': item.activeConnections, + 'Routes': item.routeCount, + }; + }} + heading1="Domain Activity" + heading2="Per-domain network activity aggregated from route metrics" + searchable + .showColumnFilters=${true} + .pagination=${false} + dataName="domain" + > + `; + } + private renderBackendProtocols(): TemplateResult { const backends = this.networkState.backends; if (!backends || backends.length === 0) { @@ -730,25 +684,6 @@ export class OpsViewNetworkActivity extends DeesElement { this.requestsPerSecHistory.shift(); } - // Reassign unconditionally so dees-table's flash diff can compare per-cell - // values against the previous snapshot. Row identity is preserved via - // rowKey='id', so DOM nodes are reused across ticks. - this.networkRequests = this.networkState.connections.map((conn) => ({ - id: conn.id, - timestamp: conn.startTime, - method: 'GET', // Default method for proxy connections - url: '/', - hostname: conn.remoteAddress, - port: conn.protocol === 'https' ? 443 : 80, - protocol: conn.protocol === 'https' || conn.protocol === 'http' ? conn.protocol : 'tcp', - statusCode: conn.state === 'connected' ? 200 : undefined, - duration: Date.now() - conn.startTime, - bytesIn: conn.bytesReceived, - bytesOut: conn.bytesSent, - remoteIp: conn.remoteAddress, - route: 'proxy', - })); - // Load server-side throughput history into chart (once) if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) { this.loadThroughputHistory();