diff --git a/changelog.md b/changelog.md index 5214520..220ffe0 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2026-02-13 - 5.4.1 - fix(network,dcrouter) +Always register SmartProxy certificate event handlers and include total bytes + improved connection metrics in network stats/UI + +- Always register SmartProxy 'certificate-issued', 'certificate-renewed', and 'certificate-failed' handlers (previously only registered when acmeConfig was present) so certificate events are processed regardless of provisioning path. +- Add totalBytes (in/out) to network stats and propagate it through ts_interfaces and app state so total data transferred is available to the UI. +- Combine metricsManager.getNetworkStats with collectServerStats to compute activeConnections and adjust connectionDetails/TopEndpoints handling. +- Update ops UI to display totalBytes in throughput cards and remove a redundant network-specific auto-refresh fetch. +- Type and state updates: ts_interfaces/data/stats.ts and ts_web/appstate.ts updated with totalBytes and initialization/default mapping adjusted. + ## 2026-02-13 - 5.4.0 - feat(certificates) include certificate source/issuer and Rust-side status checks; pass eventComms into certProvisionFunction and record expiry information diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index b2f8ed3..e65a7c1 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '5.4.0', + version: '5.4.1', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts index 14f2f00..30fcead 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -491,42 +491,41 @@ export class DcRouter { console.error('[DcRouter] Error stack:', err.stack); }); - if (acmeConfig) { - this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => { - console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`); - const routeName = this.findRouteNameForDomain(event.domain); - if (routeName) { - this.certificateStatusMap.set(routeName, { - status: 'valid', domain: event.domain, - expiryDate: event.expiryDate, issuedAt: new Date().toISOString(), - source: event.source, - }); - } - }); + // Always listen for certificate events — emitted by both ACME and certProvisionFunction paths + this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => { + console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`); + const routeName = this.findRouteNameForDomain(event.domain); + if (routeName) { + this.certificateStatusMap.set(routeName, { + status: 'valid', domain: event.domain, + expiryDate: event.expiryDate, issuedAt: new Date().toISOString(), + source: event.source, + }); + } + }); - this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => { - console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`); - const routeName = this.findRouteNameForDomain(event.domain); - if (routeName) { - this.certificateStatusMap.set(routeName, { - status: 'valid', domain: event.domain, - expiryDate: event.expiryDate, issuedAt: new Date().toISOString(), - source: event.source, - }); - } - }); + this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => { + console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`); + const routeName = this.findRouteNameForDomain(event.domain); + if (routeName) { + this.certificateStatusMap.set(routeName, { + status: 'valid', domain: event.domain, + expiryDate: event.expiryDate, issuedAt: new Date().toISOString(), + source: event.source, + }); + } + }); - this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => { - console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error); - const routeName = this.findRouteNameForDomain(event.domain); - if (routeName) { - this.certificateStatusMap.set(routeName, { - status: 'failed', domain: event.domain, error: event.error, - source: event.source, - }); - } - }); - } + this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => { + console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error); + const routeName = this.findRouteNameForDomain(event.domain); + if (routeName) { + this.certificateStatusMap.set(routeName, { + status: 'failed', domain: event.domain, error: event.error, + source: event.source, + }); + } + }); // Start SmartProxy console.log('[DcRouter] Starting SmartProxy...'); diff --git a/ts/opsserver/handlers/stats.handler.ts b/ts/opsserver/handlers/stats.handler.ts index 0421611..de15e71 100644 --- a/ts/opsserver/handlers/stats.handler.ts +++ b/ts/opsserver/handlers/stats.handler.ts @@ -251,26 +251,21 @@ export class StatsHandler { if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) { promises.push( - this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats().then(stats => { - const connectionDetails: interfaces.data.IConnectionDetails[] = []; - stats.connectionsByIP.forEach((count, ip) => { - connectionDetails.push({ - remoteAddress: ip, - protocol: 'https' as any, - state: 'established' as any, - startTime: Date.now(), - bytesIn: 0, - bytesOut: 0, - }); - }); - + (async () => { + const stats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats(); + const serverStats = await this.collectServerStats(); + metrics.network = { totalBandwidth: { in: stats.throughputRate.bytesInPerSecond, out: stats.throughputRate.bytesOutPerSecond, }, - activeConnections: stats.connectionsByIP.size, - connectionDetails: connectionDetails.slice(0, 50), // Limit to 50 connections + totalBytes: { + in: stats.totalDataTransferred.bytesIn, + out: stats.totalDataTransferred.bytesOut, + }, + activeConnections: serverStats.activeConnections, + connectionDetails: [], topEndpoints: stats.topIPs.map(ip => ({ endpoint: ip.ip, requests: ip.count, @@ -280,7 +275,7 @@ export class StatsHandler { }, })), }; - }) + })() ); } diff --git a/ts_interfaces/data/stats.ts b/ts_interfaces/data/stats.ts index 71e867f..a8c53cb 100644 --- a/ts_interfaces/data/stats.ts +++ b/ts_interfaces/data/stats.ts @@ -116,6 +116,10 @@ export interface INetworkMetrics { in: number; out: number; }; + totalBytes?: { + in: number; + out: number; + }; activeConnections: number; connectionDetails: IConnectionDetails[]; topEndpoints: Array<{ diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index b2f8ed3..e65a7c1 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '5.4.0', + version: '5.4.1', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index 42e130d..5ed0d51 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -47,6 +47,7 @@ export interface INetworkState { connections: interfaces.data.IConnectionInfo[]; connectionsByIP: { [ip: string]: number }; throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number }; + totalBytes: { in: number; out: number }; topIPs: Array<{ ip: string; count: number }>; lastUpdated: number; isLoading: boolean; @@ -144,6 +145,7 @@ export const networkStatePart = await appState.getStatePart( connections: [], connectionsByIP: {}, throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, + totalBytes: { in: 0, out: 0 }, topIPs: [], lastUpdated: 0, isLoading: false, @@ -421,6 +423,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat connections: connectionsResponse.connections, connectionsByIP, throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, + totalBytes: networkStatsResponse.totalDataTransferred + ? { in: networkStatsResponse.totalDataTransferred.bytesIn, out: networkStatsResponse.totalDataTransferred.bytesOut } + : { in: 0, out: 0 }, topIPs: networkStatsResponse.topIPs || [], lastUpdated: Date.now(), isLoading: false, @@ -790,6 +795,7 @@ async function dispatchCombinedRefreshAction() { bytesInPerSecond: network.totalBandwidth.in, bytesOutPerSecond: network.totalBandwidth.out }, + totalBytes: network.totalBytes || { in: 0, out: 0 }, topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })), lastUpdated: Date.now(), isLoading: false, @@ -805,6 +811,7 @@ async function dispatchCombinedRefreshAction() { bytesInPerSecond: network.totalBandwidth.in, bytesOutPerSecond: network.totalBandwidth.out }, + totalBytes: network.totalBytes || { in: 0, out: 0 }, topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })), lastUpdated: Date.now(), isLoading: false, @@ -845,13 +852,6 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar refreshInterval = setInterval(() => { // Use combined refresh action for efficiency dispatchCombinedRefreshAction(); - - // If network view is active, also ensure we have fresh network data - const currentView = uiStatePart.getState().activeView; - if (currentView === 'network') { - // Network view needs more frequent updates, fetch directly - networkStatePart.dispatchAction(fetchNetworkStatsAction, null); - } }, uiState.refreshInterval); } } else { diff --git a/ts_web/elements/ops-view-network.ts b/ts_web/elements/ops-view-network.ts index 94643af..9e507a5 100644 --- a/ts_web/elements/ops-view-network.ts +++ b/ts_web/elements/ops-view-network.ts @@ -426,6 +426,7 @@ export class OpsViewNetwork extends DeesElement { type: 'number', icon: 'download', color: '#22c55e', + description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`, }, { id: 'throughputOut', @@ -435,6 +436,7 @@ export class OpsViewNetwork extends DeesElement { type: 'number', icon: 'upload', color: '#8b5cf6', + description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`, }, ];