Compare commits

...

2 Commits

Author SHA1 Message Date
c33ecdc26f v5.4.1
Some checks failed
Docker (tags) / security (push) Has been cancelled
Docker (tags) / test (push) Has been cancelled
Docker (tags) / release (push) Has been cancelled
Docker (tags) / metadata (push) Has been cancelled
2026-02-13 22:03:23 +00:00
b033d80927 fix(network,dcrouter): Always register SmartProxy certificate event handlers and include total bytes + improved connection metrics in network stats/UI 2026-02-13 22:03:23 +00:00
9 changed files with 69 additions and 60 deletions

View File

@@ -1,5 +1,14 @@
# Changelog # 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) ## 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 include certificate source/issuer and Rust-side status checks; pass eventComms into certProvisionFunction and record expiry information

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "5.4.0", "version": "5.4.1",
"description": "A multifaceted routing service handling mail and SMS delivery functions.", "description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module", "type": "module",
"exports": { "exports": {

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/dcrouter', name: '@serve.zone/dcrouter',
version: '5.4.0', version: '5.4.1',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }

View File

@@ -491,42 +491,41 @@ export class DcRouter {
console.error('[DcRouter] Error stack:', err.stack); console.error('[DcRouter] Error stack:', err.stack);
}); });
if (acmeConfig) { // Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => { this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`); console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
const routeName = this.findRouteNameForDomain(event.domain); const routeName = this.findRouteNameForDomain(event.domain);
if (routeName) { if (routeName) {
this.certificateStatusMap.set(routeName, { this.certificateStatusMap.set(routeName, {
status: 'valid', domain: event.domain, status: 'valid', domain: event.domain,
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(), expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
source: event.source, source: event.source,
}); });
} }
}); });
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => { this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`); console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
const routeName = this.findRouteNameForDomain(event.domain); const routeName = this.findRouteNameForDomain(event.domain);
if (routeName) { if (routeName) {
this.certificateStatusMap.set(routeName, { this.certificateStatusMap.set(routeName, {
status: 'valid', domain: event.domain, status: 'valid', domain: event.domain,
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(), expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
source: event.source, source: event.source,
}); });
} }
}); });
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => { this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error); console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error);
const routeName = this.findRouteNameForDomain(event.domain); const routeName = this.findRouteNameForDomain(event.domain);
if (routeName) { if (routeName) {
this.certificateStatusMap.set(routeName, { this.certificateStatusMap.set(routeName, {
status: 'failed', domain: event.domain, error: event.error, status: 'failed', domain: event.domain, error: event.error,
source: event.source, source: event.source,
}); });
} }
}); });
}
// Start SmartProxy // Start SmartProxy
console.log('[DcRouter] Starting SmartProxy...'); console.log('[DcRouter] Starting SmartProxy...');

View File

@@ -251,26 +251,21 @@ export class StatsHandler {
if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) { if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) {
promises.push( promises.push(
this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats().then(stats => { (async () => {
const connectionDetails: interfaces.data.IConnectionDetails[] = []; const stats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
stats.connectionsByIP.forEach((count, ip) => { const serverStats = await this.collectServerStats();
connectionDetails.push({
remoteAddress: ip,
protocol: 'https' as any,
state: 'established' as any,
startTime: Date.now(),
bytesIn: 0,
bytesOut: 0,
});
});
metrics.network = { metrics.network = {
totalBandwidth: { totalBandwidth: {
in: stats.throughputRate.bytesInPerSecond, in: stats.throughputRate.bytesInPerSecond,
out: stats.throughputRate.bytesOutPerSecond, out: stats.throughputRate.bytesOutPerSecond,
}, },
activeConnections: stats.connectionsByIP.size, totalBytes: {
connectionDetails: connectionDetails.slice(0, 50), // Limit to 50 connections in: stats.totalDataTransferred.bytesIn,
out: stats.totalDataTransferred.bytesOut,
},
activeConnections: serverStats.activeConnections,
connectionDetails: [],
topEndpoints: stats.topIPs.map(ip => ({ topEndpoints: stats.topIPs.map(ip => ({
endpoint: ip.ip, endpoint: ip.ip,
requests: ip.count, requests: ip.count,
@@ -280,7 +275,7 @@ export class StatsHandler {
}, },
})), })),
}; };
}) })()
); );
} }

View File

@@ -116,6 +116,10 @@ export interface INetworkMetrics {
in: number; in: number;
out: number; out: number;
}; };
totalBytes?: {
in: number;
out: number;
};
activeConnections: number; activeConnections: number;
connectionDetails: IConnectionDetails[]; connectionDetails: IConnectionDetails[];
topEndpoints: Array<{ topEndpoints: Array<{

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/dcrouter', name: '@serve.zone/dcrouter',
version: '5.4.0', version: '5.4.1',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }

View File

@@ -47,6 +47,7 @@ export interface INetworkState {
connections: interfaces.data.IConnectionInfo[]; connections: interfaces.data.IConnectionInfo[];
connectionsByIP: { [ip: string]: number }; connectionsByIP: { [ip: string]: number };
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number }; throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
totalBytes: { in: number; out: number };
topIPs: Array<{ ip: string; count: number }>; topIPs: Array<{ ip: string; count: number }>;
lastUpdated: number; lastUpdated: number;
isLoading: boolean; isLoading: boolean;
@@ -144,6 +145,7 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
connections: [], connections: [],
connectionsByIP: {}, connectionsByIP: {},
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
totalBytes: { in: 0, out: 0 },
topIPs: [], topIPs: [],
lastUpdated: 0, lastUpdated: 0,
isLoading: false, isLoading: false,
@@ -421,6 +423,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
connections: connectionsResponse.connections, connections: connectionsResponse.connections,
connectionsByIP, connectionsByIP,
throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, 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 || [], topIPs: networkStatsResponse.topIPs || [],
lastUpdated: Date.now(), lastUpdated: Date.now(),
isLoading: false, isLoading: false,
@@ -790,6 +795,7 @@ async function dispatchCombinedRefreshAction() {
bytesInPerSecond: network.totalBandwidth.in, bytesInPerSecond: network.totalBandwidth.in,
bytesOutPerSecond: network.totalBandwidth.out bytesOutPerSecond: network.totalBandwidth.out
}, },
totalBytes: network.totalBytes || { in: 0, out: 0 },
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })), topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
lastUpdated: Date.now(), lastUpdated: Date.now(),
isLoading: false, isLoading: false,
@@ -805,6 +811,7 @@ async function dispatchCombinedRefreshAction() {
bytesInPerSecond: network.totalBandwidth.in, bytesInPerSecond: network.totalBandwidth.in,
bytesOutPerSecond: network.totalBandwidth.out bytesOutPerSecond: network.totalBandwidth.out
}, },
totalBytes: network.totalBytes || { in: 0, out: 0 },
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })), topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
lastUpdated: Date.now(), lastUpdated: Date.now(),
isLoading: false, isLoading: false,
@@ -845,13 +852,6 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
refreshInterval = setInterval(() => { refreshInterval = setInterval(() => {
// Use combined refresh action for efficiency // Use combined refresh action for efficiency
dispatchCombinedRefreshAction(); 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); }, uiState.refreshInterval);
} }
} else { } else {

View File

@@ -426,6 +426,7 @@ export class OpsViewNetwork extends DeesElement {
type: 'number', type: 'number',
icon: 'download', icon: 'download',
color: '#22c55e', color: '#22c55e',
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`,
}, },
{ {
id: 'throughputOut', id: 'throughputOut',
@@ -435,6 +436,7 @@ export class OpsViewNetwork extends DeesElement {
type: 'number', type: 'number',
icon: 'upload', icon: 'upload',
color: '#8b5cf6', color: '#8b5cf6',
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
}, },
]; ];