Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2c63cf170 | |||
| 09d66e4528 | |||
| 3078fa9d7b | |||
| 57fbb128e6 | |||
| d73266eeb8 | |||
| 2dbdf2d2b1 | |||
| 383e0adc23 | |||
| d7789f5a44 | |||
| 2638990667 | |||
| c33ecdc26f | |||
| b033d80927 | |||
| cf5d616769 | |||
| 8e722f5ab6 |
44
changelog.md
44
changelog.md
@@ -1,5 +1,49 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-14 - 5.4.5 - fix(dcrouter)
|
||||
bump patch for release pipeline consistency - no code changes
|
||||
|
||||
- current version: 5.4.4 (from package.json)
|
||||
- git diff: no changes detected
|
||||
- recommend patch bump to trigger release artifacts if required
|
||||
|
||||
## 2026-02-14 - 5.4.4 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.2.0
|
||||
|
||||
- Updated @push.rocks/smartproxy from ^25.1.0 to ^25.2.0 (patch, non-breaking).
|
||||
- Current package version is 5.4.3; recommend a patch release to 5.4.4.
|
||||
|
||||
## 2026-02-14 - 5.4.3 - fix(dependencies)
|
||||
bump @push.rocks/smartproxy to ^25.1.0
|
||||
|
||||
- Updated @push.rocks/smartproxy from ^25.0.0 to ^25.1.0 in package.json
|
||||
|
||||
## 2026-02-13 - 5.4.2 - fix(dcrouter)
|
||||
improve domain pattern matching to support routing-glob and wildcard patterns and use matching logic when resolving routes
|
||||
|
||||
- Support routing-glob patterns beginning with '*' (e.g. *example.com) to match base domain, wildcard form, and subdomains
|
||||
- Treat standard wildcard patterns ('*.example.com') as matching both the base domain (example.com) and its subdomains
|
||||
- Use isDomainMatch when resolving routes instead of exact array includes to allow pattern matching
|
||||
- Normalize domain and pattern to lowercase and simplify equality checks
|
||||
|
||||
## 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
|
||||
|
||||
- bump @push.rocks/smartproxy dependency to ^25.0.0
|
||||
- add optional 'source' field to certificate status and propagate event.source when certificates are issued, renewed, or failed
|
||||
- change smartProxy.certProvisionFunction signature to accept eventComms; use it to log attempts, set source and expiryDate, and fall back to http-01 on DNS-01 failure
|
||||
- make buildCertificateOverview async and query smartProxy.getCertificateStatus for a route when event-based status is unknown
|
||||
- improve logging to include certificate source and more contextual messages
|
||||
|
||||
## 2026-02-13 - 5.3.0 - feat(certificates)
|
||||
add certificate overview and reprovisioning in ops UI and API; track SmartProxy certificate events
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "5.3.0",
|
||||
"version": "5.4.5",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -49,7 +49,7 @@
|
||||
"@push.rocks/smartnetwork": "^4.4.0",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartproxy": "^24.0.0",
|
||||
"@push.rocks/smartproxy": "^25.2.0",
|
||||
"@push.rocks/smartradius": "^1.1.1",
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -75,8 +75,8 @@ importers:
|
||||
specifier: ^4.2.3
|
||||
version: 4.2.3
|
||||
'@push.rocks/smartproxy':
|
||||
specifier: ^24.0.0
|
||||
version: 24.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
||||
specifier: ^25.2.0
|
||||
version: 25.2.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
||||
'@push.rocks/smartradius':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
@@ -1040,8 +1040,8 @@ packages:
|
||||
'@push.rocks/smartpromise@4.2.3':
|
||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||
|
||||
'@push.rocks/smartproxy@24.0.0':
|
||||
resolution: {integrity: sha512-xSz6mrV59xmuiuaBgej6Fq611r9+Ay0ad2XiZAP/XGrkWykgQNeDZqzAq8dadmaCqO/3bVfH/mXlEYaKrDyTYA==}
|
||||
'@push.rocks/smartproxy@25.2.0':
|
||||
resolution: {integrity: sha512-cwqtfSI3QziyZOYXZuL4/jq1KHXQRVwGvimHcqhJDsl4cac9y7fM4gKHU4B3m2/2qaih1scP9FPGwlCCVFXR7Q==}
|
||||
|
||||
'@push.rocks/smartpuppeteer@2.0.5':
|
||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||
@@ -6441,7 +6441,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/smartpromise@4.2.3': {}
|
||||
|
||||
'@push.rocks/smartproxy@24.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
|
||||
'@push.rocks/smartproxy@25.2.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartacme': 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '5.3.0',
|
||||
version: '5.4.5',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -189,6 +189,7 @@ export class DcRouter {
|
||||
domain: string;
|
||||
expiryDate?: string;
|
||||
issuedAt?: string;
|
||||
source?: string;
|
||||
error?: string;
|
||||
}>();
|
||||
|
||||
@@ -450,9 +451,14 @@ export class DcRouter {
|
||||
});
|
||||
await this.smartAcme.start();
|
||||
|
||||
smartProxyConfig.certProvisionFunction = async (domain: string) => {
|
||||
smartProxyConfig.certProvisionFunction = async (domain, eventComms) => {
|
||||
try {
|
||||
eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
|
||||
eventComms.setSource('smartacme-dns-01');
|
||||
const cert = await this.smartAcme.getCertificateForDomain(domain);
|
||||
if (cert.validUntil) {
|
||||
eventComms.setExpiryDate(new Date(cert.validUntil));
|
||||
}
|
||||
return {
|
||||
id: cert.id,
|
||||
domainName: cert.domainName,
|
||||
@@ -463,7 +469,7 @@ export class DcRouter {
|
||||
csr: cert.csr,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`[DcRouter] SmartAcme DNS-01 failed for ${domain}, falling back to http-01:`, err.message);
|
||||
eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${err.message}, falling back to http-01`);
|
||||
return 'http01';
|
||||
}
|
||||
};
|
||||
@@ -485,39 +491,41 @@ export class DcRouter {
|
||||
console.error('[DcRouter] Error stack:', err.stack);
|
||||
});
|
||||
|
||||
if (acmeConfig) {
|
||||
this.smartProxy.on('certificate-issued', (event) => {
|
||||
console.log(`[DcRouter] Certificate issued for ${event.domain}, expires ${event.expiryDate}`);
|
||||
// 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) => {
|
||||
console.log(`[DcRouter] Certificate renewed for ${event.domain}, expires ${event.expiryDate}`);
|
||||
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) => {
|
||||
console.error(`[DcRouter] Certificate failed for ${event.domain}:`, event.error);
|
||||
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...');
|
||||
@@ -666,24 +674,25 @@ export class DcRouter {
|
||||
* @returns Whether the domain matches the pattern
|
||||
*/
|
||||
private isDomainMatch(domain: string, pattern: string): boolean {
|
||||
// Normalize inputs
|
||||
domain = domain.toLowerCase();
|
||||
pattern = pattern.toLowerCase();
|
||||
|
||||
// Check for exact match
|
||||
if (domain === pattern) {
|
||||
return true;
|
||||
if (domain === pattern) return true;
|
||||
|
||||
// Routing-glob: *example.com matches example.com, sub.example.com, *.example.com
|
||||
if (pattern.startsWith('*') && !pattern.startsWith('*.')) {
|
||||
const baseDomain = pattern.slice(1); // *nevermind.cloud → nevermind.cloud
|
||||
if (domain === baseDomain || domain === `*.${baseDomain}`) return true;
|
||||
if (domain.endsWith(baseDomain) && domain.length > baseDomain.length) return true;
|
||||
}
|
||||
|
||||
// Check for wildcard match (*.example.com)
|
||||
// Standard wildcard: *.example.com matches sub.example.com and example.com
|
||||
if (pattern.startsWith('*.')) {
|
||||
const patternSuffix = pattern.slice(2); // Remove the "*." prefix
|
||||
|
||||
// Check if domain ends with the pattern suffix and has at least one character before it
|
||||
return domain.endsWith(patternSuffix) && domain.length > patternSuffix.length;
|
||||
const suffix = pattern.slice(2);
|
||||
if (domain === suffix) return true;
|
||||
return domain.endsWith(suffix) && domain.length > suffix.length;
|
||||
}
|
||||
|
||||
// No match
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -697,7 +706,9 @@ export class DcRouter {
|
||||
const routeDomains = Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
if (routeDomains.includes(domain)) return route.name;
|
||||
for (const pattern of routeDomains) {
|
||||
if (this.isDomainMatch(domain, pattern)) return route.name;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -489,8 +489,12 @@ export class MetricsManager {
|
||||
return {
|
||||
connectionsByIP: new Map<string, number>(),
|
||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||
topIPs: [],
|
||||
topIPs: [] as Array<{ ip: string; count: number }>,
|
||||
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
||||
throughputHistory: [] as Array<{ timestamp: number; in: number; out: number }>,
|
||||
throughputByIP: new Map<string, { in: number; out: number }>(),
|
||||
requestsPerSecond: 0,
|
||||
requestsTotal: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -513,11 +517,25 @@ export class MetricsManager {
|
||||
bytesOut: proxyMetrics.totals.bytesOut()
|
||||
};
|
||||
|
||||
// Get throughput history from Rust engine (up to 300 seconds)
|
||||
const throughputHistory = proxyMetrics.throughput.history(300);
|
||||
|
||||
// Get per-IP throughput
|
||||
const throughputByIP = proxyMetrics.throughput.byIP();
|
||||
|
||||
// Get HTTP request rates
|
||||
const requestsPerSecond = proxyMetrics.requests.perSecond();
|
||||
const requestsTotal = proxyMetrics.requests.total();
|
||||
|
||||
return {
|
||||
connectionsByIP,
|
||||
throughputRate,
|
||||
topIPs,
|
||||
totalDataTransferred,
|
||||
throughputHistory,
|
||||
throughputByIP,
|
||||
requestsPerSecond,
|
||||
requestsTotal,
|
||||
};
|
||||
}, 200); // Use 200ms cache for more frequent updates
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export class CertificateHandler {
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
|
||||
'getCertificateOverview',
|
||||
async (dataArg) => {
|
||||
const certificates = this.buildCertificateOverview();
|
||||
const certificates = await this.buildCertificateOverview();
|
||||
const summary = this.buildSummary(certificates);
|
||||
return { certificates, summary };
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export class CertificateHandler {
|
||||
);
|
||||
}
|
||||
|
||||
private buildCertificateOverview(): interfaces.requests.ICertificateInfo[] {
|
||||
private async buildCertificateOverview(): Promise<interfaces.requests.ICertificateInfo[]> {
|
||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||
const smartProxy = dcRouter.smartProxy;
|
||||
if (!smartProxy) return [];
|
||||
@@ -82,14 +82,26 @@ export class CertificateHandler {
|
||||
expiryDate = eventStatus.expiryDate;
|
||||
issuedAt = eventStatus.issuedAt;
|
||||
error = eventStatus.error;
|
||||
if (eventStatus.source) {
|
||||
issuer = eventStatus.source;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get Rust-side certificate data
|
||||
// Try Rust-side certificate status if no event data
|
||||
if (status === 'unknown') {
|
||||
try {
|
||||
// getCertificateStatus is async but we're in a sync context
|
||||
// We'll rely on event-based data primarily
|
||||
const rustStatus = await smartProxy.getCertificateStatus(route.name);
|
||||
if (rustStatus) {
|
||||
if (rustStatus.expiryDate) expiryDate = rustStatus.expiryDate;
|
||||
if (rustStatus.issuer) issuer = rustStatus.issuer;
|
||||
if (rustStatus.issuedAt) issuedAt = rustStatus.issuedAt;
|
||||
if (rustStatus.status === 'valid' || rustStatus.status === 'expired') {
|
||||
status = rustStatus.status;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors from Rust bridge
|
||||
// Rust bridge may not support this command yet — ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Compute status from expiry date if we have one and status is still valid/unknown
|
||||
|
||||
@@ -85,11 +85,23 @@ export class SecurityHandler {
|
||||
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
||||
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
||||
|
||||
// Convert per-IP throughput Map to serializable array
|
||||
const throughputByIP: Array<{ ip: string; in: number; out: number }> = [];
|
||||
if (networkStats.throughputByIP) {
|
||||
for (const [ip, tp] of networkStats.throughputByIP) {
|
||||
throughputByIP.push({ ip, in: tp.in, out: tp.out });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })),
|
||||
throughputRate: networkStats.throughputRate,
|
||||
topIPs: networkStats.topIPs,
|
||||
totalDataTransferred: networkStats.totalDataTransferred,
|
||||
throughputHistory: networkStats.throughputHistory || [],
|
||||
throughputByIP,
|
||||
requestsPerSecond: networkStats.requestsPerSecond || 0,
|
||||
requestsTotal: networkStats.requestsTotal || 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,6 +111,10 @@ export class SecurityHandler {
|
||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||
topIPs: [],
|
||||
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
||||
throughputHistory: [],
|
||||
throughputByIP: [],
|
||||
requestsPerSecond: 0,
|
||||
requestsTotal: 0,
|
||||
};
|
||||
}
|
||||
)
|
||||
|
||||
@@ -251,36 +251,39 @@ 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();
|
||||
|
||||
// Build per-IP bandwidth lookup from throughputByIP
|
||||
const ipBandwidth = new Map<string, { in: number; out: number }>();
|
||||
if (stats.throughputByIP) {
|
||||
for (const [ip, tp] of stats.throughputByIP) {
|
||||
ipBandwidth.set(ip, { in: tp.in, out: tp.out });
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
bandwidth: {
|
||||
in: 0,
|
||||
out: 0,
|
||||
},
|
||||
bandwidth: ipBandwidth.get(ip.ip) || { in: 0, out: 0 },
|
||||
})),
|
||||
throughputHistory: stats.throughputHistory || [],
|
||||
requestsPerSecond: stats.requestsPerSecond || 0,
|
||||
requestsTotal: stats.requestsTotal || 0,
|
||||
};
|
||||
})
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,10 @@ export interface INetworkMetrics {
|
||||
in: number;
|
||||
out: number;
|
||||
};
|
||||
totalBytes?: {
|
||||
in: number;
|
||||
out: number;
|
||||
};
|
||||
activeConnections: number;
|
||||
connectionDetails: IConnectionDetails[];
|
||||
topEndpoints: Array<{
|
||||
@@ -126,6 +130,9 @@ export interface INetworkMetrics {
|
||||
out: number;
|
||||
};
|
||||
}>;
|
||||
throughputHistory?: Array<{ timestamp: number; in: number; out: number }>;
|
||||
requestsPerSecond?: number;
|
||||
requestsTotal?: number;
|
||||
}
|
||||
|
||||
export interface IConnectionDetails {
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '5.3.0',
|
||||
version: '5.4.5',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -47,7 +47,12 @@ 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 }>;
|
||||
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
||||
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
||||
requestsPerSecond: number;
|
||||
requestsTotal: number;
|
||||
lastUpdated: number;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
@@ -144,7 +149,12 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
||||
connections: [],
|
||||
connectionsByIP: {},
|
||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||
totalBytes: { in: 0, out: 0 },
|
||||
topIPs: [],
|
||||
throughputByIP: [],
|
||||
throughputHistory: [],
|
||||
requestsPerSecond: 0,
|
||||
requestsTotal: 0,
|
||||
lastUpdated: 0,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -421,7 +431,14 @@ 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 || [],
|
||||
throughputByIP: networkStatsResponse.throughputByIP || [],
|
||||
throughputHistory: networkStatsResponse.throughputHistory || [],
|
||||
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
||||
requestsTotal: networkStatsResponse.requestsTotal || 0,
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -790,7 +807,12 @@ 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 })),
|
||||
throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
|
||||
throughputHistory: network.throughputHistory || [],
|
||||
requestsPerSecond: network.requestsPerSecond || 0,
|
||||
requestsTotal: network.requestsTotal || 0,
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -805,7 +827,12 @@ 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 })),
|
||||
throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
|
||||
throughputHistory: network.throughputHistory || [],
|
||||
requestsPerSecond: network.requestsPerSecond || 0,
|
||||
requestsTotal: network.requestsTotal || 0,
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -845,13 +872,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 {
|
||||
|
||||
@@ -52,8 +52,7 @@ export class OpsViewNetwork extends DeesElement {
|
||||
private requestCountHistory = new Map<number, number>(); // Track requests per time bucket
|
||||
private trafficUpdateTimer: any = null;
|
||||
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
|
||||
|
||||
// Removed byte tracking - now using real-time data from SmartProxy
|
||||
private historyLoaded = false; // Whether server-side throughput history has been loaded
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -111,6 +110,54 @@ export class OpsViewNetwork extends DeesElement {
|
||||
this.lastTrafficUpdateTime = now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load server-side throughput history into the chart.
|
||||
* Called once when history data first arrives from the Rust engine.
|
||||
* This pre-populates the chart so users see historical data immediately
|
||||
* instead of starting from all zeros.
|
||||
*/
|
||||
private loadThroughputHistory() {
|
||||
const history = this.networkState.throughputHistory;
|
||||
if (!history || history.length === 0) return;
|
||||
|
||||
this.historyLoaded = true;
|
||||
|
||||
// Convert history points to chart data format (bytes/sec → Mbit/s)
|
||||
const historyIn = history.map(p => ({
|
||||
x: new Date(p.timestamp).toISOString(),
|
||||
y: Math.round((p.in * 8) / 1000000 * 10) / 10,
|
||||
}));
|
||||
const historyOut = history.map(p => ({
|
||||
x: new Date(p.timestamp).toISOString(),
|
||||
y: Math.round((p.out * 8) / 1000000 * 10) / 10,
|
||||
}));
|
||||
|
||||
// Use history as the chart data, keeping the most recent 60 points (5 min window)
|
||||
const sliceStart = Math.max(0, historyIn.length - 60);
|
||||
this.trafficDataIn = historyIn.slice(sliceStart);
|
||||
this.trafficDataOut = historyOut.slice(sliceStart);
|
||||
|
||||
// If fewer than 60 points, pad the front with zeros
|
||||
if (this.trafficDataIn.length < 60) {
|
||||
const now = Date.now();
|
||||
const range = 5 * 60 * 1000;
|
||||
const bucketSize = range / 60;
|
||||
const padCount = 60 - this.trafficDataIn.length;
|
||||
const firstTimestamp = this.trafficDataIn.length > 0
|
||||
? new Date(this.trafficDataIn[0].x).getTime()
|
||||
: now;
|
||||
|
||||
const padIn = Array.from({ length: padCount }, (_, i) => ({
|
||||
x: new Date(firstTimestamp - ((padCount - i) * bucketSize)).toISOString(),
|
||||
y: 0,
|
||||
}));
|
||||
const padOut = padIn.map(p => ({ ...p }));
|
||||
|
||||
this.trafficDataIn = [...padIn, ...this.trafficDataIn];
|
||||
this.trafficDataOut = [...padOut, ...this.trafficDataOut];
|
||||
}
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
viewHostCss,
|
||||
@@ -352,21 +399,6 @@ export class OpsViewNetwork extends DeesElement {
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
private calculateRequestsPerSecond(): number {
|
||||
// Calculate from actual request data in the last minute
|
||||
const oneMinuteAgo = Date.now() - 60000;
|
||||
const recentRequests = this.networkRequests.filter(req => req.timestamp >= oneMinuteAgo);
|
||||
const reqPerSec = Math.round(recentRequests.length / 60);
|
||||
|
||||
// Track history for trend (keep last 20 values)
|
||||
this.requestsPerSecHistory.push(reqPerSec);
|
||||
if (this.requestsPerSecHistory.length > 20) {
|
||||
this.requestsPerSecHistory.shift();
|
||||
}
|
||||
|
||||
return reqPerSec;
|
||||
}
|
||||
|
||||
private calculateThroughput(): { in: number; out: number } {
|
||||
// Use real throughput data from network state
|
||||
return {
|
||||
@@ -376,16 +408,17 @@ export class OpsViewNetwork extends DeesElement {
|
||||
}
|
||||
|
||||
private renderNetworkStats(): TemplateResult {
|
||||
const reqPerSec = this.calculateRequestsPerSecond();
|
||||
// Use server-side requests/sec from SmartProxy's Rust engine
|
||||
const reqPerSec = this.networkState.requestsPerSecond || 0;
|
||||
const throughput = this.calculateThroughput();
|
||||
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
||||
|
||||
// Throughput data is now available in the stats tiles
|
||||
|
||||
// Use request count history for the requests/sec trend
|
||||
// Track requests/sec history for the trend sparkline
|
||||
this.requestsPerSecHistory.push(reqPerSec);
|
||||
if (this.requestsPerSecHistory.length > 20) {
|
||||
this.requestsPerSecHistory.shift();
|
||||
}
|
||||
const trendData = [...this.requestsPerSecHistory];
|
||||
|
||||
// If we don't have enough data, pad with zeros
|
||||
while (trendData.length < 20) {
|
||||
trendData.unshift(0);
|
||||
}
|
||||
@@ -398,7 +431,7 @@ export class OpsViewNetwork extends DeesElement {
|
||||
type: 'number',
|
||||
icon: 'plug',
|
||||
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
|
||||
description: `Total: ${this.statsState.serverStats?.totalConnections || 0}`,
|
||||
description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`,
|
||||
actions: [
|
||||
{
|
||||
name: 'View Details',
|
||||
@@ -416,7 +449,7 @@ export class OpsViewNetwork extends DeesElement {
|
||||
icon: 'chartLine',
|
||||
color: '#3b82f6',
|
||||
trendData: trendData,
|
||||
description: `Average over last minute`,
|
||||
description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`,
|
||||
},
|
||||
{
|
||||
id: 'throughputIn',
|
||||
@@ -426,6 +459,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 +469,7 @@ export class OpsViewNetwork extends DeesElement {
|
||||
type: 'number',
|
||||
icon: 'upload',
|
||||
color: '#8b5cf6',
|
||||
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -461,19 +496,32 @@ export class OpsViewNetwork extends DeesElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
// Build per-IP bandwidth lookup
|
||||
const bandwidthByIP = new Map<string, { in: number; out: number }>();
|
||||
if (this.networkState.throughputByIP) {
|
||||
for (const entry of this.networkState.throughputByIP) {
|
||||
bandwidthByIP.set(entry.ip, { in: entry.in, out: entry.out });
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total connections across all top IPs
|
||||
const totalConnections = this.networkState.topIPs.reduce((sum, ipData) => sum + ipData.count, 0);
|
||||
|
||||
return html`
|
||||
<dees-table
|
||||
.data=${this.networkState.topIPs}
|
||||
.displayFunction=${(ipData: { ip: string; count: number }) => ({
|
||||
.displayFunction=${(ipData: { ip: string; count: number }) => {
|
||||
const bw = bandwidthByIP.get(ipData.ip);
|
||||
return {
|
||||
'IP Address': ipData.ip,
|
||||
'Connections': ipData.count,
|
||||
'Percentage': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
||||
})}
|
||||
'Bandwidth In': bw ? this.formatBitsPerSecond(bw.in) : '0 bit/s',
|
||||
'Bandwidth Out': bw ? this.formatBitsPerSecond(bw.out) : '0 bit/s',
|
||||
'Share': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
||||
};
|
||||
}}
|
||||
heading1="Top Connected IPs"
|
||||
heading2="IPs with most active connections"
|
||||
heading2="IPs with most active connections and bandwidth"
|
||||
.pagination=${false}
|
||||
dataName="ip"
|
||||
></dees-table>
|
||||
@@ -513,13 +561,10 @@ export class OpsViewNetwork extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate traffic data based on request history
|
||||
this.updateTrafficData();
|
||||
// Load server-side throughput history into chart (once)
|
||||
if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) {
|
||||
this.loadThroughputHistory();
|
||||
}
|
||||
|
||||
private updateTrafficData() {
|
||||
// This method is called when network data updates
|
||||
// The actual chart updates are handled by the timer calling addTrafficDataPoint()
|
||||
}
|
||||
|
||||
private startTrafficUpdateTimer() {
|
||||
|
||||
Reference in New Issue
Block a user