@serve.zone/dcrouter 13.13.0 → 13.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -553,12 +553,14 @@ export class MetricsManager {
553
553
  connectionsByIP: new Map<string, number>(),
554
554
  throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
555
555
  topIPs: [] as Array<{ ip: string; count: number }>,
556
+ topIPsByBandwidth: [] as Array<{ ip: string; count: number; bwIn: number; bwOut: number }>,
556
557
  totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
557
558
  throughputHistory: [] as Array<{ timestamp: number; in: number; out: number }>,
558
559
  throughputByIP: new Map<string, { in: number; out: number }>(),
559
560
  requestsPerSecond: 0,
560
561
  requestsTotal: 0,
561
562
  backends: [] as Array<any>,
563
+ domainActivity: [] as Array<{ domain: string; bytesInPerSecond: number; bytesOutPerSecond: number; activeConnections: number; routeCount: number }>,
562
564
  };
563
565
  }
564
566
 
@@ -572,7 +574,7 @@ export class MetricsManager {
572
574
  bytesOutPerSecond: instantThroughput.out
573
575
  };
574
576
 
575
- // Get top IPs
577
+ // Get top IPs by connection count
576
578
  const topIPs = proxyMetrics.connections.topIPs(10);
577
579
 
578
580
  // Get total data transferred
@@ -699,10 +701,83 @@ export class MetricsManager {
699
701
  }
700
702
  }
701
703
 
704
+ // Build top 10 IPs by bandwidth (sorted by total throughput desc)
705
+ const allIPData = new Map<string, { count: number; bwIn: number; bwOut: number }>();
706
+ for (const [ip, count] of connectionsByIP) {
707
+ allIPData.set(ip, { count, bwIn: 0, bwOut: 0 });
708
+ }
709
+ for (const [ip, tp] of throughputByIP) {
710
+ const existing = allIPData.get(ip);
711
+ if (existing) {
712
+ existing.bwIn = tp.in;
713
+ existing.bwOut = tp.out;
714
+ } else {
715
+ allIPData.set(ip, { count: 0, bwIn: tp.in, bwOut: tp.out });
716
+ }
717
+ }
718
+ const topIPsByBandwidth = Array.from(allIPData.entries())
719
+ .sort((a, b) => (b[1].bwIn + b[1].bwOut) - (a[1].bwIn + a[1].bwOut))
720
+ .slice(0, 10)
721
+ .map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut }));
722
+
723
+ // Build domain activity from per-route metrics
724
+ const connectionsByRoute = proxyMetrics.connections.byRoute();
725
+ const throughputByRoute = proxyMetrics.throughput.byRoute();
726
+
727
+ // Map route name → primary domain using dcrouter's route configs
728
+ const routeToDomain = new Map<string, string>();
729
+ if (this.dcRouter.smartProxy) {
730
+ for (const route of this.dcRouter.smartProxy.routeManager.getRoutes()) {
731
+ if (!route.name || !route.match.domains) continue;
732
+ const domains = Array.isArray(route.match.domains)
733
+ ? route.match.domains
734
+ : [route.match.domains];
735
+ if (domains.length > 0) {
736
+ routeToDomain.set(route.name, domains[0]);
737
+ }
738
+ }
739
+ }
740
+
741
+ // Aggregate metrics by domain
742
+ const domainAgg = new Map<string, {
743
+ activeConnections: number;
744
+ bytesInPerSec: number;
745
+ bytesOutPerSec: number;
746
+ routeCount: number;
747
+ }>();
748
+ for (const [routeName, activeConns] of connectionsByRoute) {
749
+ const domain = routeToDomain.get(routeName) || routeName;
750
+ const tp = throughputByRoute.get(routeName) || { in: 0, out: 0 };
751
+ const existing = domainAgg.get(domain);
752
+ if (existing) {
753
+ existing.activeConnections += activeConns;
754
+ existing.bytesInPerSec += tp.in;
755
+ existing.bytesOutPerSec += tp.out;
756
+ existing.routeCount++;
757
+ } else {
758
+ domainAgg.set(domain, {
759
+ activeConnections: activeConns,
760
+ bytesInPerSec: tp.in,
761
+ bytesOutPerSec: tp.out,
762
+ routeCount: 1,
763
+ });
764
+ }
765
+ }
766
+ const domainActivity = Array.from(domainAgg.entries())
767
+ .map(([domain, data]) => ({
768
+ domain,
769
+ bytesInPerSecond: data.bytesInPerSec,
770
+ bytesOutPerSecond: data.bytesOutPerSec,
771
+ activeConnections: data.activeConnections,
772
+ routeCount: data.routeCount,
773
+ }))
774
+ .sort((a, b) => (b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond));
775
+
702
776
  return {
703
777
  connectionsByIP,
704
778
  throughputRate,
705
779
  topIPs,
780
+ topIPsByBandwidth,
706
781
  totalDataTransferred,
707
782
  throughputHistory,
708
783
  throughputByIP,
@@ -711,6 +786,7 @@ export class MetricsManager {
711
786
  backends,
712
787
  frontendProtocols,
713
788
  backendProtocols,
789
+ domainActivity,
714
790
  };
715
791
  }, 1000); // 1s cache — matches typical dashboard poll interval
716
792
  }
@@ -51,8 +51,8 @@ export class SecurityHandler {
51
51
  startTime: conn.startTime,
52
52
  protocol: conn.type === 'http' ? 'https' : conn.type as any,
53
53
  state: conn.status as any,
54
- bytesReceived: Math.floor(conn.bytesTransferred / 2),
55
- bytesSent: Math.floor(conn.bytesTransferred / 2),
54
+ bytesReceived: (conn as any)._throughputIn || 0,
55
+ bytesSent: (conn as any)._throughputOut || 0,
56
56
  }));
57
57
 
58
58
  const summary = {
@@ -96,9 +96,11 @@ export class SecurityHandler {
96
96
  connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })),
97
97
  throughputRate: networkStats.throughputRate,
98
98
  topIPs: networkStats.topIPs,
99
+ topIPsByBandwidth: networkStats.topIPsByBandwidth,
99
100
  totalDataTransferred: networkStats.totalDataTransferred,
100
101
  throughputHistory: networkStats.throughputHistory || [],
101
102
  throughputByIP,
103
+ domainActivity: networkStats.domainActivity || [],
102
104
  requestsPerSecond: networkStats.requestsPerSecond || 0,
103
105
  requestsTotal: networkStats.requestsTotal || 0,
104
106
  backends: networkStats.backends || [],
@@ -110,9 +112,11 @@ export class SecurityHandler {
110
112
  connectionsByIP: [],
111
113
  throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
112
114
  topIPs: [],
115
+ topIPsByBandwidth: [],
113
116
  totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
114
117
  throughputHistory: [],
115
118
  throughputByIP: [],
119
+ domainActivity: [],
116
120
  requestsPerSecond: 0,
117
121
  requestsTotal: 0,
118
122
  backends: [],
@@ -251,31 +255,31 @@ export class SecurityHandler {
251
255
  const connectionInfo = await this.opsServerRef.dcRouterRef.metricsManager.getConnectionInfo();
252
256
  const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
253
257
 
254
- // Use IP-based connection data from the new metrics API
258
+ // One aggregate row per IP with real throughput data
255
259
  if (networkStats.connectionsByIP && networkStats.connectionsByIP.size > 0) {
256
260
  let connIndex = 0;
257
261
  const publicIp = this.opsServerRef.dcRouterRef.options.publicIp || 'server';
258
-
262
+
259
263
  for (const [ip, count] of networkStats.connectionsByIP) {
260
- // Create a connection entry for each active IP connection
261
- for (let i = 0; i < Math.min(count, 5); i++) { // Limit to 5 connections per IP for UI performance
262
- connections.push({
263
- id: `conn-${connIndex++}`,
264
- type: 'http',
265
- source: {
266
- ip: ip,
267
- port: Math.floor(Math.random() * 50000) + 10000, // High port range
268
- },
269
- destination: {
270
- ip: publicIp,
271
- port: 443,
272
- service: 'proxy',
273
- },
274
- startTime: Date.now() - Math.floor(Math.random() * 3600000), // Within last hour
275
- bytesTransferred: Math.floor(networkStats.totalDataTransferred.bytesIn / networkStats.connectionsByIP.size),
276
- status: 'active',
277
- });
278
- }
264
+ const tp = networkStats.throughputByIP?.get(ip);
265
+ connections.push({
266
+ id: `ip-${connIndex++}`,
267
+ type: 'http',
268
+ source: {
269
+ ip: ip,
270
+ port: 0,
271
+ },
272
+ destination: {
273
+ ip: publicIp,
274
+ port: 443,
275
+ service: 'proxy',
276
+ },
277
+ startTime: 0,
278
+ bytesTransferred: count, // Store connection count here
279
+ status: 'active',
280
+ // Attach real throughput for the handler mapping
281
+ ...(tp ? { _throughputIn: tp.in, _throughputOut: tp.out } : {}),
282
+ } as any);
279
283
  }
280
284
  } else if (connectionInfo.length > 0) {
281
285
  // Fallback to route-based connection info if no IP data available
@@ -291,6 +291,20 @@ export class StatsHandler {
291
291
  }
292
292
  }
293
293
 
294
+ // Build connectionDetails from real per-IP data
295
+ const connectionDetails: interfaces.data.IConnectionDetails[] = [];
296
+ for (const [ip, count] of stats.connectionsByIP) {
297
+ const tp = stats.throughputByIP?.get(ip);
298
+ connectionDetails.push({
299
+ remoteAddress: ip,
300
+ protocol: 'https',
301
+ state: 'connected',
302
+ startTime: 0,
303
+ bytesIn: tp?.in || 0,
304
+ bytesOut: tp?.out || 0,
305
+ });
306
+ }
307
+
294
308
  metrics.network = {
295
309
  totalBandwidth: {
296
310
  in: stats.throughputRate.bytesInPerSecond,
@@ -301,12 +315,18 @@ export class StatsHandler {
301
315
  out: stats.totalDataTransferred.bytesOut,
302
316
  },
303
317
  activeConnections: serverStats.activeConnections,
304
- connectionDetails: [],
318
+ connectionDetails,
305
319
  topEndpoints: stats.topIPs.map(ip => ({
306
320
  endpoint: ip.ip,
307
- requests: ip.count,
321
+ connections: ip.count,
308
322
  bandwidth: ipBandwidth.get(ip.ip) || { in: 0, out: 0 },
309
323
  })),
324
+ topEndpointsByBandwidth: stats.topIPsByBandwidth.map(ip => ({
325
+ endpoint: ip.ip,
326
+ connections: ip.count,
327
+ bandwidth: { in: ip.bwIn, out: ip.bwOut },
328
+ })),
329
+ domainActivity: stats.domainActivity || [],
310
330
  throughputHistory: stats.throughputHistory || [],
311
331
  requestsPerSecond: stats.requestsPerSecond || 0,
312
332
  requestsTotal: stats.requestsTotal || 0,
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.13.0',
6
+ version: '13.14.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -52,7 +52,9 @@ export interface INetworkState {
52
52
  throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
53
53
  totalBytes: { in: number; out: number };
54
54
  topIPs: Array<{ ip: string; count: number }>;
55
+ topIPsByBandwidth: Array<{ ip: string; count: number; bwIn: number; bwOut: number }>;
55
56
  throughputByIP: Array<{ ip: string; in: number; out: number }>;
57
+ domainActivity: interfaces.data.IDomainActivity[];
56
58
  throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
57
59
  requestsPerSecond: number;
58
60
  requestsTotal: number;
@@ -160,7 +162,9 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
160
162
  throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
161
163
  totalBytes: { in: 0, out: 0 },
162
164
  topIPs: [],
165
+ topIPsByBandwidth: [],
163
166
  throughputByIP: [],
167
+ domainActivity: [],
164
168
  throughputHistory: [],
165
169
  requestsPerSecond: 0,
166
170
  requestsTotal: 0,
@@ -552,7 +556,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
552
556
  ? { in: networkStatsResponse.totalDataTransferred.bytesIn, out: networkStatsResponse.totalDataTransferred.bytesOut }
553
557
  : { in: 0, out: 0 },
554
558
  topIPs: networkStatsResponse.topIPs || [],
559
+ topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [],
555
560
  throughputByIP: networkStatsResponse.throughputByIP || [],
561
+ domainActivity: networkStatsResponse.domainActivity || [],
556
562
  throughputHistory: networkStatsResponse.throughputHistory || [],
557
563
  requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
558
564
  requestsTotal: networkStatsResponse.requestsTotal || 0,
@@ -2649,67 +2655,52 @@ async function dispatchCombinedRefreshActionInner() {
2649
2655
  if (combinedResponse.metrics.network && currentView === 'network') {
2650
2656
  const network = combinedResponse.metrics.network;
2651
2657
  const connectionsByIP: { [ip: string]: number } = {};
2652
-
2653
- // Convert connection details to IP counts
2658
+
2659
+ // Build connectionsByIP from connectionDetails (now populated with real per-IP data)
2654
2660
  network.connectionDetails.forEach(conn => {
2655
2661
  connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + 1;
2656
2662
  });
2657
2663
 
2658
- // Fetch detailed connections for the network view
2659
- try {
2660
- const connectionsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
2661
- interfaces.requests.IReq_GetActiveConnections
2662
- >('/typedrequest', 'getActiveConnections');
2663
-
2664
- const connectionsResponse = await connectionsRequest.fire({
2665
- identity: context.identity,
2666
- });
2667
-
2668
- networkStatePart.setState({
2669
- ...networkStatePart.getState()!,
2670
- connections: connectionsResponse.connections,
2671
- connectionsByIP,
2672
- throughputRate: {
2673
- bytesInPerSecond: network.totalBandwidth.in,
2674
- bytesOutPerSecond: network.totalBandwidth.out
2675
- },
2676
- totalBytes: network.totalBytes || { in: 0, out: 0 },
2677
- topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
2678
- throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
2679
- throughputHistory: network.throughputHistory || [],
2680
- requestsPerSecond: network.requestsPerSecond || 0,
2681
- requestsTotal: network.requestsTotal || 0,
2682
- backends: network.backends || [],
2683
- frontendProtocols: network.frontendProtocols || null,
2684
- backendProtocols: network.backendProtocols || null,
2685
- lastUpdated: Date.now(),
2686
- isLoading: false,
2687
- error: null,
2688
- });
2689
- } catch (error: unknown) {
2690
- console.error('Failed to fetch connections:', error);
2691
- networkStatePart.setState({
2692
- ...networkStatePart.getState()!,
2693
- connections: [],
2694
- connectionsByIP,
2695
- throughputRate: {
2696
- bytesInPerSecond: network.totalBandwidth.in,
2697
- bytesOutPerSecond: network.totalBandwidth.out
2698
- },
2699
- totalBytes: network.totalBytes || { in: 0, out: 0 },
2700
- topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
2701
- throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
2702
- throughputHistory: network.throughputHistory || [],
2703
- requestsPerSecond: network.requestsPerSecond || 0,
2704
- requestsTotal: network.requestsTotal || 0,
2705
- backends: network.backends || [],
2706
- frontendProtocols: network.frontendProtocols || null,
2707
- backendProtocols: network.backendProtocols || null,
2708
- lastUpdated: Date.now(),
2709
- isLoading: false,
2710
- error: null,
2711
- });
2712
- }
2664
+ // Build connections from connectionDetails (real per-IP aggregates)
2665
+ const connections: interfaces.data.IConnectionInfo[] = network.connectionDetails.map((conn, i) => ({
2666
+ id: `ip-${conn.remoteAddress}`,
2667
+ remoteAddress: conn.remoteAddress,
2668
+ localAddress: 'server',
2669
+ startTime: conn.startTime,
2670
+ protocol: conn.protocol as any,
2671
+ state: conn.state as any,
2672
+ bytesReceived: conn.bytesIn,
2673
+ bytesSent: conn.bytesOut,
2674
+ }));
2675
+
2676
+ networkStatePart.setState({
2677
+ ...networkStatePart.getState()!,
2678
+ connections,
2679
+ connectionsByIP,
2680
+ throughputRate: {
2681
+ bytesInPerSecond: network.totalBandwidth.in,
2682
+ bytesOutPerSecond: network.totalBandwidth.out,
2683
+ },
2684
+ totalBytes: network.totalBytes || { in: 0, out: 0 },
2685
+ topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.connections })),
2686
+ topIPsByBandwidth: (network.topEndpointsByBandwidth || []).map(e => ({
2687
+ ip: e.endpoint,
2688
+ count: e.connections,
2689
+ bwIn: e.bandwidth?.in || 0,
2690
+ bwOut: e.bandwidth?.out || 0,
2691
+ })),
2692
+ throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
2693
+ domainActivity: network.domainActivity || [],
2694
+ throughputHistory: network.throughputHistory || [],
2695
+ requestsPerSecond: network.requestsPerSecond || 0,
2696
+ requestsTotal: network.requestsTotal || 0,
2697
+ backends: network.backends || [],
2698
+ frontendProtocols: network.frontendProtocols || null,
2699
+ backendProtocols: network.backendProtocols || null,
2700
+ lastUpdated: Date.now(),
2701
+ isLoading: false,
2702
+ error: null,
2703
+ });
2713
2704
  }
2714
2705
 
2715
2706
  // Refresh certificate data if on Domains > Certificates subview
@@ -323,16 +323,14 @@ export class OpsViewDomains extends DeesElement {
323
323
 
324
324
  // Build target options based on current source
325
325
  const targetOptions: { option: string; key: string }[] = [];
326
- if (domain.source === 'provider') {
327
- targetOptions.push({ option: 'DcRouter (authoritative)', key: 'dcrouter' });
328
- }
329
- // Add all providers (except the current one if already provider-managed)
330
326
  for (const p of providers) {
331
- if (domain.source === 'provider' && domain.providerId === p.id) continue;
332
- targetOptions.push({ option: `${p.name} (${p.type})`, key: `provider:${p.id}` });
333
- }
334
- if (domain.source === 'dcrouter') {
335
- targetOptions.unshift({ option: 'DcRouter (authoritative)', key: 'dcrouter' });
327
+ // Skip current source
328
+ if (p.builtIn && domain.source === 'dcrouter') continue;
329
+ if (!p.builtIn && domain.source === 'provider' && domain.providerId === p.id) continue;
330
+
331
+ const label = p.builtIn ? 'DcRouter (self)' : `${p.name} (${p.type})`;
332
+ const key = p.builtIn ? 'dcrouter' : `provider:${p.id}`;
333
+ targetOptions.push({ option: label, key });
336
334
  }
337
335
 
338
336
  if (targetOptions.length === 0) {
@@ -345,7 +343,7 @@ export class OpsViewDomains extends DeesElement {
345
343
  }
346
344
 
347
345
  const currentLabel = domain.source === 'dcrouter'
348
- ? 'DcRouter (authoritative)'
346
+ ? 'DcRouter (self)'
349
347
  : providers.find((p) => p.id === domain.providerId)?.name || 'Provider';
350
348
 
351
349
  DeesModal.createAndShow({
@@ -10,22 +10,6 @@ declare global {
10
10
  }
11
11
  }
12
12
 
13
- interface INetworkRequest {
14
- id: string;
15
- timestamp: number;
16
- method: string;
17
- url: string;
18
- hostname: string;
19
- port: number;
20
- protocol: 'http' | 'https' | 'tcp' | 'udp';
21
- statusCode?: number;
22
- duration: number;
23
- bytesIn: number;
24
- bytesOut: number;
25
- remoteIp: string;
26
- route?: string;
27
- }
28
-
29
13
  @customElement('ops-view-network-activity')
30
14
  export class OpsViewNetworkActivity extends DeesElement {
31
15
  /** How far back the traffic chart shows */
@@ -42,9 +26,6 @@ export class OpsViewNetworkActivity extends DeesElement {
42
26
  accessor networkState = appstate.networkStatePart.getState()!;
43
27
 
44
28
 
45
- @state()
46
- accessor networkRequests: INetworkRequest[] = [];
47
-
48
29
  @state()
49
30
  accessor trafficDataIn: Array<{ x: string | number; y: number }> = [];
50
31
 
@@ -314,108 +295,21 @@ export class OpsViewNetworkActivity extends DeesElement {
314
295
  <!-- Protocol Distribution Charts -->
315
296
  ${this.renderProtocolCharts()}
316
297
 
317
- <!-- Top IPs Section -->
298
+ <!-- Top IPs by Connection Count -->
318
299
  ${this.renderTopIPs()}
319
300
 
301
+ <!-- Top IPs by Bandwidth -->
302
+ ${this.renderTopIPsByBandwidth()}
303
+
304
+ <!-- Domain Activity -->
305
+ ${this.renderDomainActivity()}
306
+
320
307
  <!-- Backend Protocols Section -->
321
308
  ${this.renderBackendProtocols()}
322
-
323
- <!-- Requests Table -->
324
- <dees-table
325
- .data=${this.networkRequests}
326
- .rowKey=${'id'}
327
- .highlightUpdates=${'flash'}
328
- .displayFunction=${(req: INetworkRequest) => ({
329
- Time: new Date(req.timestamp).toLocaleTimeString(),
330
- Protocol: html`<span class="protocolBadge ${req.protocol}">${req.protocol.toUpperCase()}</span>`,
331
- Method: req.method,
332
- 'Host:Port': `${req.hostname}:${req.port}`,
333
- Path: this.truncateUrl(req.url),
334
- Status: this.renderStatus(req.statusCode),
335
- Duration: `${req.duration}ms`,
336
- 'In/Out': `${this.formatBytes(req.bytesIn)} / ${this.formatBytes(req.bytesOut)}`,
337
- 'Remote IP': req.remoteIp,
338
- })}
339
- .dataActions=${[
340
- {
341
- name: 'View Details',
342
- iconName: 'fa:magnifyingGlass',
343
- type: ['inRow', 'doubleClick', 'contextmenu'],
344
- actionFunc: async (actionData) => {
345
- await this.showRequestDetails(actionData.item);
346
- }
347
- }
348
- ]}
349
- heading1="Recent Network Activity"
350
- heading2="Recent network requests"
351
- searchable
352
- .showColumnFilters=${true}
353
- .pagination=${true}
354
- .paginationSize=${50}
355
- dataName="request"
356
- ></dees-table>
357
309
  </div>
358
310
  `;
359
311
  }
360
312
 
361
- private async showRequestDetails(request: INetworkRequest) {
362
- const { DeesModal } = await import('@design.estate/dees-catalog');
363
-
364
- await DeesModal.createAndShow({
365
- heading: 'Request Details',
366
- content: html`
367
- <div style="padding: 20px;">
368
- <dees-dataview-codebox
369
- .heading=${'Request Information'}
370
- progLang="json"
371
- .codeToDisplay=${JSON.stringify({
372
- id: request.id,
373
- timestamp: new Date(request.timestamp).toISOString(),
374
- protocol: request.protocol,
375
- method: request.method,
376
- url: request.url,
377
- hostname: request.hostname,
378
- port: request.port,
379
- statusCode: request.statusCode,
380
- duration: `${request.duration}ms`,
381
- bytesIn: request.bytesIn,
382
- bytesOut: request.bytesOut,
383
- remoteIp: request.remoteIp,
384
- route: request.route,
385
- }, null, 2)}
386
- ></dees-dataview-codebox>
387
- </div>
388
- `,
389
- menuOptions: [
390
- {
391
- name: 'Copy Request ID',
392
- iconName: 'lucide:Copy',
393
- action: async () => {
394
- await navigator.clipboard.writeText(request.id);
395
- }
396
- }
397
- ]
398
- });
399
- }
400
-
401
-
402
- private renderStatus(statusCode?: number): TemplateResult {
403
- if (!statusCode) {
404
- return html`<span class="statusBadge warning">N/A</span>`;
405
- }
406
-
407
- const statusClass = statusCode >= 200 && statusCode < 300 ? 'success' :
408
- statusCode >= 400 ? 'error' : 'warning';
409
-
410
- return html`<span class="statusBadge ${statusClass}">${statusCode}</span>`;
411
- }
412
-
413
- private truncateUrl(url: string, maxLength = 50): string {
414
- if (url.length <= maxLength) return url;
415
- return url.substring(0, maxLength - 3) + '...';
416
- }
417
-
418
-
419
313
  private formatNumber(num: number): string {
420
314
  if (num >= 1000000) {
421
315
  return (num / 1000000).toFixed(1) + 'M';
@@ -619,6 +513,66 @@ export class OpsViewNetworkActivity extends DeesElement {
619
513
  `;
620
514
  }
621
515
 
516
+ private renderTopIPsByBandwidth(): TemplateResult {
517
+ if (!this.networkState.topIPsByBandwidth || this.networkState.topIPsByBandwidth.length === 0) {
518
+ return html``;
519
+ }
520
+
521
+ return html`
522
+ <dees-table
523
+ .data=${this.networkState.topIPsByBandwidth}
524
+ .rowKey=${'ip'}
525
+ .highlightUpdates=${'flash'}
526
+ .displayFunction=${(ipData: { ip: string; count: number; bwIn: number; bwOut: number }) => {
527
+ return {
528
+ 'IP Address': ipData.ip,
529
+ 'Bandwidth In': this.formatBitsPerSecond(ipData.bwIn),
530
+ 'Bandwidth Out': this.formatBitsPerSecond(ipData.bwOut),
531
+ 'Total Bandwidth': this.formatBitsPerSecond(ipData.bwIn + ipData.bwOut),
532
+ 'Connections': ipData.count,
533
+ };
534
+ }}
535
+ heading1="Top IPs by Bandwidth"
536
+ heading2="IPs with highest throughput"
537
+ searchable
538
+ .showColumnFilters=${true}
539
+ .pagination=${false}
540
+ dataName="ip"
541
+ ></dees-table>
542
+ `;
543
+ }
544
+
545
+ private renderDomainActivity(): TemplateResult {
546
+ if (!this.networkState.domainActivity || this.networkState.domainActivity.length === 0) {
547
+ return html``;
548
+ }
549
+
550
+ return html`
551
+ <dees-table
552
+ .data=${this.networkState.domainActivity}
553
+ .rowKey=${'domain'}
554
+ .highlightUpdates=${'flash'}
555
+ .displayFunction=${(item: interfaces.data.IDomainActivity) => {
556
+ const totalBytesPerMin = (item.bytesInPerSecond + item.bytesOutPerSecond) * 60;
557
+ return {
558
+ 'Domain': item.domain,
559
+ 'Throughput In': this.formatBitsPerSecond(item.bytesInPerSecond),
560
+ 'Throughput Out': this.formatBitsPerSecond(item.bytesOutPerSecond),
561
+ 'Transferred / min': this.formatBytes(totalBytesPerMin),
562
+ 'Connections': item.activeConnections,
563
+ 'Routes': item.routeCount,
564
+ };
565
+ }}
566
+ heading1="Domain Activity"
567
+ heading2="Per-domain network activity aggregated from route metrics"
568
+ searchable
569
+ .showColumnFilters=${true}
570
+ .pagination=${false}
571
+ dataName="domain"
572
+ ></dees-table>
573
+ `;
574
+ }
575
+
622
576
  private renderBackendProtocols(): TemplateResult {
623
577
  const backends = this.networkState.backends;
624
578
  if (!backends || backends.length === 0) {
@@ -730,25 +684,6 @@ export class OpsViewNetworkActivity extends DeesElement {
730
684
  this.requestsPerSecHistory.shift();
731
685
  }
732
686
 
733
- // Reassign unconditionally so dees-table's flash diff can compare per-cell
734
- // values against the previous snapshot. Row identity is preserved via
735
- // rowKey='id', so DOM nodes are reused across ticks.
736
- this.networkRequests = this.networkState.connections.map((conn) => ({
737
- id: conn.id,
738
- timestamp: conn.startTime,
739
- method: 'GET', // Default method for proxy connections
740
- url: '/',
741
- hostname: conn.remoteAddress,
742
- port: conn.protocol === 'https' ? 443 : 80,
743
- protocol: conn.protocol === 'https' || conn.protocol === 'http' ? conn.protocol : 'tcp',
744
- statusCode: conn.state === 'connected' ? 200 : undefined,
745
- duration: Date.now() - conn.startTime,
746
- bytesIn: conn.bytesReceived,
747
- bytesOut: conn.bytesSent,
748
- remoteIp: conn.remoteAddress,
749
- route: 'proxy',
750
- }));
751
-
752
687
  // Load server-side throughput history into chart (once)
753
688
  if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) {
754
689
  this.loadThroughputHistory();