Compare commits

..

12 Commits

Author SHA1 Message Date
0195a21f30 v11.10.5
Some checks failed
Docker (tags) / security (push) Failing after 4s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-26 07:10:59 +00:00
4dca747386 fix(build): rename smart tooling config to .smartconfig.json and update package references 2026-03-26 07:10:59 +00:00
7663f502fa v11.10.4
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-24 13:40:28 +00:00
104cd417d8 fix(monitoring): handle multiple protocol cache entries per backend in metrics output 2026-03-24 13:40:28 +00:00
93254d5d3d v11.10.3
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-23 21:18:20 +00:00
9a3f121a9c fix(deps): bump tstest, smartmetrics, and taskbuffer to latest patch releases 2026-03-23 21:18:20 +00:00
bef74eb1aa v11.10.2
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-23 14:22:24 +00:00
308d8e4851 fix(deps): bump @api.global/typedserver to ^8.4.6 and @push.rocks/smartproxy to ^26.2.1 2026-03-23 14:22:24 +00:00
dc010dc3ae v11.10.1
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-23 10:29:16 +00:00
61d5d3b1ad fix(deps): bump @push.rocks/smartproxy to ^26.2.0 2026-03-23 10:29:16 +00:00
dd70790d40 v11.10.0
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-23 07:17:33 +00:00
2f8c04edc4 feat(monitoring): add backend protocol metrics to network stats and ops dashboard 2026-03-23 07:17:33 +00:00
15 changed files with 902 additions and 526 deletions

View File

@@ -1,7 +1,7 @@
{
"json.schemas": [
{
"fileMatch": ["/npmextra.json"],
"fileMatch": ["/.smartconfig.json"],
"schema": {
"type": "object",
"properties": {

View File

@@ -1,5 +1,45 @@
# Changelog
## 2026-03-26 - 11.10.5 - fix(build)
rename smart tooling config to .smartconfig.json and update package references
- Moves the shared tool configuration from npmextra.json to .smartconfig.json.
- Updates package.json published files and documentation to reference the new config file.
- Refreshes several development and runtime dependency versions alongside the config migration.
## 2026-03-24 - 11.10.4 - fix(monitoring)
handle multiple protocol cache entries per backend in metrics output
- Group detected protocol cache entries by backend host and port so multiple domain-specific records are preserved.
- Emit one backend metrics row per cached domain and avoid dropping unmatched protocol cache entries by tracking seen entries with a composite host:port:domain key.
- Use cached protocol values when available while keeping backend-only rows for metrics without protocol cache data.
## 2026-03-23 - 11.10.3 - fix(deps)
bump tstest, smartmetrics, and taskbuffer to latest patch releases
- update @git.zone/tstest from ^3.5.0 to ^3.5.1
- update @push.rocks/smartmetrics from ^3.0.2 to ^3.0.3
- update @push.rocks/taskbuffer from ^8.0.0 to ^8.0.2
## 2026-03-23 - 11.10.2 - fix(deps)
bump @api.global/typedserver to ^8.4.6 and @push.rocks/smartproxy to ^26.2.1
- Updates @api.global/typedserver from ^8.4.2 to ^8.4.6
- Updates @push.rocks/smartproxy from ^26.2.0 to ^26.2.1
## 2026-03-23 - 11.10.1 - fix(deps)
bump @push.rocks/smartproxy to ^26.2.0
- Updates the @push.rocks/smartproxy dependency from ^26.1.0 to ^26.2.0 in package.json.
## 2026-03-23 - 11.10.0 - feat(monitoring)
add backend protocol metrics to network stats and ops dashboard
- Expose backend protocol, connection, error, and suppression metrics in stats responses.
- Add typed backend info interfaces and app state support for backend metrics.
- Render a new backend protocols table in the ops network view with detail modal and suppression badges.
- Update smartproxy and lik dependencies to support backend protocol metrics collection.
## 2026-03-21 - 11.9.1 - fix(lifecycle)
clean up service subscriptions, proxy retries, and stale runtime state on shutdown

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "11.9.1",
"version": "11.10.5",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -22,48 +22,48 @@
"watch": "tswatch"
},
"devDependencies": {
"@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsbundle": "^2.9.1",
"@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.5.0",
"@git.zone/tswatch": "^3.3.0",
"@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsbundle": "^2.10.0",
"@git.zone/tsrun": "^2.0.2",
"@git.zone/tstest": "^3.6.0",
"@git.zone/tswatch": "^3.3.2",
"@types/node": "^25.5.0"
},
"dependencies": {
"@api.global/typedrequest": "^3.3.0",
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^8.4.2",
"@api.global/typedserver": "^8.4.6",
"@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.48.5",
"@design.estate/dees-catalog": "^3.49.0",
"@design.estate/dees-element": "^2.2.3",
"@push.rocks/lik": "^6.3.1",
"@push.rocks/lik": "^6.4.0",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^9.3.0",
"@push.rocks/smartdata": "^7.1.0",
"@push.rocks/smartdata": "^7.1.2",
"@push.rocks/smartdns": "^7.9.0",
"@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartguard": "^3.1.0",
"@push.rocks/smartjwt": "^2.2.1",
"@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartmetrics": "^3.0.2",
"@push.rocks/smartmetrics": "^3.0.3",
"@push.rocks/smartmongo": "^5.1.0",
"@push.rocks/smartmta": "^5.3.1",
"@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartproxy": "^26.0.0",
"@push.rocks/smartproxy": "^26.2.4",
"@push.rocks/smartradius": "^1.1.1",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartstate": "^2.2.0",
"@push.rocks/smartstate": "^2.2.1",
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/taskbuffer": "^8.0.0",
"@push.rocks/taskbuffer": "^8.0.2",
"@serve.zone/catalog": "^2.9.0",
"@serve.zone/interfaces": "^5.3.0",
"@serve.zone/remoteingress": "^4.14.1",
"@tsclass/tsclass": "^9.4.0",
"@serve.zone/remoteingress": "^4.14.2",
"@tsclass/tsclass": "^9.5.0",
"lru-cache": "^11.2.7",
"uuid": "^13.0.0"
},
@@ -112,7 +112,7 @@
"dist_ts_apiclient/**/*",
"assets/**/*",
"cli.js",
"npmextra.json",
".smartconfig.json",
"readme.md"
]
}

1074
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -133,7 +133,7 @@ The project now uses tswatch for development:
```bash
pnpm run watch
```
Configuration in `npmextra.json`:
Configuration in `.smartconfig.json`:
```json
{
"@git.zone/tswatch": {

View File

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

View File

@@ -558,6 +558,7 @@ export class MetricsManager {
throughputByIP: new Map<string, { in: number; out: number }>(),
requestsPerSecond: 0,
requestsTotal: 0,
backends: [] as Array<any>,
};
}
@@ -590,6 +591,110 @@ export class MetricsManager {
const requestsPerSecond = proxyMetrics.requests.perSecond();
const requestsTotal = proxyMetrics.requests.total();
// Collect backend protocol data
const backendMetrics = proxyMetrics.backends.byBackend();
const protocolCache = proxyMetrics.backends.detectedProtocols();
// Group protocol cache entries by host:port so we can match them to backend metrics.
// The protocol cache is keyed by (host, port, domain) in Rust, so the same host:port
// can have multiple entries for different domains.
const cacheByBackend = new Map<string, (typeof protocolCache)[number][]>();
for (const entry of protocolCache) {
const backendKey = `${entry.host}:${entry.port}`;
let entries = cacheByBackend.get(backendKey);
if (!entries) {
entries = [];
cacheByBackend.set(backendKey, entries);
}
entries.push(entry);
}
const backends: Array<any> = [];
const seenCacheKeys = new Set<string>();
for (const [key, bm] of backendMetrics) {
const cacheEntries = cacheByBackend.get(key);
if (!cacheEntries || cacheEntries.length === 0) {
// No protocol cache entry — emit one row with backend metrics only
backends.push({
backend: key,
domain: null,
protocol: bm.protocol,
activeConnections: bm.activeConnections,
totalConnections: bm.totalConnections,
connectErrors: bm.connectErrors,
handshakeErrors: bm.handshakeErrors,
requestErrors: bm.requestErrors,
avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
h2Failures: bm.h2Failures,
h2Suppressed: false,
h3Suppressed: false,
h2CooldownRemainingSecs: null,
h3CooldownRemainingSecs: null,
h2ConsecutiveFailures: null,
h3ConsecutiveFailures: null,
h3Port: null,
cacheAgeSecs: null,
});
} else {
// One row per domain, each enriched with the shared backend metrics
for (const cache of cacheEntries) {
const compositeKey = `${cache.host}:${cache.port}:${cache.domain ?? ''}`;
seenCacheKeys.add(compositeKey);
backends.push({
backend: key,
domain: cache.domain ?? null,
protocol: cache.protocol ?? bm.protocol,
activeConnections: bm.activeConnections,
totalConnections: bm.totalConnections,
connectErrors: bm.connectErrors,
handshakeErrors: bm.handshakeErrors,
requestErrors: bm.requestErrors,
avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
h2Failures: bm.h2Failures,
h2Suppressed: cache.h2Suppressed,
h3Suppressed: cache.h3Suppressed,
h2CooldownRemainingSecs: cache.h2CooldownRemainingSecs,
h3CooldownRemainingSecs: cache.h3CooldownRemainingSecs,
h2ConsecutiveFailures: cache.h2ConsecutiveFailures,
h3ConsecutiveFailures: cache.h3ConsecutiveFailures,
h3Port: cache.h3Port,
cacheAgeSecs: cache.ageSecs,
});
}
}
}
// Include protocol cache entries with no matching backend metric
for (const entry of protocolCache) {
const compositeKey = `${entry.host}:${entry.port}:${entry.domain ?? ''}`;
if (!seenCacheKeys.has(compositeKey)) {
backends.push({
backend: `${entry.host}:${entry.port}`,
domain: entry.domain,
protocol: entry.protocol,
activeConnections: 0,
totalConnections: 0,
connectErrors: 0,
handshakeErrors: 0,
requestErrors: 0,
avgConnectTimeMs: 0,
poolHitRate: 0,
h2Failures: 0,
h2Suppressed: entry.h2Suppressed,
h3Suppressed: entry.h3Suppressed,
h2CooldownRemainingSecs: entry.h2CooldownRemainingSecs,
h3CooldownRemainingSecs: entry.h3CooldownRemainingSecs,
h2ConsecutiveFailures: entry.h2ConsecutiveFailures,
h3ConsecutiveFailures: entry.h3ConsecutiveFailures,
h3Port: entry.h3Port,
cacheAgeSecs: entry.ageSecs,
});
}
}
return {
connectionsByIP,
throughputRate,
@@ -599,6 +704,7 @@ export class MetricsManager {
throughputByIP,
requestsPerSecond,
requestsTotal,
backends,
};
}, 1000); // 1s cache — matches typical dashboard poll interval
}

View File

@@ -101,6 +101,7 @@ export class SecurityHandler {
throughputByIP,
requestsPerSecond: networkStats.requestsPerSecond || 0,
requestsTotal: networkStats.requestsTotal || 0,
backends: networkStats.backends || [],
};
}
@@ -114,6 +115,7 @@ export class SecurityHandler {
throughputByIP: [],
requestsPerSecond: 0,
requestsTotal: 0,
backends: [],
};
}
)

View File

@@ -309,6 +309,7 @@ export class StatsHandler {
throughputHistory: stats.throughputHistory || [],
requestsPerSecond: stats.requestsPerSecond || 0,
requestsTotal: stats.requestsTotal || 0,
backends: stats.backends || [],
};
})()
);

View File

@@ -165,6 +165,7 @@ export interface INetworkMetrics {
throughputHistory?: Array<{ timestamp: number; in: number; out: number }>;
requestsPerSecond?: number;
requestsTotal?: number;
backends?: IBackendInfo[];
}
export interface IConnectionDetails {
@@ -174,4 +175,26 @@ export interface IConnectionDetails {
startTime: number;
bytesIn: number;
bytesOut: number;
}
export interface IBackendInfo {
backend: string;
domain: string | null;
protocol: string;
activeConnections: number;
totalConnections: number;
connectErrors: number;
handshakeErrors: number;
requestErrors: number;
avgConnectTimeMs: number;
poolHitRate: number;
h2Failures: number;
h2Suppressed: boolean;
h3Suppressed: boolean;
h2CooldownRemainingSecs: number | null;
h3CooldownRemainingSecs: number | null;
h2ConsecutiveFailures: number | null;
h3ConsecutiveFailures: number | null;
h3Port: number | null;
cacheAgeSecs: number | null;
}

View File

@@ -179,5 +179,6 @@ export interface IReq_GetNetworkStats extends plugins.typedrequestInterfaces.imp
throughputByIP: Array<{ ip: string; in: number; out: number }>;
requestsPerSecond: number;
requestsTotal: number;
backends?: statsInterfaces.IBackendInfo[];
};
}

View File

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

View File

@@ -53,6 +53,7 @@ export interface INetworkState {
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
requestsPerSecond: number;
requestsTotal: number;
backends: interfaces.data.IBackendInfo[];
lastUpdated: number;
isLoading: boolean;
error: string | null;
@@ -148,6 +149,7 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
throughputHistory: [],
requestsPerSecond: 0,
requestsTotal: 0,
backends: [],
lastUpdated: 0,
isLoading: false,
error: null,
@@ -503,6 +505,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
throughputHistory: networkStatsResponse.throughputHistory || [],
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
requestsTotal: networkStatsResponse.requestsTotal || 0,
backends: networkStatsResponse.backends || [],
lastUpdated: Date.now(),
isLoading: false,
error: null,

View File

@@ -1,5 +1,6 @@
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
import * as interfaces from '../../dist_ts_interfaces/index.js';
import { viewHostCss } from './shared/css.js';
import { type IStatsTile } from '@design.estate/dees-catalog';
@@ -198,6 +199,38 @@ export class OpsViewNetwork extends DeesElement {
color: ${cssManager.bdTheme('#00796b', '#4db6ac')};
}
.protocolBadge.h1 {
background: ${cssManager.bdTheme('#e3f2fd', '#1a2c3a')};
color: ${cssManager.bdTheme('#1976d2', '#5a9fd4')};
}
.protocolBadge.h2 {
background: ${cssManager.bdTheme('#e8f5e9', '#1a3a1a')};
color: ${cssManager.bdTheme('#388e3c', '#66bb6a')};
}
.protocolBadge.h3 {
background: ${cssManager.bdTheme('#f3e5f5', '#2a1a3a')};
color: ${cssManager.bdTheme('#7b1fa2', '#ba68c8')};
}
.protocolBadge.unknown {
background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')};
color: ${cssManager.bdTheme('#757575', '#999999')};
}
.suppressionBadge {
display: inline-flex;
align-items: center;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
background: ${cssManager.bdTheme('#fff3e0', '#3a2a1a')};
color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
margin-left: 4px;
}
.statusBadge {
display: inline-flex;
align-items: center;
@@ -265,6 +298,9 @@ export class OpsViewNetwork extends DeesElement {
<!-- Top IPs Section -->
${this.renderTopIPs()}
<!-- Backend Protocols Section -->
${this.renderBackendProtocols()}
<!-- Requests Table -->
<dees-table
.data=${this.networkRequests}
@@ -519,6 +555,106 @@ export class OpsViewNetwork extends DeesElement {
`;
}
private renderBackendProtocols(): TemplateResult {
const backends = this.networkState.backends;
if (!backends || backends.length === 0) {
return html``;
}
return html`
<dees-table
.data=${backends}
.displayFunction=${(item: interfaces.data.IBackendInfo) => {
const totalErrors = item.connectErrors + item.handshakeErrors + item.requestErrors;
const protocolClass = item.protocol.toLowerCase().replace(/[^a-z0-9]/g, '');
return {
'Backend': item.backend,
'Domain': item.domain || '-',
'Protocol': html`
<span class="protocolBadge ${protocolClass}">${item.protocol.toUpperCase()}</span>
${item.h2Suppressed ? html`<span class="suppressionBadge" title="H2 suppressed: ${item.h2ConsecutiveFailures ?? 0} failures, cooldown ${item.h2CooldownRemainingSecs ?? 0}s">H2 suppressed</span>` : ''}
${item.h3Suppressed ? html`<span class="suppressionBadge" title="H3 suppressed: ${item.h3ConsecutiveFailures ?? 0} failures, cooldown ${item.h3CooldownRemainingSecs ?? 0}s">H3 suppressed</span>` : ''}
`,
'Active': item.activeConnections,
'Total': this.formatNumber(item.totalConnections),
'Avg Connect': item.avgConnectTimeMs > 0 ? `${item.avgConnectTimeMs.toFixed(1)}ms` : '-',
'Pool Hit Rate': item.poolHitRate > 0 ? `${(item.poolHitRate * 100).toFixed(1)}%` : '-',
'Errors': totalErrors > 0
? html`<span class="statusBadge error">${totalErrors}</span>`
: html`<span class="statusBadge success">0</span>`,
'Cache Age': item.cacheAgeSecs != null ? `${Math.round(item.cacheAgeSecs)}s` : '-',
};
}}
.dataActions=${[
{
name: 'View Details',
iconName: 'lucide:info',
type: ['inRow', 'doubleClick', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
await this.showBackendDetails(actionData.item);
}
}
]}
heading1="Backend Protocols"
heading2="Auto-detected backend protocols and connection pool health"
searchable
.pagination=${false}
dataName="backend"
></dees-table>
`;
}
private async showBackendDetails(backend: interfaces.data.IBackendInfo) {
const { DeesModal } = await import('@design.estate/dees-catalog');
await DeesModal.createAndShow({
heading: `Backend: ${backend.backend}`,
content: html`
<div style="padding: 20px;">
<dees-dataview-codebox
.heading=${'Backend Details'}
progLang="json"
.codeToDisplay=${JSON.stringify({
backend: backend.backend,
domain: backend.domain,
protocol: backend.protocol,
activeConnections: backend.activeConnections,
totalConnections: backend.totalConnections,
avgConnectTimeMs: backend.avgConnectTimeMs,
poolHitRate: backend.poolHitRate,
errors: {
connect: backend.connectErrors,
handshake: backend.handshakeErrors,
request: backend.requestErrors,
h2Failures: backend.h2Failures,
},
suppression: {
h2Suppressed: backend.h2Suppressed,
h3Suppressed: backend.h3Suppressed,
h2CooldownRemainingSecs: backend.h2CooldownRemainingSecs,
h3CooldownRemainingSecs: backend.h3CooldownRemainingSecs,
h2ConsecutiveFailures: backend.h2ConsecutiveFailures,
h3ConsecutiveFailures: backend.h3ConsecutiveFailures,
},
h3Port: backend.h3Port,
cacheAgeSecs: backend.cacheAgeSecs,
}, null, 2)}
></dees-dataview-codebox>
</div>
`,
menuOptions: [
{
name: 'Copy Backend Key',
iconName: 'lucide:Copy',
action: async () => {
await navigator.clipboard.writeText(backend.backend);
}
}
]
});
}
private async updateNetworkData() {
// Track requests/sec history for the trend sparkline (moved out of render)
const reqPerSec = this.networkState.requestsPerSecond || 0;