@serve.zone/dcrouter 13.13.0 → 13.15.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.15.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,
@@ -518,14 +522,13 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
518
522
  });
519
523
 
520
524
  // Get network stats for throughput and IP data
521
- const networkStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest(
522
- '/typedrequest',
523
- 'getNetworkStats'
524
- );
525
-
525
+ const networkStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
526
+ interfaces.requests.IReq_GetNetworkStats
527
+ >('/typedrequest', 'getNetworkStats');
528
+
526
529
  const networkStatsResponse = await networkStatsRequest.fire({
527
530
  identity: context.identity,
528
- }) as any;
531
+ });
529
532
 
530
533
  // Use the connections data for the connection list
531
534
  // and network stats for throughput and IP analytics
@@ -552,7 +555,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
552
555
  ? { in: networkStatsResponse.totalDataTransferred.bytesIn, out: networkStatsResponse.totalDataTransferred.bytesOut }
553
556
  : { in: 0, out: 0 },
554
557
  topIPs: networkStatsResponse.topIPs || [],
558
+ topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [],
555
559
  throughputByIP: networkStatsResponse.throughputByIP || [],
560
+ domainActivity: networkStatsResponse.domainActivity || [],
556
561
  throughputHistory: networkStatsResponse.throughputHistory || [],
557
562
  requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
558
563
  requestsTotal: networkStatsResponse.requestsTotal || 0,
@@ -2649,67 +2654,52 @@ async function dispatchCombinedRefreshActionInner() {
2649
2654
  if (combinedResponse.metrics.network && currentView === 'network') {
2650
2655
  const network = combinedResponse.metrics.network;
2651
2656
  const connectionsByIP: { [ip: string]: number } = {};
2652
-
2653
- // Convert connection details to IP counts
2657
+
2658
+ // Build connectionsByIP from connectionDetails (now populated with real per-IP data)
2654
2659
  network.connectionDetails.forEach(conn => {
2655
2660
  connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + 1;
2656
2661
  });
2657
2662
 
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
- }
2663
+ // Build connections from connectionDetails (real per-IP aggregates)
2664
+ const connections: interfaces.data.IConnectionInfo[] = network.connectionDetails.map((conn, i) => ({
2665
+ id: `ip-${conn.remoteAddress}`,
2666
+ remoteAddress: conn.remoteAddress,
2667
+ localAddress: 'server',
2668
+ startTime: conn.startTime,
2669
+ protocol: conn.protocol as any,
2670
+ state: conn.state as any,
2671
+ bytesReceived: conn.bytesIn,
2672
+ bytesSent: conn.bytesOut,
2673
+ }));
2674
+
2675
+ networkStatePart.setState({
2676
+ ...networkStatePart.getState()!,
2677
+ connections,
2678
+ connectionsByIP,
2679
+ throughputRate: {
2680
+ bytesInPerSecond: network.totalBandwidth.in,
2681
+ bytesOutPerSecond: network.totalBandwidth.out,
2682
+ },
2683
+ totalBytes: network.totalBytes || { in: 0, out: 0 },
2684
+ topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.connections })),
2685
+ topIPsByBandwidth: (network.topEndpointsByBandwidth || []).map(e => ({
2686
+ ip: e.endpoint,
2687
+ count: e.connections,
2688
+ bwIn: e.bandwidth?.in || 0,
2689
+ bwOut: e.bandwidth?.out || 0,
2690
+ })),
2691
+ throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
2692
+ domainActivity: network.domainActivity || [],
2693
+ throughputHistory: network.throughputHistory || [],
2694
+ requestsPerSecond: network.requestsPerSecond || 0,
2695
+ requestsTotal: network.requestsTotal || 0,
2696
+ backends: network.backends || [],
2697
+ frontendProtocols: network.frontendProtocols || null,
2698
+ backendProtocols: network.backendProtocols || null,
2699
+ lastUpdated: Date.now(),
2700
+ isLoading: false,
2701
+ error: null,
2702
+ });
2713
2703
  }
2714
2704
 
2715
2705
  // 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({