From 6c3d8714a261209baad02c7d9bd260fd1ed86fce Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sat, 4 Apr 2026 16:45:02 +0000 Subject: [PATCH] feat(monitoring): add frontend and backend protocol distribution metrics to network stats --- changelog.md | 8 ++++ package.json | 2 +- pnpm-lock.yaml | 43 +++++++++++++----- ts/00_commitinfo_data.ts | 2 +- ts/classes.storage-cert-manager.ts | 2 +- ts/monitoring/classes.metricsmanager.ts | 7 +++ ts/opsserver/handlers/stats.handler.ts | 2 + ts_interfaces/data/stats.ts | 15 +++++++ ts_web/00_commitinfo_data.ts | 2 +- ts_web/appstate.ts | 10 +++++ ts_web/elements/ops-view-network.ts | 58 ++++++++++++++++++++++++- 11 files changed, 135 insertions(+), 16 deletions(-) diff --git a/changelog.md b/changelog.md index 3f7104f..e0e6b88 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-04-04 - 12.9.0 - feat(monitoring) +add frontend and backend protocol distribution metrics to network stats + +- Expose frontend and backend protocol distribution data in monitoring metrics, stats responses, and shared interfaces. +- Render protocol distribution donut charts in the ops network view using the new stats fields. +- Preserve existing stored certificate IDs when updating certificate records by domain. +- Bump @design.estate/dees-catalog to ^3.55.5 for the new chart component support. + ## 2026-04-04 - 12.8.1 - fix(ops-view-routes) correct route form dropdown selection handling for security profiles and network targets diff --git a/package.json b/package.json index 38c1567..2b9aa95 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@api.global/typedserver": "^8.4.6", "@api.global/typedsocket": "^4.1.2", "@apiclient.xyz/cloudflare": "^7.1.0", - "@design.estate/dees-catalog": "^3.55.1", + "@design.estate/dees-catalog": "^3.55.5", "@design.estate/dees-element": "^2.2.4", "@push.rocks/lik": "^6.4.0", "@push.rocks/projectinfo": "^5.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11d4f96..300e7a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^7.1.0 version: 7.1.0 '@design.estate/dees-catalog': - specifier: ^3.55.1 - version: 3.55.1(@tiptap/pm@2.27.2) + specifier: ^3.55.5 + version: 3.55.5(@tiptap/pm@2.27.2) '@design.estate/dees-element': specifier: ^2.2.4 version: 2.2.4 @@ -350,8 +350,8 @@ packages: '@configvault.io/interfaces@1.0.17': resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} - '@design.estate/dees-catalog@3.55.1': - resolution: {integrity: sha512-UXBC68Dg+L2cGJynvrXyaJATlSLnmiA5SMrE1SWVcp2H1niXqHph6KVi9+E52YSLoEmsCS23cf+XjUjoRNgZTw==} + '@design.estate/dees-catalog@3.55.5': + resolution: {integrity: sha512-NAMUkTVqdZZmwI/g1xKOxOYM9QUd9FHODh6MYkP6LhLjD0NOGh3bITCnNN9Z3x8/mI7vQQOlSe9tyTtxCP1itQ==} '@design.estate/dees-comms@1.0.30': resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==} @@ -2516,6 +2516,9 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + echarts@5.6.0: + resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2866,8 +2869,8 @@ packages: humanize-ms@1.2.1: resolution: {integrity: sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=} - ibantools@4.5.1: - resolution: {integrity: sha512-DfKQpLlFq9yEUIEnFuCJzss3XavD7iHZTU5PyqXiAJ+rmaMp+NFP3hboumHKuK8nZjuOJg93WemTzcQ5b9jOZA==} + ibantools@4.5.2: + resolution: {integrity: sha512-is+8TgZcKS/AMv/z9nW1zz0bhjhoyjpA1p0nc3A6GkW/InOdcQiUZpkufADzh/aO/LY/TOD/P3oPWncNRn5QMA==} iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} @@ -4068,6 +4071,9 @@ packages: tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -4315,6 +4321,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zrender@5.6.1: + resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -4342,7 +4351,7 @@ snapshots: '@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3) '@cloudflare/workers-types': 4.20260317.1 - '@design.estate/dees-catalog': 3.55.1(@tiptap/pm@2.27.2) + '@design.estate/dees-catalog': 3.55.5(@tiptap/pm@2.27.2) '@design.estate/dees-comms': 1.0.30 '@push.rocks/lik': 6.4.0 '@push.rocks/smartdelay': 3.0.5 @@ -4871,7 +4880,7 @@ snapshots: dependencies: '@api.global/typedrequest-interfaces': 3.0.19 - '@design.estate/dees-catalog@3.55.1(@tiptap/pm@2.27.2)': + '@design.estate/dees-catalog@3.55.5(@tiptap/pm@2.27.2)': dependencies: '@design.estate/dees-domtools': 2.5.4 '@design.estate/dees-element': 2.2.4 @@ -4891,8 +4900,9 @@ snapshots: '@tiptap/extension-underline': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) '@tiptap/starter-kit': 2.27.2 '@tsclass/tsclass': 9.5.0 + echarts: 5.6.0 highlight.js: 11.11.1 - ibantools: 4.5.1 + ibantools: 4.5.2 lightweight-charts: 5.1.0 lucide: 0.577.0 monaco-editor: 0.55.1 @@ -6909,7 +6919,7 @@ snapshots: '@serve.zone/catalog@2.11.0(@tiptap/pm@2.27.2)': dependencies: - '@design.estate/dees-catalog': 3.55.1(@tiptap/pm@2.27.2) + '@design.estate/dees-catalog': 3.55.5(@tiptap/pm@2.27.2) '@design.estate/dees-domtools': 2.5.4 '@design.estate/dees-element': 2.2.4 '@design.estate/dees-wcctools': 3.8.0 @@ -7978,6 +7988,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + echarts@5.6.0: + dependencies: + tslib: 2.3.0 + zrender: 5.6.1 + emoji-regex@8.0.0: {} encoding-japanese@2.2.0: {} @@ -8410,7 +8425,7 @@ snapshots: dependencies: ms: 2.1.3 - ibantools@4.5.1: {} + ibantools@4.5.2: {} iconv-lite@0.4.24: dependencies: @@ -9942,6 +9957,8 @@ snapshots: tslib@1.14.1: {} + tslib@2.3.0: {} + tslib@2.8.1: {} tsx@4.21.0: @@ -10169,4 +10186,8 @@ snapshots: zod@3.25.76: {} + zrender@5.6.1: + dependencies: + tslib: 2.3.0 + zwitch@2.0.4: {} diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 5011ad5..2205229 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: '12.8.1', + version: '12.9.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/classes.storage-cert-manager.ts b/ts/classes.storage-cert-manager.ts index 8067207..700bd2a 100644 --- a/ts/classes.storage-cert-manager.ts +++ b/ts/classes.storage-cert-manager.ts @@ -29,9 +29,9 @@ export class StorageBackedCertManager implements plugins.smartacme.ICertManager let doc = await AcmeCertDoc.findByDomain(cert.domainName); if (!doc) { doc = new AcmeCertDoc(); + doc.id = cert.id; doc.domainName = cert.domainName; } - doc.id = cert.id; doc.created = cert.created; doc.privateKey = cert.privateKey; doc.publicKey = cert.publicKey; diff --git a/ts/monitoring/classes.metricsmanager.ts b/ts/monitoring/classes.metricsmanager.ts index c0b97c5..f1e78e2 100644 --- a/ts/monitoring/classes.metricsmanager.ts +++ b/ts/monitoring/classes.metricsmanager.ts @@ -591,6 +591,11 @@ export class MetricsManager { const requestsPerSecond = proxyMetrics.requests.perSecond(); const requestsTotal = proxyMetrics.requests.total(); + // Get frontend/backend protocol distribution (available in SmartProxy >= next release) + const conn = proxyMetrics.connections as any; + const frontendProtocols = conn.frontendProtocols?.() ?? null; + const backendProtocols = conn.backendProtocols?.() ?? null; + // Collect backend protocol data const backendMetrics = proxyMetrics.backends.byBackend(); const protocolCache = proxyMetrics.backends.detectedProtocols(); @@ -705,6 +710,8 @@ export class MetricsManager { requestsPerSecond, requestsTotal, backends, + frontendProtocols, + backendProtocols, }; }, 1000); // 1s cache — matches typical dashboard poll interval } diff --git a/ts/opsserver/handlers/stats.handler.ts b/ts/opsserver/handlers/stats.handler.ts index 054753a..052eeb9 100644 --- a/ts/opsserver/handlers/stats.handler.ts +++ b/ts/opsserver/handlers/stats.handler.ts @@ -311,6 +311,8 @@ export class StatsHandler { requestsPerSecond: stats.requestsPerSecond || 0, requestsTotal: stats.requestsTotal || 0, backends: stats.backends || [], + frontendProtocols: stats.frontendProtocols || null, + backendProtocols: stats.backendProtocols || null, }; })() ); diff --git a/ts_interfaces/data/stats.ts b/ts_interfaces/data/stats.ts index 6b2df46..469ecf4 100644 --- a/ts_interfaces/data/stats.ts +++ b/ts_interfaces/data/stats.ts @@ -166,6 +166,21 @@ export interface INetworkMetrics { requestsPerSecond?: number; requestsTotal?: number; backends?: IBackendInfo[]; + frontendProtocols?: IProtocolDistribution | null; + backendProtocols?: IProtocolDistribution | null; +} + +export interface IProtocolDistribution { + h1Active: number; + h1Total: number; + h2Active: number; + h2Total: number; + h3Active: number; + h3Total: number; + wsActive: number; + wsTotal: number; + otherActive: number; + otherTotal: number; } export interface IConnectionDetails { diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 5011ad5..2205229 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: '12.8.1', + version: '12.9.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index 9df3dcf..f096a07 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -56,6 +56,8 @@ export interface INetworkState { requestsPerSecond: number; requestsTotal: number; backends: interfaces.data.IBackendInfo[]; + frontendProtocols: interfaces.data.IProtocolDistribution | null; + backendProtocols: interfaces.data.IProtocolDistribution | null; lastUpdated: number; isLoading: boolean; error: string | null; @@ -154,6 +156,8 @@ export const networkStatePart = await appState.getStatePart( requestsPerSecond: 0, requestsTotal: 0, backends: [], + frontendProtocols: null, + backendProtocols: null, lastUpdated: 0, isLoading: false, error: null, @@ -523,6 +527,8 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat requestsPerSecond: networkStatsResponse.requestsPerSecond || 0, requestsTotal: networkStatsResponse.requestsTotal || 0, backends: networkStatsResponse.backends || [], + frontendProtocols: networkStatsResponse.frontendProtocols || null, + backendProtocols: networkStatsResponse.backendProtocols || null, lastUpdated: Date.now(), isLoading: false, error: null, @@ -1845,6 +1851,8 @@ async function dispatchCombinedRefreshActionInner() { requestsPerSecond: network.requestsPerSecond || 0, requestsTotal: network.requestsTotal || 0, backends: network.backends || [], + frontendProtocols: network.frontendProtocols || null, + backendProtocols: network.backendProtocols || null, lastUpdated: Date.now(), isLoading: false, error: null, @@ -1866,6 +1874,8 @@ async function dispatchCombinedRefreshActionInner() { requestsPerSecond: network.requestsPerSecond || 0, requestsTotal: network.requestsTotal || 0, backends: network.backends || [], + frontendProtocols: network.frontendProtocols || null, + backendProtocols: network.backendProtocols || null, lastUpdated: Date.now(), isLoading: false, error: null, diff --git a/ts_web/elements/ops-view-network.ts b/ts_web/elements/ops-view-network.ts index 8bcb799..870d6ff 100644 --- a/ts_web/elements/ops-view-network.ts +++ b/ts_web/elements/ops-view-network.ts @@ -274,6 +274,12 @@ export class OpsViewNetwork extends DeesElement { background: ${cssManager.bdTheme('#fff3e0', '#3a2a1a')}; color: ${cssManager.bdTheme('#f57c00', '#ff9933')}; } + + .protocolChartGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + } `, ]; @@ -305,6 +311,9 @@ export class OpsViewNetwork extends DeesElement { .yAxisFormatter=${(val: number) => `${val} Mbit/s`} > + + ${this.renderProtocolCharts()} + ${this.renderTopIPs()} @@ -527,7 +536,54 @@ export class OpsViewNetwork extends DeesElement { `; } - + private renderProtocolCharts(): TemplateResult { + const fp = this.networkState.frontendProtocols; + const bp = this.networkState.backendProtocols; + + const protoColors: Record = { + 'HTTP/1.1': '#1976d2', + 'HTTP/2': '#388e3c', + 'HTTP/3': '#7b1fa2', + 'WebSocket': '#f57c00', + 'Other': '#757575', + }; + + const buildDonutData = (dist: interfaces.data.IProtocolDistribution | null) => { + if (!dist) return []; + const items: Array<{ name: string; value: number; color: string }> = []; + if (dist.h1Active > 0) items.push({ name: 'HTTP/1.1', value: dist.h1Active, color: protoColors['HTTP/1.1'] }); + if (dist.h2Active > 0) items.push({ name: 'HTTP/2', value: dist.h2Active, color: protoColors['HTTP/2'] }); + if (dist.h3Active > 0) items.push({ name: 'HTTP/3', value: dist.h3Active, color: protoColors['HTTP/3'] }); + if (dist.wsActive > 0) items.push({ name: 'WebSocket', value: dist.wsActive, color: protoColors['WebSocket'] }); + if (dist.otherActive > 0) items.push({ name: 'Other', value: dist.otherActive, color: protoColors['Other'] }); + return items; + }; + + const frontendData = buildDonutData(fp); + const backendData = buildDonutData(bp); + + return html` +
+ 0 ? frontendData : [{ name: 'No Traffic', value: 1, color: '#757575' }]} + .showLegend=${true} + .showLabels=${true} + .innerRadiusPercent=${'55%'} + .valueFormatter=${(val: number) => `${val} active`} + > + 0 ? backendData : [{ name: 'No Traffic', value: 1, color: '#757575' }]} + .showLegend=${true} + .showLabels=${true} + .innerRadiusPercent=${'55%'} + .valueFormatter=${(val: number) => `${val} active`} + > +
+ `; + } + private renderTopIPs(): TemplateResult { if (this.networkState.topIPs.length === 0) { return html``;