@serve.zone/dcrouter 11.10.3 → 11.10.7

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 (76) hide show
  1. package/dist_serve/bundle.js +5102 -5102
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/cache/classes.cache.cleaner.js +1 -1
  4. package/dist_ts/cache/classes.cached.document.js +1 -1
  5. package/dist_ts/cache/classes.cachedb.js +1 -1
  6. package/dist_ts/cache/documents/classes.cached.email.js +1 -1
  7. package/dist_ts/cache/documents/classes.cached.ip.reputation.js +1 -1
  8. package/dist_ts/classes.dcrouter.js +3 -3
  9. package/dist_ts/config/classes.route-config-manager.d.ts +1 -1
  10. package/dist_ts/config/validator.js +1 -1
  11. package/dist_ts/errors/base.errors.js +2 -2
  12. package/dist_ts/monitoring/classes.metricsmanager.d.ts +6 -1
  13. package/dist_ts/monitoring/classes.metricsmanager.js +69 -31
  14. package/dist_ts/opsserver/classes.opsserver.js +2 -2
  15. package/dist_ts/opsserver/handlers/admin.handler.js +1 -1
  16. package/dist_ts/opsserver/handlers/certificate.handler.js +1 -1
  17. package/dist_ts/opsserver/handlers/radius.handler.js +1 -1
  18. package/dist_ts/opsserver/handlers/stats.handler.js +1 -1
  19. package/dist_ts/radius/classes.accounting.manager.js +1 -1
  20. package/dist_ts/radius/classes.radius.server.js +1 -1
  21. package/dist_ts/radius/classes.vlan.manager.js +1 -1
  22. package/dist_ts/security/classes.contentscanner.js +3 -3
  23. package/dist_ts/security/classes.ipreputationchecker.js +4 -4
  24. package/dist_ts/security/classes.securitylogger.js +5 -3
  25. package/dist_ts/sms/classes.smsservice.js +2 -2
  26. package/dist_ts/storage/classes.storagemanager.d.ts +1 -1
  27. package/dist_ts/storage/classes.storagemanager.js +2 -4
  28. package/dist_ts_web/00_commitinfo_data.js +1 -1
  29. package/dist_ts_web/appstate.js +9 -6
  30. package/dist_ts_web/elements/ops-dashboard.js +3 -2
  31. package/dist_ts_web/elements/ops-view-certificates.js +1 -1
  32. package/dist_ts_web/elements/ops-view-config.js +1 -1
  33. package/dist_ts_web/elements/ops-view-emails.js +1 -1
  34. package/dist_ts_web/elements/ops-view-network.js +1 -1
  35. package/dist_ts_web/elements/ops-view-remoteingress.js +1 -1
  36. package/dist_ts_web/router.js +1 -1
  37. package/license +21 -0
  38. package/package.json +15 -15
  39. package/readme.hints.md +1 -1
  40. package/readme.md +1 -1
  41. package/ts/00_commitinfo_data.ts +1 -1
  42. package/ts/cache/classes.cache.cleaner.ts +10 -10
  43. package/ts/cache/classes.cached.document.ts +1 -1
  44. package/ts/cache/classes.cachedb.ts +6 -6
  45. package/ts/cache/documents/classes.cached.email.ts +14 -14
  46. package/ts/cache/documents/classes.cached.ip.reputation.ts +10 -10
  47. package/ts/classes.dcrouter.ts +31 -31
  48. package/ts/config/validator.ts +5 -5
  49. package/ts/errors/base.errors.ts +1 -1
  50. package/ts/monitoring/classes.metricsmanager.ts +70 -33
  51. package/ts/opsserver/classes.opsserver.ts +13 -13
  52. package/ts/opsserver/handlers/admin.handler.ts +1 -1
  53. package/ts/opsserver/handlers/certificate.handler.ts +6 -6
  54. package/ts/opsserver/handlers/radius.handler.ts +4 -4
  55. package/ts/opsserver/handlers/stats.handler.ts +1 -1
  56. package/ts/radius/classes.accounting.manager.ts +10 -10
  57. package/ts/radius/classes.radius.server.ts +2 -2
  58. package/ts/radius/classes.vlan.manager.ts +5 -5
  59. package/ts/readme.md +1 -1
  60. package/ts/security/classes.contentscanner.ts +12 -12
  61. package/ts/security/classes.ipreputationchecker.ts +26 -26
  62. package/ts/security/classes.securitylogger.ts +6 -4
  63. package/ts/sms/classes.smsservice.ts +3 -3
  64. package/ts/storage/classes.storagemanager.ts +23 -25
  65. package/ts_apiclient/readme.md +1 -1
  66. package/ts_web/00_commitinfo_data.ts +1 -1
  67. package/ts_web/appstate.ts +136 -133
  68. package/ts_web/elements/ops-dashboard.ts +15 -14
  69. package/ts_web/elements/ops-view-certificates.ts +10 -10
  70. package/ts_web/elements/ops-view-config.ts +8 -8
  71. package/ts_web/elements/ops-view-emails.ts +2 -2
  72. package/ts_web/elements/ops-view-network.ts +2 -2
  73. package/ts_web/elements/ops-view-remoteingress.ts +6 -6
  74. package/ts_web/readme.md +1 -1
  75. package/ts_web/router.ts +3 -3
  76. /package/{npmextra.json → .smartconfig.json} +0 -0
@@ -35,55 +35,55 @@ export class CachedEmail extends CachedDocument<CachedEmail> {
35
35
  */
36
36
  @plugins.smartdata.unI()
37
37
  @plugins.smartdata.svDb()
38
- public id: string;
38
+ public id!: string;
39
39
 
40
40
  /**
41
41
  * Email message ID (RFC 822 Message-ID header)
42
42
  */
43
43
  @plugins.smartdata.svDb()
44
- public messageId: string;
44
+ public messageId!: string;
45
45
 
46
46
  /**
47
47
  * Sender email address (envelope from)
48
48
  */
49
49
  @plugins.smartdata.svDb()
50
- public from: string;
50
+ public from!: string;
51
51
 
52
52
  /**
53
53
  * Recipient email addresses
54
54
  */
55
55
  @plugins.smartdata.svDb()
56
- public to: string[];
56
+ public to!: string[];
57
57
 
58
58
  /**
59
59
  * CC recipients
60
60
  */
61
61
  @plugins.smartdata.svDb()
62
- public cc: string[];
62
+ public cc!: string[];
63
63
 
64
64
  /**
65
65
  * BCC recipients
66
66
  */
67
67
  @plugins.smartdata.svDb()
68
- public bcc: string[];
68
+ public bcc!: string[];
69
69
 
70
70
  /**
71
71
  * Email subject
72
72
  */
73
73
  @plugins.smartdata.svDb()
74
- public subject: string;
74
+ public subject!: string;
75
75
 
76
76
  /**
77
77
  * Raw RFC822 email content
78
78
  */
79
79
  @plugins.smartdata.svDb()
80
- public rawContent: string;
80
+ public rawContent!: string;
81
81
 
82
82
  /**
83
83
  * Current status of the email
84
84
  */
85
85
  @plugins.smartdata.svDb()
86
- public status: TCachedEmailStatus;
86
+ public status!: TCachedEmailStatus;
87
87
 
88
88
  /**
89
89
  * Number of delivery attempts
@@ -101,25 +101,25 @@ export class CachedEmail extends CachedDocument<CachedEmail> {
101
101
  * Timestamp for next delivery attempt
102
102
  */
103
103
  @plugins.smartdata.svDb()
104
- public nextAttempt: Date;
104
+ public nextAttempt!: Date;
105
105
 
106
106
  /**
107
107
  * Last error message if delivery failed
108
108
  */
109
109
  @plugins.smartdata.svDb()
110
- public lastError: string;
110
+ public lastError!: string;
111
111
 
112
112
  /**
113
113
  * Timestamp when the email was successfully delivered
114
114
  */
115
115
  @plugins.smartdata.svDb()
116
- public deliveredAt: Date;
116
+ public deliveredAt!: Date;
117
117
 
118
118
  /**
119
119
  * Sender domain (for querying/filtering)
120
120
  */
121
121
  @plugins.smartdata.svDb()
122
- public senderDomain: string;
122
+ public senderDomain!: string;
123
123
 
124
124
  /**
125
125
  * Priority level (higher = more important)
@@ -131,7 +131,7 @@ export class CachedEmail extends CachedDocument<CachedEmail> {
131
131
  * JSON-serialized route data
132
132
  */
133
133
  @plugins.smartdata.svDb()
134
- public routeData: string;
134
+ public routeData!: string;
135
135
 
136
136
  /**
137
137
  * DKIM signature status
@@ -45,61 +45,61 @@ export class CachedIPReputation extends CachedDocument<CachedIPReputation> {
45
45
  */
46
46
  @plugins.smartdata.unI()
47
47
  @plugins.smartdata.svDb()
48
- public ipAddress: string;
48
+ public ipAddress!: string;
49
49
 
50
50
  /**
51
51
  * Reputation score (0-100, higher = better)
52
52
  */
53
53
  @plugins.smartdata.svDb()
54
- public score: number;
54
+ public score!: number;
55
55
 
56
56
  /**
57
57
  * Whether the IP is flagged as spam source
58
58
  */
59
59
  @plugins.smartdata.svDb()
60
- public isSpam: boolean;
60
+ public isSpam!: boolean;
61
61
 
62
62
  /**
63
63
  * Whether the IP is a known proxy
64
64
  */
65
65
  @plugins.smartdata.svDb()
66
- public isProxy: boolean;
66
+ public isProxy!: boolean;
67
67
 
68
68
  /**
69
69
  * Whether the IP is a Tor exit node
70
70
  */
71
71
  @plugins.smartdata.svDb()
72
- public isTor: boolean;
72
+ public isTor!: boolean;
73
73
 
74
74
  /**
75
75
  * Whether the IP is a VPN endpoint
76
76
  */
77
77
  @plugins.smartdata.svDb()
78
- public isVPN: boolean;
78
+ public isVPN!: boolean;
79
79
 
80
80
  /**
81
81
  * Country code (ISO 3166-1 alpha-2)
82
82
  */
83
83
  @plugins.smartdata.svDb()
84
- public country: string;
84
+ public country!: string;
85
85
 
86
86
  /**
87
87
  * Autonomous System Number
88
88
  */
89
89
  @plugins.smartdata.svDb()
90
- public asn: string;
90
+ public asn!: string;
91
91
 
92
92
  /**
93
93
  * Organization name
94
94
  */
95
95
  @plugins.smartdata.svDb()
96
- public org: string;
96
+ public org!: string;
97
97
 
98
98
  /**
99
99
  * List of blacklists the IP appears on
100
100
  */
101
101
  @plugins.smartdata.svDb()
102
- public blacklists: string[];
102
+ public blacklists!: string[];
103
103
 
104
104
  /**
105
105
  * Number of times this IP has been checked
@@ -215,7 +215,7 @@ export class DcRouter {
215
215
  public emailServer?: UnifiedEmailServer;
216
216
  public radiusServer?: RadiusServer;
217
217
  public storageManager: StorageManager;
218
- public opsServer: OpsServer;
218
+ public opsServer!: OpsServer;
219
219
  public metricsManager?: MetricsManager;
220
220
 
221
221
  // Cache system (smartdata + LocalTsmDb)
@@ -448,7 +448,7 @@ export class DcRouter {
448
448
  }
449
449
 
450
450
  // DNS Server: optional, depends on SmartProxy
451
- if (this.options.dnsNsDomains?.length > 0 && this.options.dnsScopes?.length > 0) {
451
+ if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0 && this.options.dnsScopes && this.options.dnsScopes.length > 0) {
452
452
  this.serviceManager.addService(
453
453
  new plugins.taskbuffer.Service('DnsServer')
454
454
  .optional()
@@ -787,7 +787,7 @@ export class DcRouter {
787
787
  eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
788
788
  eventComms.setSource('smartacme-dns-01');
789
789
  const isWildcardDomain = domain.startsWith('*.');
790
- const cert = await this.smartAcme.getCertificateForDomain(domain, {
790
+ const cert = await this.smartAcme!.getCertificateForDomain(domain, {
791
791
  includeWildcard: !isWildcardDomain,
792
792
  });
793
793
  if (cert.validUntil) {
@@ -806,10 +806,10 @@ export class DcRouter {
806
806
  // Success — clear any backoff
807
807
  await scheduler.clearBackoff(domain);
808
808
  return result;
809
- } catch (err) {
809
+ } catch (err: unknown) {
810
810
  // Record failure for backoff tracking
811
- await scheduler.recordFailure(domain, err.message);
812
- eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${err.message}, falling back to http-01`);
811
+ await scheduler.recordFailure(domain, (err as Error).message);
812
+ eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${(err as Error).message}, falling back to http-01`);
813
813
  return 'http01';
814
814
  }
815
815
  };
@@ -1248,21 +1248,21 @@ export class DcRouter {
1248
1248
  // Wire delivery events to MetricsManager and logger
1249
1249
  if (this.metricsManager && this.emailServer.deliverySystem) {
1250
1250
  this.emailServer.deliverySystem.on('deliveryStart', (item: any) => {
1251
- this.metricsManager.trackEmailReceived(item?.from);
1251
+ this.metricsManager!.trackEmailReceived(item?.from);
1252
1252
  logger.log('info', `Email delivery started: ${item?.from} → ${item?.to}`, { zone: 'email' });
1253
1253
  });
1254
1254
  this.emailServer.deliverySystem.on('deliverySuccess', (item: any) => {
1255
- this.metricsManager.trackEmailSent(item?.to);
1255
+ this.metricsManager!.trackEmailSent(item?.to);
1256
1256
  logger.log('info', `Email delivered to ${item?.to}`, { zone: 'email' });
1257
1257
  });
1258
1258
  this.emailServer.deliverySystem.on('deliveryFailed', (item: any, error: any) => {
1259
- this.metricsManager.trackEmailFailed(item?.to, error?.message);
1259
+ this.metricsManager!.trackEmailFailed(item?.to, error?.message);
1260
1260
  logger.log('warn', `Email delivery failed to ${item?.to}: ${error?.message}`, { zone: 'email' });
1261
1261
  });
1262
1262
  }
1263
1263
  if (this.metricsManager && this.emailServer) {
1264
1264
  this.emailServer.on('bounceProcessed', () => {
1265
- this.metricsManager.trackEmailBounced();
1265
+ this.metricsManager!.trackEmailBounced();
1266
1266
  logger.log('warn', 'Email bounce processed', { zone: 'email' });
1267
1267
  });
1268
1268
  }
@@ -1305,12 +1305,12 @@ export class DcRouter {
1305
1305
  }
1306
1306
 
1307
1307
  logger.log('info', 'All unified email components stopped');
1308
- } catch (error) {
1309
- logger.log('error', `Error stopping unified email components: ${error.message}`);
1308
+ } catch (error: unknown) {
1309
+ logger.log('error', `Error stopping unified email components: ${(error as Error).message}`);
1310
1310
  throw error;
1311
1311
  }
1312
1312
  }
1313
-
1313
+
1314
1314
  /**
1315
1315
  * Update domain rules for email routing
1316
1316
  * @param rules New domain rules to apply
@@ -1468,7 +1468,7 @@ export class DcRouter {
1468
1468
  this.dnsServer.on('query', (event: plugins.smartdns.dnsServerMod.IDnsQueryCompletedEvent) => {
1469
1469
  // Metrics tracking
1470
1470
  for (const question of event.questions) {
1471
- this.metricsManager.trackDnsQuery(
1471
+ this.metricsManager?.trackDnsQuery(
1472
1472
  question.type,
1473
1473
  question.name,
1474
1474
  false,
@@ -1553,8 +1553,8 @@ export class DcRouter {
1553
1553
  // Use the built-in socket handler from smartdns
1554
1554
  // This handles HTTP/2, DoH protocol, etc.
1555
1555
  await (this.dnsServer as any).handleHttpsSocket(socket);
1556
- } catch (error) {
1557
- logger.log('error', `DNS socket handler error: ${error.message}`);
1556
+ } catch (error: unknown) {
1557
+ logger.log('error', `DNS socket handler error: ${(error as Error).message}`);
1558
1558
  if (!socket.destroyed) {
1559
1559
  socket.destroy();
1560
1560
  }
@@ -1695,14 +1695,14 @@ export class DcRouter {
1695
1695
  } else {
1696
1696
  logger.log('warn', `Invalid DKIM record structure in ${file}`);
1697
1697
  }
1698
- } catch (error) {
1699
- logger.log('error', `Failed to load DKIM record from ${file}: ${error.message}`);
1698
+ } catch (error: unknown) {
1699
+ logger.log('error', `Failed to load DKIM record from ${file}: ${(error as Error).message}`);
1700
1700
  }
1701
1701
  }
1702
- } catch (error) {
1703
- logger.log('error', `Failed to load DKIM records: ${error.message}`);
1702
+ } catch (error: unknown) {
1703
+ logger.log('error', `Failed to load DKIM records: ${(error as Error).message}`);
1704
1704
  }
1705
-
1705
+
1706
1706
  return records;
1707
1707
  }
1708
1708
 
@@ -1734,11 +1734,11 @@ export class DcRouter {
1734
1734
  // This ensures keys are ready even if DNS mode changes later
1735
1735
  await dkimCreator.handleDKIMKeysForDomain(domainConfig.domain);
1736
1736
  logger.log('info', `DKIM keys initialized for ${domainConfig.domain}`);
1737
- } catch (error) {
1738
- logger.log('error', `Failed to initialize DKIM for ${domainConfig.domain}: ${error.message}`);
1737
+ } catch (error: unknown) {
1738
+ logger.log('error', `Failed to initialize DKIM for ${domainConfig.domain}: ${(error as Error).message}`);
1739
1739
  }
1740
1740
  }
1741
-
1741
+
1742
1742
  logger.log('info', 'DKIM initialization complete');
1743
1743
  }
1744
1744
 
@@ -1779,10 +1779,10 @@ export class DcRouter {
1779
1779
  } else {
1780
1780
  logger.log('warn', 'Could not auto-discover public IPv4 address');
1781
1781
  }
1782
- } catch (error) {
1783
- logger.log('error', `Failed to auto-discover public IP: ${error.message}`);
1782
+ } catch (error: unknown) {
1783
+ logger.log('error', `Failed to auto-discover public IP: ${(error as Error).message}`);
1784
1784
  }
1785
-
1785
+
1786
1786
  if (!publicIp) {
1787
1787
  logger.log('warn', 'No public IP available. Nameserver A records require either proxyIps, publicIp, or successful auto-discovery.');
1788
1788
  }
@@ -1876,8 +1876,8 @@ export class DcRouter {
1876
1876
  }
1877
1877
 
1878
1878
  return null;
1879
- } catch (error) {
1880
- logger.log('warn', `Failed to detect public IP: ${error.message}`);
1879
+ } catch (error: unknown) {
1880
+ logger.log('warn', `Failed to detect public IP: ${(error as Error).message}`);
1881
1881
  return null;
1882
1882
  }
1883
1883
  }
@@ -1911,8 +1911,8 @@ export class DcRouter {
1911
1911
  const keyPem = plugins.fs.readFileSync(riCfg.tls.keyPath, 'utf8');
1912
1912
  tlsConfig = { certPem, keyPem };
1913
1913
  logger.log('info', 'Using explicit TLS cert/key for RemoteIngress tunnel');
1914
- } catch (err) {
1915
- logger.log('warn', `Failed to read RemoteIngress TLS cert/key files: ${err.message}`);
1914
+ } catch (err: unknown) {
1915
+ logger.log('warn', `Failed to read RemoteIngress TLS cert/key files: ${(err as Error).message}`);
1916
1916
  }
1917
1917
  }
1918
1918
 
@@ -170,7 +170,7 @@ export class ConfigValidator {
170
170
  } else if (rules.items.schema && itemType === 'object') {
171
171
  const itemResult = this.validate(value[i], rules.items.schema);
172
172
  if (!itemResult.valid) {
173
- errors.push(...itemResult.errors.map(err => `${key}[${i}].${err}`));
173
+ errors.push(...itemResult.errors!.map(err => `${key}[${i}].${err}`));
174
174
  }
175
175
  }
176
176
  }
@@ -181,7 +181,7 @@ export class ConfigValidator {
181
181
  if (rules.schema) {
182
182
  const nestedResult = this.validate(value, rules.schema);
183
183
  if (!nestedResult.valid) {
184
- errors.push(...nestedResult.errors.map(err => `${key}.${err}`));
184
+ errors.push(...nestedResult.errors!.map(err => `${key}.${err}`));
185
185
  }
186
186
  validatedConfig[key] = nestedResult.config;
187
187
  }
@@ -233,8 +233,8 @@ export class ConfigValidator {
233
233
 
234
234
  // Apply defaults to array items
235
235
  if (result[key] && rules.type === 'array' && rules.items && rules.items.schema) {
236
- result[key] = result[key].map(item =>
237
- typeof item === 'object' ? this.applyDefaults(item, rules.items.schema) : item
236
+ result[key] = result[key].map(item =>
237
+ typeof item === 'object' ? this.applyDefaults(item, rules.items!.schema!) : item
238
238
  );
239
239
  }
240
240
  }
@@ -255,7 +255,7 @@ export class ConfigValidator {
255
255
 
256
256
  if (!result.valid) {
257
257
  throw new ValidationError(
258
- `Configuration validation failed: ${result.errors.join(', ')}`,
258
+ `Configuration validation failed: ${result.errors!.join(', ')}`,
259
259
  'CONFIG_VALIDATION_ERROR',
260
260
  { data: { errors: result.errors } }
261
261
  );
@@ -227,7 +227,7 @@ export class PlatformError extends Error {
227
227
  const { retry } = this.context;
228
228
  if (!retry) return false;
229
229
 
230
- return retry.currentRetry < retry.maxRetries;
230
+ return (retry.currentRetry ?? 0) < (retry.maxRetries ?? 0);
231
231
  }
232
232
 
233
233
  /**
@@ -296,11 +296,11 @@ export class MetricsManager {
296
296
  const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
297
297
 
298
298
  if (!proxyMetrics) {
299
- return [];
299
+ return [] as Array<{ type: string; count: number; source: string; lastActivity: Date }>;
300
300
  }
301
-
301
+
302
302
  const connectionsByRoute = proxyMetrics.connections.byRoute();
303
- const connectionInfo = [];
303
+ const connectionInfo: Array<{ type: string; count: number; source: string; lastActivity: Date }> = [];
304
304
 
305
305
  for (const [routeName, count] of connectionsByRoute) {
306
306
  connectionInfo.push({
@@ -595,47 +595,84 @@ export class MetricsManager {
595
595
  const backendMetrics = proxyMetrics.backends.byBackend();
596
596
  const protocolCache = proxyMetrics.backends.detectedProtocols();
597
597
 
598
- // Index protocol cache by "host:port"
599
- const cacheByKey = new Map<string, (typeof protocolCache)[number]>();
598
+ // Group protocol cache entries by host:port so we can match them to backend metrics.
599
+ // The protocol cache is keyed by (host, port, domain) in Rust, so the same host:port
600
+ // can have multiple entries for different domains.
601
+ const cacheByBackend = new Map<string, (typeof protocolCache)[number][]>();
600
602
  for (const entry of protocolCache) {
601
- cacheByKey.set(`${entry.host}:${entry.port}`, entry);
603
+ const backendKey = `${entry.host}:${entry.port}`;
604
+ let entries = cacheByBackend.get(backendKey);
605
+ if (!entries) {
606
+ entries = [];
607
+ cacheByBackend.set(backendKey, entries);
608
+ }
609
+ entries.push(entry);
602
610
  }
603
611
 
604
612
  const backends: Array<any> = [];
605
- const seen = new Set<string>();
613
+ const seenCacheKeys = new Set<string>();
606
614
 
607
615
  for (const [key, bm] of backendMetrics) {
608
- seen.add(key);
609
- const cache = cacheByKey.get(key);
610
- backends.push({
611
- backend: key,
612
- domain: cache?.domain ?? null,
613
- protocol: bm.protocol,
614
- activeConnections: bm.activeConnections,
615
- totalConnections: bm.totalConnections,
616
- connectErrors: bm.connectErrors,
617
- handshakeErrors: bm.handshakeErrors,
618
- requestErrors: bm.requestErrors,
619
- avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
620
- poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
621
- h2Failures: bm.h2Failures,
622
- h2Suppressed: cache?.h2Suppressed ?? false,
623
- h3Suppressed: cache?.h3Suppressed ?? false,
624
- h2CooldownRemainingSecs: cache?.h2CooldownRemainingSecs ?? null,
625
- h3CooldownRemainingSecs: cache?.h3CooldownRemainingSecs ?? null,
626
- h2ConsecutiveFailures: cache?.h2ConsecutiveFailures ?? null,
627
- h3ConsecutiveFailures: cache?.h3ConsecutiveFailures ?? null,
628
- h3Port: cache?.h3Port ?? null,
629
- cacheAgeSecs: cache?.ageSecs ?? null,
630
- });
616
+ const cacheEntries = cacheByBackend.get(key);
617
+ if (!cacheEntries || cacheEntries.length === 0) {
618
+ // No protocol cache entry — emit one row with backend metrics only
619
+ backends.push({
620
+ backend: key,
621
+ domain: null,
622
+ protocol: bm.protocol,
623
+ activeConnections: bm.activeConnections,
624
+ totalConnections: bm.totalConnections,
625
+ connectErrors: bm.connectErrors,
626
+ handshakeErrors: bm.handshakeErrors,
627
+ requestErrors: bm.requestErrors,
628
+ avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
629
+ poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
630
+ h2Failures: bm.h2Failures,
631
+ h2Suppressed: false,
632
+ h3Suppressed: false,
633
+ h2CooldownRemainingSecs: null,
634
+ h3CooldownRemainingSecs: null,
635
+ h2ConsecutiveFailures: null,
636
+ h3ConsecutiveFailures: null,
637
+ h3Port: null,
638
+ cacheAgeSecs: null,
639
+ });
640
+ } else {
641
+ // One row per domain, each enriched with the shared backend metrics
642
+ for (const cache of cacheEntries) {
643
+ const compositeKey = `${cache.host}:${cache.port}:${cache.domain ?? ''}`;
644
+ seenCacheKeys.add(compositeKey);
645
+ backends.push({
646
+ backend: key,
647
+ domain: cache.domain ?? null,
648
+ protocol: cache.protocol ?? bm.protocol,
649
+ activeConnections: bm.activeConnections,
650
+ totalConnections: bm.totalConnections,
651
+ connectErrors: bm.connectErrors,
652
+ handshakeErrors: bm.handshakeErrors,
653
+ requestErrors: bm.requestErrors,
654
+ avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
655
+ poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
656
+ h2Failures: bm.h2Failures,
657
+ h2Suppressed: cache.h2Suppressed,
658
+ h3Suppressed: cache.h3Suppressed,
659
+ h2CooldownRemainingSecs: cache.h2CooldownRemainingSecs,
660
+ h3CooldownRemainingSecs: cache.h3CooldownRemainingSecs,
661
+ h2ConsecutiveFailures: cache.h2ConsecutiveFailures,
662
+ h3ConsecutiveFailures: cache.h3ConsecutiveFailures,
663
+ h3Port: cache.h3Port,
664
+ cacheAgeSecs: cache.ageSecs,
665
+ });
666
+ }
667
+ }
631
668
  }
632
669
 
633
670
  // Include protocol cache entries with no matching backend metric
634
671
  for (const entry of protocolCache) {
635
- const key = `${entry.host}:${entry.port}`;
636
- if (!seen.has(key)) {
672
+ const compositeKey = `${entry.host}:${entry.port}:${entry.domain ?? ''}`;
673
+ if (!seenCacheKeys.has(compositeKey)) {
637
674
  backends.push({
638
- backend: key,
675
+ backend: `${entry.host}:${entry.port}`,
639
676
  domain: entry.domain,
640
677
  protocol: entry.protocol,
641
678
  activeConnections: 0,
@@ -7,7 +7,7 @@ import { requireValidIdentity, requireAdminIdentity } from './helpers/guards.js'
7
7
 
8
8
  export class OpsServer {
9
9
  public dcRouterRef: DcRouter;
10
- public server: plugins.typedserver.utilityservers.UtilityWebsiteServer;
10
+ public server!: plugins.typedserver.utilityservers.UtilityWebsiteServer;
11
11
 
12
12
  // Main TypedRouter — unauthenticated endpoints (login/logout/verify) and own-auth handlers
13
13
  public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -17,17 +17,17 @@ export class OpsServer {
17
17
  public adminRouter = new plugins.typedrequest.TypedRouter<{ request: { identity: interfaces.data.IIdentity } }>();
18
18
 
19
19
  // Handler instances
20
- public adminHandler: handlers.AdminHandler;
21
- private configHandler: handlers.ConfigHandler;
22
- private logsHandler: handlers.LogsHandler;
23
- private securityHandler: handlers.SecurityHandler;
24
- private statsHandler: handlers.StatsHandler;
25
- private radiusHandler: handlers.RadiusHandler;
26
- private emailOpsHandler: handlers.EmailOpsHandler;
27
- private certificateHandler: handlers.CertificateHandler;
28
- private remoteIngressHandler: handlers.RemoteIngressHandler;
29
- private routeManagementHandler: handlers.RouteManagementHandler;
30
- private apiTokenHandler: handlers.ApiTokenHandler;
20
+ public adminHandler!: handlers.AdminHandler;
21
+ private configHandler!: handlers.ConfigHandler;
22
+ private logsHandler!: handlers.LogsHandler;
23
+ private securityHandler!: handlers.SecurityHandler;
24
+ private statsHandler!: handlers.StatsHandler;
25
+ private radiusHandler!: handlers.RadiusHandler;
26
+ private emailOpsHandler!: handlers.EmailOpsHandler;
27
+ private certificateHandler!: handlers.CertificateHandler;
28
+ private remoteIngressHandler!: handlers.RemoteIngressHandler;
29
+ private routeManagementHandler!: handlers.RouteManagementHandler;
30
+ private apiTokenHandler!: handlers.ApiTokenHandler;
31
31
 
32
32
  constructor(dcRouterRefArg: DcRouter) {
33
33
  this.dcRouterRef = dcRouterRefArg;
@@ -39,7 +39,7 @@ export class OpsServer {
39
39
  public async start() {
40
40
  this.server = new plugins.typedserver.utilityservers.UtilityWebsiteServer({
41
41
  domain: 'localhost',
42
- feedMetadata: null,
42
+ feedMetadata: undefined,
43
43
  serveDir: paths.distServe,
44
44
  });
45
45
 
@@ -12,7 +12,7 @@ export class AdminHandler {
12
12
  public typedrouter = new plugins.typedrequest.TypedRouter();
13
13
 
14
14
  // JWT instance
15
- public smartjwtInstance: plugins.smartjwt.SmartJwt<IJwtData>;
15
+ public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
16
16
 
17
17
  // Simple in-memory user storage (in production, use proper database)
18
18
  private users = new Map<string, {
@@ -311,8 +311,8 @@ export class CertificateHandler {
311
311
  }
312
312
  }
313
313
  return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
314
- } catch (err) {
315
- return { success: false, message: err.message || 'Failed to reprovision certificate' };
314
+ } catch (err: unknown) {
315
+ return { success: false, message: (err as Error).message || 'Failed to reprovision certificate' };
316
316
  }
317
317
  }
318
318
 
@@ -340,8 +340,8 @@ export class CertificateHandler {
340
340
  try {
341
341
  await dcRouter.smartAcme.getCertificateForDomain(domain);
342
342
  return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}'` };
343
- } catch (err) {
344
- return { success: false, message: err.message || `Failed to reprovision certificate for ${domain}` };
343
+ } catch (err: unknown) {
344
+ return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
345
345
  }
346
346
  }
347
347
 
@@ -351,8 +351,8 @@ export class CertificateHandler {
351
351
  try {
352
352
  await smartProxy.provisionCertificate(routeNames[0]);
353
353
  return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}' via route '${routeNames[0]}'` };
354
- } catch (err) {
355
- return { success: false, message: err.message || `Failed to reprovision certificate for ${domain}` };
354
+ } catch (err: unknown) {
355
+ return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
356
356
  }
357
357
  }
358
358
 
@@ -52,8 +52,8 @@ export class RadiusHandler {
52
52
  try {
53
53
  await radiusServer.addClient(dataArg.client);
54
54
  return { success: true };
55
- } catch (error) {
56
- return { success: false, message: error.message };
55
+ } catch (error: unknown) {
56
+ return { success: false, message: (error as Error).message };
57
57
  }
58
58
  }
59
59
  )
@@ -144,8 +144,8 @@ export class RadiusHandler {
144
144
  updatedAt: mapping.updatedAt,
145
145
  },
146
146
  };
147
- } catch (error) {
148
- return { success: false, message: error.message };
147
+ } catch (error: unknown) {
148
+ return { success: false, message: (error as Error).message };
149
149
  }
150
150
  }
151
151
  )
@@ -279,7 +279,7 @@ export class StatsHandler {
279
279
  if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) {
280
280
  promises.push(
281
281
  (async () => {
282
- const stats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
282
+ const stats = await this.opsServerRef.dcRouterRef.metricsManager!.getNetworkStats();
283
283
  const serverStats = await this.collectServerStats();
284
284
 
285
285
  // Build per-IP bandwidth lookup from throughputByIP