@serve.zone/dcrouter 17.1.8 → 17.2.2

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.
@@ -101,6 +101,8 @@ export class SecurityHandler {
101
101
  topASNs: networkStats.topASNs,
102
102
  totalDataTransferred: networkStats.totalDataTransferred,
103
103
  throughputHistory: networkStats.throughputHistory || [],
104
+ frontendConnectionHistory: networkStats.frontendConnectionHistory || [],
105
+ backendConnectionHistory: networkStats.backendConnectionHistory || [],
104
106
  throughputByIP,
105
107
  domainActivity: networkStats.domainActivity || [],
106
108
  requestsPerSecond: networkStats.requestsPerSecond || 0,
@@ -121,6 +123,8 @@ export class SecurityHandler {
121
123
  totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
122
124
  throughputHistory: [],
123
125
  throughputByIP: [],
126
+ frontendConnectionHistory: [],
127
+ backendConnectionHistory: [],
124
128
  domainActivity: [],
125
129
  requestsPerSecond: 0,
126
130
  requestsTotal: 0,
@@ -336,6 +336,8 @@ export class StatsHandler {
336
336
  topASNs: stats.topASNs || [],
337
337
  domainActivity: stats.domainActivity || [],
338
338
  throughputHistory: stats.throughputHistory || [],
339
+ frontendConnectionHistory: stats.frontendConnectionHistory || [],
340
+ backendConnectionHistory: stats.backendConnectionHistory || [],
339
341
  requestsPerSecond: stats.requestsPerSecond || 0,
340
342
  requestsTotal: stats.requestsTotal || 0,
341
343
  backends: stats.backends || [],
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '17.1.8',
6
+ version: '17.2.2',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -19,6 +19,8 @@ export interface INetworkState {
19
19
  ipIntelligence: interfaces.data.IIpIntelligenceRecord[];
20
20
  domainActivity: interfaces.data.IDomainActivity[];
21
21
  throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
22
+ frontendConnectionHistory: interfaces.data.IProtocolConnectionHistoryPoint[];
23
+ backendConnectionHistory: interfaces.data.IProtocolConnectionHistoryPoint[];
22
24
  requestsPerSecond: number;
23
25
  requestsTotal: number;
24
26
  backends: interfaces.data.IBackendInfo[];
@@ -43,6 +45,8 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
43
45
  ipIntelligence: [],
44
46
  domainActivity: [],
45
47
  throughputHistory: [],
48
+ frontendConnectionHistory: [],
49
+ backendConnectionHistory: [],
46
50
  requestsPerSecond: 0,
47
51
  requestsTotal: 0,
48
52
  backends: [],
@@ -150,6 +154,8 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
150
154
  ipIntelligence: currentState.ipIntelligence,
151
155
  domainActivity: networkStatsResponse.domainActivity || [],
152
156
  throughputHistory: networkStatsResponse.throughputHistory || [],
157
+ frontendConnectionHistory: networkStatsResponse.frontendConnectionHistory || [],
158
+ backendConnectionHistory: networkStatsResponse.backendConnectionHistory || [],
153
159
  requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
154
160
  requestsTotal: networkStatsResponse.requestsTotal || 0,
155
161
  backends: networkStatsResponse.backends || [],
@@ -340,6 +340,8 @@ async function dispatchCombinedRefreshActionInner() {
340
340
  throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
341
341
  domainActivity: network.domainActivity || [],
342
342
  throughputHistory: network.throughputHistory || [],
343
+ frontendConnectionHistory: network.frontendConnectionHistory || [],
344
+ backendConnectionHistory: network.backendConnectionHistory || [],
343
345
  requestsPerSecond: network.requestsPerSecond || 0,
344
346
  requestsTotal: network.requestsTotal || 0,
345
347
  backends: network.backends || [],
@@ -0,0 +1,126 @@
1
+ export interface IChartPoint {
2
+ x: string | number;
3
+ y: number;
4
+ }
5
+
6
+ export interface IProtocolConnectionHistoryPointLike {
7
+ timestamp: number;
8
+ total: number;
9
+ protocols: Record<string, number>;
10
+ }
11
+
12
+ export type TProtocolChartSeries = Array<{
13
+ name: string;
14
+ data: IChartPoint[];
15
+ }>;
16
+
17
+ export const PROTOCOL_ORDER = ['HTTP/1.1', 'HTTP/2', 'HTTP/3', 'WebSocket', 'Other'] as const;
18
+ export const PROTOCOL_SERIES_ORDER = ['Total Active', ...PROTOCOL_ORDER] as const;
19
+
20
+ const toEpochSecond = (timestampArg: string | number): number => {
21
+ const timestampMs = typeof timestampArg === 'number'
22
+ ? timestampArg
23
+ : new Date(timestampArg).getTime();
24
+ return Math.floor(timestampMs / 1000);
25
+ };
26
+
27
+ export const getLatestHistoryEpochSecond = (
28
+ historyArg: Array<{ timestamp: number }>,
29
+ ): number => historyArg.reduce(
30
+ (latest, point) => Math.max(latest, Math.floor(point.timestamp / 1000)),
31
+ 0,
32
+ );
33
+
34
+ export const mergeChartPoints = (
35
+ localPointsArg: IChartPoint[],
36
+ canonicalPointsArg: IChartPoint[],
37
+ maximumPointsArg: number,
38
+ ): IChartPoint[] => {
39
+ const pointsBySecond = new Map<number, IChartPoint>();
40
+ for (const point of localPointsArg) {
41
+ const epochSecond = toEpochSecond(point.x);
42
+ if (Number.isFinite(epochSecond)) {
43
+ pointsBySecond.set(epochSecond, point);
44
+ }
45
+ }
46
+ // Canonical engine samples win when a locally repeated cached sample used the same second.
47
+ for (const point of canonicalPointsArg) {
48
+ const epochSecond = toEpochSecond(point.x);
49
+ if (Number.isFinite(epochSecond)) {
50
+ pointsBySecond.set(epochSecond, point);
51
+ }
52
+ }
53
+
54
+ return [...pointsBySecond.entries()]
55
+ .sort(([secondA], [secondB]) => secondA - secondB)
56
+ .map(([, point]) => point)
57
+ .slice(-maximumPointsArg);
58
+ };
59
+
60
+ export const createProtocolConnectionSeries = (
61
+ historyArg: IProtocolConnectionHistoryPointLike[] = [],
62
+ ): TProtocolChartSeries => PROTOCOL_SERIES_ORDER.map((seriesName) => ({
63
+ name: seriesName,
64
+ data: historyArg.map((point) => ({
65
+ x: new Date(point.timestamp).toISOString(),
66
+ y: seriesName === 'Total Active'
67
+ ? point.total
68
+ : (point.protocols[seriesName] ?? 0),
69
+ })),
70
+ }));
71
+
72
+ export const mergeProtocolConnectionHistory = (
73
+ localSeriesArg: TProtocolChartSeries,
74
+ canonicalHistoryArg: IProtocolConnectionHistoryPointLike[],
75
+ maximumPointsArg: number,
76
+ ): TProtocolChartSeries => {
77
+ const localSeriesByName = new Map(localSeriesArg.map((series) => [series.name, series]));
78
+ const canonicalSeriesByName = new Map(
79
+ createProtocolConnectionSeries(canonicalHistoryArg)
80
+ .map((series) => [series.name, series]),
81
+ );
82
+
83
+ return PROTOCOL_SERIES_ORDER.map((seriesName) => ({
84
+ name: seriesName,
85
+ data: mergeChartPoints(
86
+ localSeriesByName.get(seriesName)?.data ?? [],
87
+ canonicalSeriesByName.get(seriesName)?.data ?? [],
88
+ maximumPointsArg,
89
+ ),
90
+ }));
91
+ };
92
+
93
+ export const getProtocolConnectionTotal = (
94
+ protocolsArg: Record<string, number>,
95
+ ): number => (protocolsArg['HTTP/1.1'] ?? 0)
96
+ + (protocolsArg['HTTP/2'] ?? 0)
97
+ + (protocolsArg['HTTP/3'] ?? 0)
98
+ + (protocolsArg.Other ?? 0);
99
+
100
+ export const appendProtocolConnectionSample = (
101
+ localSeriesArg: TProtocolChartSeries,
102
+ timestampArg: string | number,
103
+ protocolsArg: Record<string, number>,
104
+ maximumPointsArg: number,
105
+ ): TProtocolChartSeries => {
106
+ const localSeriesByName = new Map(localSeriesArg.map((series) => [series.name, series]));
107
+ return PROTOCOL_SERIES_ORDER.map((seriesName) => ({
108
+ name: seriesName,
109
+ data: mergeChartPoints(
110
+ localSeriesByName.get(seriesName)?.data ?? [],
111
+ [{
112
+ x: timestampArg,
113
+ y: seriesName === 'Total Active'
114
+ ? getProtocolConnectionTotal(protocolsArg)
115
+ : (protocolsArg[seriesName] ?? 0),
116
+ }],
117
+ maximumPointsArg,
118
+ ),
119
+ }));
120
+ };
121
+
122
+ export const shouldRestartNetworkActivityTimer = (
123
+ isConnectedArg: boolean,
124
+ documentHiddenArg: boolean,
125
+ ): boolean => isConnectedArg && !documentHiddenArg;
126
+
@@ -4,6 +4,25 @@ import * as interfaces from '../../../dist_ts_interfaces/index.js';
4
4
  import { viewHostCss } from '../shared/css.js';
5
5
  import { type IStatsTile } from '@design.estate/dees-catalog';
6
6
 
7
+ import {
8
+ appendProtocolConnectionSample,
9
+ createProtocolConnectionSeries,
10
+ getLatestHistoryEpochSecond,
11
+ mergeChartPoints,
12
+ mergeProtocolConnectionHistory,
13
+ shouldRestartNetworkActivityTimer,
14
+ type IChartPoint,
15
+ type TProtocolChartSeries,
16
+ } from './network-activity-history.js';
17
+
18
+ const PROTOCOL_COLORS: Record<string, string> = {
19
+ 'HTTP/1.1': '#1976d2',
20
+ 'HTTP/2': '#388e3c',
21
+ 'HTTP/3': '#7b1fa2',
22
+ 'WebSocket': '#f57c00',
23
+ 'Other': '#757575',
24
+ };
25
+
7
26
  declare global {
8
27
  interface HTMLElementTagNameMap {
9
28
  'ops-view-network-activity': OpsViewNetworkActivity;
@@ -12,8 +31,8 @@ declare global {
12
31
 
13
32
  @customElement('ops-view-network-activity')
14
33
  export class OpsViewNetworkActivity extends DeesElement {
15
- /** How far back the traffic chart shows */
16
- private static readonly CHART_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
34
+ /** How far back the realtime charts show */
35
+ private static readonly CHART_WINDOW_MS = 10 * 60 * 1000; // 10 minutes
17
36
  /** How often a new data point is added */
18
37
  private static readonly UPDATE_INTERVAL_MS = 1000; // 1 second
19
38
  /** Derived: max data points the buffer holds */
@@ -27,10 +46,16 @@ export class OpsViewNetworkActivity extends DeesElement {
27
46
 
28
47
 
29
48
  @state()
30
- accessor trafficDataIn: Array<{ x: string | number; y: number }> = [];
49
+ accessor trafficDataIn: IChartPoint[] = [];
50
+
51
+ @state()
52
+ accessor trafficDataOut: IChartPoint[] = [];
53
+
54
+ @state()
55
+ accessor frontendConnectionSeries: TProtocolChartSeries = createProtocolConnectionSeries();
31
56
 
32
57
  @state()
33
- accessor trafficDataOut: Array<{ x: string | number; y: number }> = [];
58
+ accessor backendConnectionSeries: TProtocolChartSeries = createProtocolConnectionSeries();
34
59
 
35
60
  // Track if we need to update the chart to avoid unnecessary re-renders
36
61
  private lastChartUpdate = 0;
@@ -38,8 +63,10 @@ export class OpsViewNetworkActivity extends DeesElement {
38
63
 
39
64
  private trafficUpdateTimer: any = null;
40
65
  private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
41
- private historyLoaded = false; // Whether server-side throughput history has been loaded
42
66
  private visibilityHandler: (() => void) | null = null;
67
+ private lastThroughputHistoryEpochSecond = 0;
68
+ private lastFrontendHistoryEpochSecond = 0;
69
+ private lastBackendHistoryEpochSecond = 0;
43
70
 
44
71
  constructor() {
45
72
  super();
@@ -57,7 +84,16 @@ export class OpsViewNetworkActivity extends DeesElement {
57
84
  if (document.hidden) {
58
85
  this.stopTrafficUpdateTimer();
59
86
  } else {
60
- this.startTrafficUpdateTimer();
87
+ void appstate.networkStatePart
88
+ .dispatchAction(appstate.fetchNetworkStatsAction, null)
89
+ .catch((error) => {
90
+ console.error('Failed to refresh network history after visibility resume:', error);
91
+ })
92
+ .finally(() => {
93
+ if (shouldRestartNetworkActivityTimer(this.isConnected, document.hidden)) {
94
+ this.startTrafficUpdateTimer();
95
+ }
96
+ });
61
97
  }
62
98
  };
63
99
  document.addEventListener('visibilitychange', this.visibilityHandler);
@@ -106,51 +142,92 @@ export class OpsViewNetworkActivity extends DeesElement {
106
142
  this.trafficDataOut = emptyData.map(point => ({ ...point }));
107
143
  }
108
144
 
109
- /**
110
- * Load server-side throughput history into the chart.
111
- * Called once when history data first arrives from the Rust engine.
112
- * This pre-populates the chart so users see historical data immediately
113
- * instead of starting from all zeros.
114
- */
115
- private loadThroughputHistory() {
116
- const history = this.networkState.throughputHistory;
117
- if (!history || history.length === 0) return;
118
-
119
- this.historyLoaded = true;
120
-
121
- // Convert history points to chart data format (bytes/sec → Mbit/s)
122
- const historyIn = history.map(p => ({
123
- x: new Date(p.timestamp).toISOString(),
124
- y: Math.round((p.in * 8) / 1000000 * 10) / 10,
145
+ private mergeThroughputHistory(): void {
146
+ const history = this.networkState.throughputHistory || [];
147
+ const latestHistoryEpochSecond = getLatestHistoryEpochSecond(history);
148
+ if (latestHistoryEpochSecond <= this.lastThroughputHistoryEpochSecond) return;
149
+
150
+ const canonicalIn = history.map((point) => ({
151
+ x: new Date(point.timestamp).toISOString(),
152
+ y: Math.round(((point.in * 8) / 1000000) * 10) / 10,
125
153
  }));
126
- const historyOut = history.map(p => ({
127
- x: new Date(p.timestamp).toISOString(),
128
- y: Math.round((p.out * 8) / 1000000 * 10) / 10,
154
+ const canonicalOut = history.map((point) => ({
155
+ x: new Date(point.timestamp).toISOString(),
156
+ y: Math.round(((point.out * 8) / 1000000) * 10) / 10,
129
157
  }));
130
158
 
131
- const { MAX_DATA_POINTS, UPDATE_INTERVAL_MS } = OpsViewNetworkActivity;
159
+ this.trafficDataIn = mergeChartPoints(
160
+ this.trafficDataIn,
161
+ canonicalIn,
162
+ OpsViewNetworkActivity.MAX_DATA_POINTS,
163
+ );
164
+ this.trafficDataOut = mergeChartPoints(
165
+ this.trafficDataOut,
166
+ canonicalOut,
167
+ OpsViewNetworkActivity.MAX_DATA_POINTS,
168
+ );
169
+ this.lastThroughputHistoryEpochSecond = latestHistoryEpochSecond;
170
+ }
132
171
 
133
- // Use history as the chart data, keeping the most recent points within the window
134
- const sliceStart = Math.max(0, historyIn.length - MAX_DATA_POINTS);
135
- this.trafficDataIn = historyIn.slice(sliceStart);
136
- this.trafficDataOut = historyOut.slice(sliceStart);
137
-
138
- // If fewer than MAX_DATA_POINTS, pad the front with zeros
139
- if (this.trafficDataIn.length < MAX_DATA_POINTS) {
140
- const now = Date.now();
141
- const padCount = MAX_DATA_POINTS - this.trafficDataIn.length;
142
- const firstTimestamp = this.trafficDataIn.length > 0
143
- ? new Date(this.trafficDataIn[0].x).getTime()
144
- : now;
145
-
146
- const padIn = Array.from({ length: padCount }, (_, i) => ({
147
- x: new Date(firstTimestamp - ((padCount - i) * UPDATE_INTERVAL_MS)).toISOString(),
148
- y: 0,
149
- }));
150
- const padOut = padIn.map(p => ({ ...p }));
172
+ private getProtocolCounts(
173
+ distributionArg: interfaces.data.IProtocolDistribution,
174
+ ): Record<string, number> {
175
+ return {
176
+ 'HTTP/1.1': distributionArg.h1Active,
177
+ 'HTTP/2': distributionArg.h2Active,
178
+ 'HTTP/3': distributionArg.h3Active,
179
+ 'WebSocket': distributionArg.wsActive,
180
+ 'Other': distributionArg.otherActive,
181
+ };
182
+ }
183
+
184
+ private getProtocolSeriesWithColors(seriesArg: TProtocolChartSeries) {
185
+ return seriesArg.map((series) => ({
186
+ ...series,
187
+ color: series.name === 'Total Active' ? '#ef4444' : (PROTOCOL_COLORS[series.name] ?? '#757575'),
188
+ }));
189
+ }
190
+
191
+ private mergeProtocolConnectionHistories(): void {
192
+ const frontendHistory = this.networkState.frontendConnectionHistory || [];
193
+ const latestFrontendEpochSecond = getLatestHistoryEpochSecond(frontendHistory);
194
+ if (latestFrontendEpochSecond > this.lastFrontendHistoryEpochSecond) {
195
+ this.frontendConnectionSeries = mergeProtocolConnectionHistory(
196
+ this.frontendConnectionSeries,
197
+ frontendHistory,
198
+ OpsViewNetworkActivity.MAX_DATA_POINTS,
199
+ );
200
+ this.lastFrontendHistoryEpochSecond = latestFrontendEpochSecond;
201
+ }
202
+
203
+ const backendHistory = this.networkState.backendConnectionHistory || [];
204
+ const latestBackendEpochSecond = getLatestHistoryEpochSecond(backendHistory);
205
+ if (latestBackendEpochSecond > this.lastBackendHistoryEpochSecond) {
206
+ this.backendConnectionSeries = mergeProtocolConnectionHistory(
207
+ this.backendConnectionSeries,
208
+ backendHistory,
209
+ OpsViewNetworkActivity.MAX_DATA_POINTS,
210
+ );
211
+ this.lastBackendHistoryEpochSecond = latestBackendEpochSecond;
212
+ }
213
+ }
151
214
 
152
- this.trafficDataIn = [...padIn, ...this.trafficDataIn];
153
- this.trafficDataOut = [...padOut, ...this.trafficDataOut];
215
+ private addProtocolConnectionDataPoints(timestampArg: string): void {
216
+ if (this.networkState.frontendProtocols) {
217
+ this.frontendConnectionSeries = appendProtocolConnectionSample(
218
+ this.frontendConnectionSeries,
219
+ timestampArg,
220
+ this.getProtocolCounts(this.networkState.frontendProtocols),
221
+ OpsViewNetworkActivity.MAX_DATA_POINTS,
222
+ );
223
+ }
224
+ if (this.networkState.backendProtocols) {
225
+ this.backendConnectionSeries = appendProtocolConnectionSample(
226
+ this.backendConnectionSeries,
227
+ timestampArg,
228
+ this.getProtocolCounts(this.networkState.backendProtocols),
229
+ OpsViewNetworkActivity.MAX_DATA_POINTS,
230
+ );
154
231
  }
155
232
  }
156
233
 
@@ -162,6 +239,7 @@ export class OpsViewNetworkActivity extends DeesElement {
162
239
  display: flex;
163
240
  flex-direction: column;
164
241
  gap: 24px;
242
+ container-type: inline-size;
165
243
  }
166
244
 
167
245
 
@@ -268,9 +346,31 @@ export class OpsViewNetworkActivity extends DeesElement {
268
346
 
269
347
  .protocolChartGrid {
270
348
  display: grid;
271
- grid-template-columns: repeat(2, 1fr);
349
+ grid-template-columns: repeat(2, minmax(0, 1fr));
350
+ gap: 16px;
351
+ min-width: 0;
352
+ }
353
+
354
+ .protocolChartColumn {
355
+ display: flex;
356
+ flex-direction: column;
272
357
  gap: 16px;
358
+ min-width: 0;
359
+ }
360
+
361
+ .protocolChartColumn > dees-chart-donut,
362
+ .protocolChartColumn > dees-chart-area {
363
+ width: 100%;
364
+ max-width: 100%;
365
+ min-width: 0;
273
366
  }
367
+
368
+ @container (max-width: 800px) {
369
+ .protocolChartGrid {
370
+ grid-template-columns: minmax(0, 1fr);
371
+ }
372
+ }
373
+
274
374
  `,
275
375
  ];
276
376
 
@@ -557,13 +657,7 @@ export class OpsViewNetworkActivity extends DeesElement {
557
657
  const fp = this.networkState.frontendProtocols;
558
658
  const bp = this.networkState.backendProtocols;
559
659
 
560
- const protoColors: Record<string, string> = {
561
- 'HTTP/1.1': '#1976d2',
562
- 'HTTP/2': '#388e3c',
563
- 'HTTP/3': '#7b1fa2',
564
- 'WebSocket': '#f57c00',
565
- 'Other': '#757575',
566
- };
660
+ const protoColors = PROTOCOL_COLORS;
567
661
 
568
662
  const buildDonutData = (dist: interfaces.data.IProtocolDistribution | null) => {
569
663
  if (!dist) return [];
@@ -581,22 +675,40 @@ export class OpsViewNetworkActivity extends DeesElement {
581
675
 
582
676
  return html`
583
677
  <div class="protocolChartGrid">
584
- <dees-chart-donut
585
- .label=${'Frontend Protocols'}
586
- .data=${frontendData.length > 0 ? frontendData : [{ name: 'No Traffic', value: 1, color: '#757575' }]}
587
- .showLegend=${true}
588
- .showLabels=${true}
589
- .innerRadiusPercent=${'55%'}
590
- .valueFormatter=${(val: number) => `${val} active`}
591
- ></dees-chart-donut>
592
- <dees-chart-donut
593
- .label=${'Backend Protocols'}
594
- .data=${backendData.length > 0 ? backendData : [{ name: 'No Traffic', value: 1, color: '#757575' }]}
595
- .showLegend=${true}
596
- .showLabels=${true}
597
- .innerRadiusPercent=${'55%'}
598
- .valueFormatter=${(val: number) => `${val} active`}
599
- ></dees-chart-donut>
678
+ <div class="protocolChartColumn">
679
+ <dees-chart-donut
680
+ .label=${'Frontend Protocols'}
681
+ .data=${frontendData.length > 0 ? frontendData : [{ name: 'No Traffic', value: 1, color: '#757575' }]}
682
+ .showLegend=${true}
683
+ .showLabels=${true}
684
+ .innerRadiusPercent=${'55%'}
685
+ .valueFormatter=${(val: number) => `${val} active`}
686
+ ></dees-chart-donut>
687
+ <dees-chart-area
688
+ .label=${'Frontend Active Connections'}
689
+ .series=${this.getProtocolSeriesWithColors(this.frontendConnectionSeries)}
690
+ .realtimeMode=${true}
691
+ .rollingWindow=${OpsViewNetworkActivity.CHART_WINDOW_MS}
692
+ .yAxisFormatter=${(val: number) => `${Math.round(val)} active`}
693
+ ></dees-chart-area>
694
+ </div>
695
+ <div class="protocolChartColumn">
696
+ <dees-chart-donut
697
+ .label=${'Backend Protocols'}
698
+ .data=${backendData.length > 0 ? backendData : [{ name: 'No Traffic', value: 1, color: '#757575' }]}
699
+ .showLegend=${true}
700
+ .showLabels=${true}
701
+ .innerRadiusPercent=${'55%'}
702
+ .valueFormatter=${(val: number) => `${val} active`}
703
+ ></dees-chart-donut>
704
+ <dees-chart-area
705
+ .label=${'Backend Active Connections'}
706
+ .series=${this.getProtocolSeriesWithColors(this.backendConnectionSeries)}
707
+ .realtimeMode=${true}
708
+ .rollingWindow=${OpsViewNetworkActivity.CHART_WINDOW_MS}
709
+ .yAxisFormatter=${(val: number) => `${Math.round(val)} active`}
710
+ ></dees-chart-area>
711
+ </div>
600
712
  </div>
601
713
  `;
602
714
  }
@@ -962,10 +1074,8 @@ export class OpsViewNetworkActivity extends DeesElement {
962
1074
  this.requestsPerSecHistory.shift();
963
1075
  }
964
1076
 
965
- // Load server-side throughput history into chart (once)
966
- if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) {
967
- this.loadThroughputHistory();
968
- }
1077
+ this.mergeThroughputHistory();
1078
+ this.mergeProtocolConnectionHistories();
969
1079
  }
970
1080
 
971
1081
  private startTrafficUpdateTimer() {
@@ -1005,13 +1115,17 @@ export class OpsViewNetworkActivity extends DeesElement {
1005
1115
  y: Math.round(throughputOutMbps * 10) / 10
1006
1116
  };
1007
1117
 
1008
- // In-place mutation then reassign for Lit reactivity (avoids 4 intermediate arrays)
1009
- if (this.trafficDataIn.length >= OpsViewNetworkActivity.MAX_DATA_POINTS) {
1010
- this.trafficDataIn.shift();
1011
- this.trafficDataOut.shift();
1012
- }
1013
- this.trafficDataIn = [...this.trafficDataIn, newDataPointIn];
1014
- this.trafficDataOut = [...this.trafficDataOut, newDataPointOut];
1118
+ this.trafficDataIn = mergeChartPoints(
1119
+ this.trafficDataIn,
1120
+ [newDataPointIn],
1121
+ OpsViewNetworkActivity.MAX_DATA_POINTS,
1122
+ );
1123
+ this.trafficDataOut = mergeChartPoints(
1124
+ this.trafficDataOut,
1125
+ [newDataPointOut],
1126
+ OpsViewNetworkActivity.MAX_DATA_POINTS,
1127
+ );
1128
+ this.addProtocolConnectionDataPoints(timestamp);
1015
1129
 
1016
1130
  this.lastChartUpdate = now;
1017
1131
  }