@serve.zone/dcrouter 13.12.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.
Files changed (33) hide show
  1. package/dist_serve/bundle.js +809 -779
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.js +6 -5
  4. package/dist_ts/dns/manager.dns.d.ts +46 -8
  5. package/dist_ts/dns/manager.dns.js +189 -36
  6. package/dist_ts/monitoring/classes.metricsmanager.d.ts +26 -0
  7. package/dist_ts/monitoring/classes.metricsmanager.js +72 -2
  8. package/dist_ts/opsserver/handlers/config.handler.js +2 -2
  9. package/dist_ts/opsserver/handlers/domain.handler.js +14 -1
  10. package/dist_ts/opsserver/handlers/security.handler.js +27 -23
  11. package/dist_ts/opsserver/handlers/stats.handler.js +22 -3
  12. package/dist_ts_interfaces/data/stats.d.ts +17 -1
  13. package/dist_ts_interfaces/requests/domains.d.ts +24 -0
  14. package/dist_ts_web/00_commitinfo_data.js +1 -1
  15. package/dist_ts_web/appstate.d.ts +13 -0
  16. package/dist_ts_web/appstate.js +62 -54
  17. package/dist_ts_web/elements/domains/ops-view-domains.d.ts +1 -0
  18. package/dist_ts_web/elements/domains/ops-view-domains.js +95 -1
  19. package/dist_ts_web/elements/network/ops-view-network-activity.d.ts +2 -20
  20. package/dist_ts_web/elements/network/ops-view-network-activity.js +65 -115
  21. package/package.json +1 -1
  22. package/ts/00_commitinfo_data.ts +1 -1
  23. package/ts/classes.dcrouter.ts +5 -4
  24. package/ts/dns/manager.dns.ts +219 -35
  25. package/ts/monitoring/classes.metricsmanager.ts +77 -1
  26. package/ts/opsserver/handlers/config.handler.ts +1 -1
  27. package/ts/opsserver/handlers/domain.handler.ts +18 -0
  28. package/ts/opsserver/handlers/security.handler.ts +27 -23
  29. package/ts/opsserver/handlers/stats.handler.ts +22 -2
  30. package/ts_web/00_commitinfo_data.ts +1 -1
  31. package/ts_web/appstate.ts +74 -57
  32. package/ts_web/elements/domains/ops-view-domains.ts +97 -0
  33. package/ts_web/elements/network/ops-view-network-activity.ts +67 -132
@@ -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,
@@ -1887,6 +1893,32 @@ export const syncDomainAction = domainsStatePart.createAction<{ id: string }>(
1887
1893
  },
1888
1894
  );
1889
1895
 
1896
+ export const migrateDomainAction = domainsStatePart.createAction<{
1897
+ id: string;
1898
+ targetSource: interfaces.data.TDomainSource;
1899
+ targetProviderId?: string;
1900
+ deleteExistingProviderRecords?: boolean;
1901
+ }>(
1902
+ async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
1903
+ const context = getActionContext();
1904
+ try {
1905
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1906
+ interfaces.requests.IReq_MigrateDomain
1907
+ >('/typedrequest', 'migrateDomain');
1908
+ const response = await request.fire({ identity: context.identity!, ...dataArg });
1909
+ if (!response.success) {
1910
+ return { ...statePartArg.getState()!, error: response.message || 'Migration failed' };
1911
+ }
1912
+ return await actionContext!.dispatch(fetchDomainsAndProvidersAction, null);
1913
+ } catch (error: unknown) {
1914
+ return {
1915
+ ...statePartArg.getState()!,
1916
+ error: error instanceof Error ? error.message : 'Migration failed',
1917
+ };
1918
+ }
1919
+ },
1920
+ );
1921
+
1890
1922
  export const createDnsRecordAction = domainsStatePart.createAction<{
1891
1923
  domainId: string;
1892
1924
  name: string;
@@ -2623,67 +2655,52 @@ async function dispatchCombinedRefreshActionInner() {
2623
2655
  if (combinedResponse.metrics.network && currentView === 'network') {
2624
2656
  const network = combinedResponse.metrics.network;
2625
2657
  const connectionsByIP: { [ip: string]: number } = {};
2626
-
2627
- // Convert connection details to IP counts
2658
+
2659
+ // Build connectionsByIP from connectionDetails (now populated with real per-IP data)
2628
2660
  network.connectionDetails.forEach(conn => {
2629
2661
  connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + 1;
2630
2662
  });
2631
2663
 
2632
- // Fetch detailed connections for the network view
2633
- try {
2634
- const connectionsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
2635
- interfaces.requests.IReq_GetActiveConnections
2636
- >('/typedrequest', 'getActiveConnections');
2637
-
2638
- const connectionsResponse = await connectionsRequest.fire({
2639
- identity: context.identity,
2640
- });
2641
-
2642
- networkStatePart.setState({
2643
- ...networkStatePart.getState()!,
2644
- connections: connectionsResponse.connections,
2645
- connectionsByIP,
2646
- throughputRate: {
2647
- bytesInPerSecond: network.totalBandwidth.in,
2648
- bytesOutPerSecond: network.totalBandwidth.out
2649
- },
2650
- totalBytes: network.totalBytes || { in: 0, out: 0 },
2651
- topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
2652
- throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
2653
- throughputHistory: network.throughputHistory || [],
2654
- requestsPerSecond: network.requestsPerSecond || 0,
2655
- requestsTotal: network.requestsTotal || 0,
2656
- backends: network.backends || [],
2657
- frontendProtocols: network.frontendProtocols || null,
2658
- backendProtocols: network.backendProtocols || null,
2659
- lastUpdated: Date.now(),
2660
- isLoading: false,
2661
- error: null,
2662
- });
2663
- } catch (error: unknown) {
2664
- console.error('Failed to fetch connections:', error);
2665
- networkStatePart.setState({
2666
- ...networkStatePart.getState()!,
2667
- connections: [],
2668
- connectionsByIP,
2669
- throughputRate: {
2670
- bytesInPerSecond: network.totalBandwidth.in,
2671
- bytesOutPerSecond: network.totalBandwidth.out
2672
- },
2673
- totalBytes: network.totalBytes || { in: 0, out: 0 },
2674
- topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
2675
- throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
2676
- throughputHistory: network.throughputHistory || [],
2677
- requestsPerSecond: network.requestsPerSecond || 0,
2678
- requestsTotal: network.requestsTotal || 0,
2679
- backends: network.backends || [],
2680
- frontendProtocols: network.frontendProtocols || null,
2681
- backendProtocols: network.backendProtocols || null,
2682
- lastUpdated: Date.now(),
2683
- isLoading: false,
2684
- error: null,
2685
- });
2686
- }
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
+ });
2687
2704
  }
2688
2705
 
2689
2706
  // Refresh certificate data if on Domains > Certificates subview
@@ -149,6 +149,15 @@ export class OpsViewDomains extends DeesElement {
149
149
  });
150
150
  },
151
151
  },
152
+ {
153
+ name: 'Migrate',
154
+ iconName: 'lucide:arrow-right-left',
155
+ type: ['inRow', 'contextmenu'] as any,
156
+ actionFunc: async (actionData: any) => {
157
+ const domain = actionData.item as interfaces.data.IDomain;
158
+ await this.showMigrateDialog(domain);
159
+ },
160
+ },
152
161
  {
153
162
  name: 'Delete',
154
163
  iconName: 'lucide:trash2',
@@ -308,6 +317,94 @@ export class OpsViewDomains extends DeesElement {
308
317
  });
309
318
  }
310
319
 
320
+ private async showMigrateDialog(domain: interfaces.data.IDomain) {
321
+ const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
322
+ const providers = this.domainsState.providers;
323
+
324
+ // Build target options based on current source
325
+ const targetOptions: { option: string; key: string }[] = [];
326
+ for (const p of providers) {
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 });
334
+ }
335
+
336
+ if (targetOptions.length === 0) {
337
+ DeesToast.show({
338
+ message: 'No migration targets available. Add a DNS provider first.',
339
+ type: 'warning',
340
+ duration: 3000,
341
+ });
342
+ return;
343
+ }
344
+
345
+ const currentLabel = domain.source === 'dcrouter'
346
+ ? 'DcRouter (self)'
347
+ : providers.find((p) => p.id === domain.providerId)?.name || 'Provider';
348
+
349
+ DeesModal.createAndShow({
350
+ heading: `Migrate: ${domain.name}`,
351
+ content: html`
352
+ <dees-form>
353
+ <dees-input-text
354
+ .key=${'currentSource'}
355
+ .label=${'Current source'}
356
+ .value=${currentLabel}
357
+ .disabled=${true}
358
+ ></dees-input-text>
359
+ <dees-input-dropdown
360
+ .key=${'target'}
361
+ .label=${'Migrate to'}
362
+ .description=${'Select the target DNS management'}
363
+ .options=${targetOptions}
364
+ .required=${true}
365
+ ></dees-input-dropdown>
366
+ <dees-input-checkbox
367
+ .key=${'deleteExisting'}
368
+ .label=${'Delete existing records at provider first'}
369
+ .description=${'Removes all records at the provider before pushing migrated records'}
370
+ .value=${true}
371
+ ></dees-input-checkbox>
372
+ </dees-form>
373
+ `,
374
+ menuOptions: [
375
+ { name: 'Cancel', action: async (m: any) => m.destroy() },
376
+ {
377
+ name: 'Migrate',
378
+ action: async (m: any) => {
379
+ const form = m.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
380
+ if (!form) return;
381
+ const data = await form.collectFormData();
382
+ const targetKey = typeof data.target === 'object' ? data.target.key : data.target;
383
+ if (!targetKey) return;
384
+
385
+ let targetSource: interfaces.data.TDomainSource;
386
+ let targetProviderId: string | undefined;
387
+ if (targetKey === 'dcrouter') {
388
+ targetSource = 'dcrouter';
389
+ } else {
390
+ targetSource = 'provider';
391
+ targetProviderId = targetKey.replace('provider:', '');
392
+ }
393
+
394
+ await appstate.domainsStatePart.dispatchAction(appstate.migrateDomainAction, {
395
+ id: domain.id,
396
+ targetSource,
397
+ targetProviderId,
398
+ deleteExistingProviderRecords: targetSource === 'provider' ? Boolean(data.deleteExisting) : false,
399
+ });
400
+ DeesToast.show({ message: `Domain ${domain.name} migrated successfully`, type: 'success', duration: 3000 });
401
+ m.destroy();
402
+ },
403
+ },
404
+ ],
405
+ });
406
+ }
407
+
311
408
  private async deleteDomain(domain: interfaces.data.IDomain) {
312
409
  const { DeesModal } = await import('@design.estate/dees-catalog');
313
410
  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();