Compare commits

..

12 Commits

Author SHA1 Message Date
4a6913d4bb v12.7.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-04-03 14:11:17 +00:00
f6a9e344e5 feat(opsserver): add RADIUS and VPN metrics to combined ops stats and overview dashboards, and stream live log buffer entries in follow mode 2026-04-03 14:11:17 +00:00
b3296c6522 v12.6.6
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 13:53:20 +00:00
10a2b922d3 fix(deps): bump @design.estate/dees-catalog to ^3.52.3 2026-04-03 13:53:20 +00:00
ee5cdde225 v12.6.5
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-04-03 13:40:52 +00:00
d2e9efccd0 fix(deps): bump @design.estate/dees-catalog to ^3.52.2 2026-04-03 13:40:51 +00:00
a07901a28a v12.6.4
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 13:02:07 +00:00
a3954d6eb5 fix(deps): bump @design.estate/dees-catalog to ^3.52.0 2026-04-03 13:02:07 +00:00
9685fcd89d v12.6.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-04-03 12:39:02 +00:00
74c23ce5ff fix(deps): bump @types/node and @design.estate/dees-catalog patch versions 2026-04-03 12:39:02 +00:00
746fbb15e6 v12.6.2
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 10:48:26 +00:00
415065b246 fix(deps): bump @design.estate/dees-catalog to ^3.51.1 2026-04-03 10:48:26 +00:00
12 changed files with 294 additions and 72 deletions

View File

@@ -1,5 +1,39 @@
# Changelog
## 2026-04-03 - 12.7.0 - feat(opsserver)
add RADIUS and VPN metrics to combined ops stats and overview dashboards, and stream live log buffer entries in follow mode
- Expose RADIUS and VPN sections in the combined stats API and shared TypeScript interfaces
- Populate frontend app state and overview tiles with RADIUS authentication, session, traffic, and VPN client metrics
- Replace simulated follow-mode log events with real log buffer tailing and timestamp-based incremental streaming
- Use commit metadata for reported server version instead of a hardcoded value
## 2026-04-03 - 12.6.6 - fix(deps)
bump @design.estate/dees-catalog to ^3.52.3
- Updates @design.estate/dees-catalog from ^3.52.2 to ^3.52.3 in package.json
## 2026-04-03 - 12.6.5 - fix(deps)
bump @design.estate/dees-catalog to ^3.52.2
- Updates the @design.estate/dees-catalog dependency from ^3.52.0 to ^3.52.2 in package.json.
## 2026-04-03 - 12.6.4 - fix(deps)
bump @design.estate/dees-catalog to ^3.52.0
- Updates the @design.estate/dees-catalog dependency from ^3.51.2 to ^3.52.0 in package.json.
## 2026-04-03 - 12.6.3 - fix(deps)
bump @types/node and @design.estate/dees-catalog patch versions
- updates @types/node from ^25.5.1 to ^25.5.2
- updates @design.estate/dees-catalog from ^3.51.1 to ^3.51.2
## 2026-04-03 - 12.6.2 - fix(deps)
bump @design.estate/dees-catalog to ^3.51.1
- Updates @design.estate/dees-catalog from ^3.51.0 to ^3.51.1 in package.json
## 2026-04-03 - 12.6.1 - fix(repo)
no changes to commit

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "12.6.1",
"version": "12.7.0",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -27,7 +27,7 @@
"@git.zone/tsrun": "^2.0.2",
"@git.zone/tstest": "^3.6.3",
"@git.zone/tswatch": "^3.3.2",
"@types/node": "^25.5.1"
"@types/node": "^25.5.2"
},
"dependencies": {
"@api.global/typedrequest": "^3.3.0",
@@ -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.51.0",
"@design.estate/dees-catalog": "^3.52.3",
"@design.estate/dees-element": "^2.2.4",
"@push.rocks/lik": "^6.4.0",
"@push.rocks/projectinfo": "^5.1.0",

56
pnpm-lock.yaml generated
View File

@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.0
version: 7.1.0
'@design.estate/dees-catalog':
specifier: ^3.51.0
version: 3.51.0(@tiptap/pm@2.27.2)
specifier: ^3.52.3
version: 3.52.3(@tiptap/pm@2.27.2)
'@design.estate/dees-element':
specifier: ^2.2.4
version: 2.2.4
@@ -142,8 +142,8 @@ importers:
specifier: ^3.3.2
version: 3.3.2(@tiptap/pm@2.27.2)
'@types/node':
specifier: ^25.5.1
version: 25.5.1
specifier: ^25.5.2
version: 25.5.2
packages:
@@ -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.51.0':
resolution: {integrity: sha512-2SHWXNni802RgilzWBM9WUkoexguT1HjKWErswzj6oecnJpD6MXomJ5/tvdKVRDd4oA+l6IqtgiVIewdFVPfUg==}
'@design.estate/dees-catalog@3.52.3':
resolution: {integrity: sha512-4/vybRZQtdkpa3IOZQ/EbL6gGjIMO+430db43RRfdw+HPXird7Jl+oIXz6pDh+rGah2H2+Srb/c+eve46xAhtQ==}
'@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -2050,11 +2050,11 @@ packages:
'@types/node@18.19.130':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/node@22.19.16':
resolution: {integrity: sha512-K6csxIjY+9RoDxdP6/wzaJzXaCf4znBz0/y0rrQDsbqmzQ5QFsOjubbsYWZhj6ZCgz3mjlyDZS+EJkhA9jWl9Q==}
'@types/node@22.19.17':
resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==}
'@types/node@25.5.1':
resolution: {integrity: sha512-lgrR3HRNQdTEeeXBnLURFO4JIIbpcVcMlLM9IG0jsNRTRNSbMkm9S2hyhxhnokke1NM25Dr9QghgeB5PQKolrw==}
'@types/node@25.5.2':
resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==}
'@types/qrcode@1.5.6':
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
@@ -4342,7 +4342,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.51.0(@tiptap/pm@2.27.2)
'@design.estate/dees-catalog': 3.52.3(@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 +4871,7 @@ snapshots:
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.51.0(@tiptap/pm@2.27.2)':
'@design.estate/dees-catalog@3.52.3(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
@@ -5372,7 +5372,7 @@ snapshots:
'@inquirer/figures': 1.0.15
'@inquirer/type': 2.0.0
'@types/mute-stream': 0.0.4
'@types/node': 22.19.16
'@types/node': 22.19.17
'@types/wrap-ansi': 3.0.0
ansi-escapes: 4.3.2
cli-width: 4.1.0
@@ -6909,7 +6909,7 @@ snapshots:
'@serve.zone/catalog@2.11.0(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-catalog': 3.51.0(@tiptap/pm@2.27.2)
'@design.estate/dees-catalog': 3.52.3(@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
@@ -7457,7 +7457,7 @@ snapshots:
'@types/clean-css@4.2.11':
dependencies:
'@types/node': 25.5.1
'@types/node': 25.5.2
source-map: 0.6.1
'@types/debug@4.1.13':
@@ -7467,7 +7467,7 @@ snapshots:
'@types/fs-extra@11.0.4':
dependencies:
'@types/jsonfile': 6.1.4
'@types/node': 25.5.1
'@types/node': 25.5.2
'@types/hast@3.0.4':
dependencies:
@@ -7487,12 +7487,12 @@ snapshots:
'@types/jsonfile@6.1.4':
dependencies:
'@types/node': 25.5.1
'@types/node': 25.5.2
'@types/jsonwebtoken@9.0.10':
dependencies:
'@types/ms': 2.1.0
'@types/node': 25.5.1
'@types/node': 25.5.2
'@types/linkify-it@5.0.0': {}
@@ -7513,16 +7513,16 @@ snapshots:
'@types/mute-stream@0.0.4':
dependencies:
'@types/node': 25.5.1
'@types/node': 25.5.2
'@types/node-fetch@2.6.13':
dependencies:
'@types/node': 25.5.1
'@types/node': 25.5.2
form-data: 4.0.5
'@types/node-forge@1.3.14':
dependencies:
'@types/node': 25.5.1
'@types/node': 25.5.2
'@types/node@16.9.1': {}
@@ -7530,17 +7530,17 @@ snapshots:
dependencies:
undici-types: 5.26.5
'@types/node@22.19.16':
'@types/node@22.19.17':
dependencies:
undici-types: 6.21.0
'@types/node@25.5.1':
'@types/node@25.5.2':
dependencies:
undici-types: 7.18.2
'@types/qrcode@1.5.6':
dependencies:
'@types/node': 25.5.1
'@types/node': 25.5.2
'@types/randomatic@3.1.5': {}
@@ -7550,11 +7550,11 @@ snapshots:
'@types/tar-stream@3.1.4':
dependencies:
'@types/node': 25.5.1
'@types/node': 25.5.2
'@types/through2@2.0.41':
dependencies:
'@types/node': 25.5.1
'@types/node': 25.5.2
'@types/trusted-types@2.0.7': {}
@@ -7584,11 +7584,11 @@ snapshots:
'@types/ws@8.18.1':
dependencies:
'@types/node': 25.5.1
'@types/node': 25.5.2
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 25.5.1
'@types/node': 25.5.2
optional: true
'@ungap/structured-clone@1.3.0': {}

View File

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

View File

@@ -255,7 +255,7 @@ export class LogsHandler {
} {
let intervalId: NodeJS.Timeout | null = null;
let stopped = false;
let logIndex = 0;
let lastTimestamp = Date.now();
const stop = () => {
stopped = true;
@@ -284,53 +284,65 @@ export class LogsHandler {
return;
}
// For follow mode, simulate real-time log streaming
// For follow mode, tail real log entries from the in-memory buffer
intervalId = setInterval(async () => {
if (stopped) {
// Guard: clear interval if stop() was called between ticks
clearInterval(intervalId!);
intervalId = null;
return;
}
const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
// Fetch new entries since last poll
const rawEntries = logBuffer.getEntries({
since: lastTimestamp,
limit: 50,
});
const mockCategory = categories[Math.floor(Math.random() * categories.length)];
const mockLevel = levels[Math.floor(Math.random() * levels.length)];
if (rawEntries.length === 0) return;
// Filter by requested criteria
if (levelFilter && !levelFilter.includes(mockLevel)) return;
if (categoryFilter && !categoryFilter.includes(mockCategory)) return;
for (const raw of rawEntries) {
const mappedLevel = LogsHandler.mapLogLevel(raw.level);
const mappedCategory = LogsHandler.deriveCategory(
(raw as any).context?.zone,
raw.message,
);
const logEntry = {
timestamp: Date.now(),
level: mockLevel,
category: mockCategory,
message: `Real-time log ${logIndex++} from ${mockCategory}`,
metadata: {
requestId: plugins.uuid.v4(),
},
};
// Apply filters
if (levelFilter && !levelFilter.includes(mappedLevel)) continue;
if (categoryFilter && !categoryFilter.includes(mappedCategory)) continue;
const logData = JSON.stringify(logEntry);
const encoder = new TextEncoder();
try {
// Use a timeout to detect hung streams (sendData can hang if the
// VirtualStream's keepAlive loop has ended)
let timeoutHandle: ReturnType<typeof setTimeout>;
await Promise.race([
virtualStream.sendData(encoder.encode(logData)).then((result) => {
clearTimeout(timeoutHandle);
return result;
}),
new Promise<never>((_, reject) => {
timeoutHandle = setTimeout(() => reject(new Error('stream send timeout')), 10_000);
}),
]);
} catch {
// Stream closed, errored, or timed out — clean up
stop();
const logEntry = {
timestamp: raw.timestamp || Date.now(),
level: mappedLevel,
category: mappedCategory,
message: raw.message,
metadata: (raw as any).data,
};
const logData = JSON.stringify(logEntry);
const encoder = new TextEncoder();
try {
let timeoutHandle: ReturnType<typeof setTimeout>;
await Promise.race([
virtualStream.sendData(encoder.encode(logData)).then((result) => {
clearTimeout(timeoutHandle);
return result;
}),
new Promise<never>((_, reject) => {
timeoutHandle = setTimeout(() => reject(new Error('stream send timeout')), 10_000);
}),
]);
} catch {
// Stream closed, errored, or timed out — clean up
stop();
return;
}
}
// Advance the watermark past all entries we just processed
const newest = rawEntries[rawEntries.length - 1];
if (newest.timestamp && newest.timestamp >= lastTimestamp) {
lastTimestamp = newest.timestamp + 1;
}
}, 2000);
};

View File

@@ -3,6 +3,7 @@ import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js';
import { MetricsManager } from '../../monitoring/index.js';
import { SecurityLogger } from '../../security/classes.securitylogger.js';
import { commitinfo } from '../../00_commitinfo_data.js';
export class StatsHandler {
constructor(private opsServerRef: OpsServer) {
@@ -158,7 +159,7 @@ export class StatsHandler {
};
return acc;
}, {} as any),
version: '2.12.0', // TODO: Get from package.json
version: commitinfo.version,
},
};
}
@@ -314,7 +315,47 @@ export class StatsHandler {
})()
);
}
if (sections.radius) {
promises.push(
(async () => {
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
if (!radiusServer) return;
const stats = radiusServer.getStats();
const accountingStats = radiusServer.getAccountingManager().getStats();
metrics.radius = {
running: stats.running,
uptime: stats.uptime,
authRequests: stats.authRequests,
authAccepts: stats.authAccepts,
authRejects: stats.authRejects,
accountingRequests: stats.accountingRequests,
activeSessions: stats.activeSessions,
totalInputBytes: accountingStats.totalInputBytes,
totalOutputBytes: accountingStats.totalOutputBytes,
};
})()
);
}
if (sections.vpn) {
promises.push(
(async () => {
const vpnManager = this.opsServerRef.dcRouterRef.vpnManager;
const vpnConfig = this.opsServerRef.dcRouterRef.options.vpnConfig;
if (!vpnManager) return;
const connected = await vpnManager.getConnectedClients();
metrics.vpn = {
running: vpnManager.running,
subnet: vpnManager.getSubnet(),
registeredClients: vpnManager.listClients().length,
connectedClients: connected.length,
wgListenPort: vpnConfig?.wgListenPort ?? 51820,
};
})()
);
}
await Promise.all(promises);
return {

View File

@@ -197,4 +197,24 @@ export interface IBackendInfo {
h3ConsecutiveFailures: number | null;
h3Port: number | null;
cacheAgeSecs: number | null;
}
export interface IRadiusStats {
running: boolean;
uptime: number;
authRequests: number;
authAccepts: number;
authRejects: number;
accountingRequests: number;
activeSessions: number;
totalInputBytes: number;
totalOutputBytes: number;
}
export interface IVpnStats {
running: boolean;
subnet: string;
registeredClients: number;
connectedClients: number;
wgListenPort: number;
}

View File

@@ -10,6 +10,8 @@ export interface IReq_GetCombinedMetrics {
dns?: boolean;
security?: boolean;
network?: boolean;
radius?: boolean;
vpn?: boolean;
};
};
response: {
@@ -19,6 +21,8 @@ export interface IReq_GetCombinedMetrics {
dns?: data.IDnsStats;
security?: data.ISecurityMetrics;
network?: data.INetworkMetrics;
radius?: data.IRadiusStats;
vpn?: data.IVpnStats;
};
timestamp: number;
};

View File

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

View File

@@ -15,6 +15,8 @@ export interface IStatsState {
emailStats: interfaces.data.IEmailStats | null;
dnsStats: interfaces.data.IDnsStats | null;
securityMetrics: interfaces.data.ISecurityMetrics | null;
radiusStats: interfaces.data.IRadiusStats | null;
vpnStats: interfaces.data.IVpnStats | null;
lastUpdated: number;
isLoading: boolean;
error: string | null;
@@ -91,6 +93,8 @@ export const statsStatePart = await appState.getStatePart<IStatsState>(
emailStats: null,
dnsStats: null,
securityMetrics: null,
radiusStats: null,
vpnStats: null,
lastUpdated: 0,
isLoading: false,
error: null,
@@ -319,6 +323,8 @@ export const fetchAllStatsAction = statsStatePart.createAction(async (statePartA
dns: true,
security: true,
network: false, // Network is fetched separately for the network view
radius: true,
vpn: true,
},
});
@@ -328,6 +334,8 @@ export const fetchAllStatsAction = statsStatePart.createAction(async (statePartA
emailStats: combinedResponse.metrics.email || currentState.emailStats,
dnsStats: combinedResponse.metrics.dns || currentState.dnsStats,
securityMetrics: combinedResponse.metrics.security || currentState.securityMetrics,
radiusStats: combinedResponse.metrics.radius || currentState.radiusStats,
vpnStats: combinedResponse.metrics.vpn || currentState.vpnStats,
lastUpdated: Date.now(),
isLoading: false,
error: null,
@@ -1781,6 +1789,8 @@ async function dispatchCombinedRefreshActionInner() {
dns: true,
security: true,
network: currentView === 'network', // Only fetch network if on network view
radius: true,
vpn: true,
},
});
@@ -1792,6 +1802,8 @@ async function dispatchCombinedRefreshActionInner() {
emailStats: combinedResponse.metrics.email || currentStatsState.emailStats,
dnsStats: combinedResponse.metrics.dns || currentStatsState.dnsStats,
securityMetrics: combinedResponse.metrics.security || currentStatsState.securityMetrics,
radiusStats: combinedResponse.metrics.radius || currentStatsState.radiusStats,
vpnStats: combinedResponse.metrics.vpn || currentStatsState.vpnStats,
lastUpdated: Date.now(),
isLoading: false,
error: null,

View File

@@ -21,6 +21,8 @@ export class OpsViewOverview extends DeesElement {
emailStats: null,
dnsStats: null,
securityMetrics: null,
radiusStats: null,
vpnStats: null,
lastUpdated: 0,
isLoading: false,
error: null,
@@ -117,6 +119,10 @@ export class OpsViewOverview extends DeesElement {
${this.renderDnsStats()}
${this.renderRadiusStats()}
${this.renderVpnStats()}
<div class="chartGrid">
<dees-chart-area
.label=${'Email Traffic (24h)'}
@@ -378,6 +384,97 @@ export class OpsViewOverview extends DeesElement {
`;
}
private renderRadiusStats(): TemplateResult {
if (!this.statsState.radiusStats) return html``;
const stats = this.statsState.radiusStats;
const authTotal = stats.authRequests || 0;
const acceptRate = authTotal > 0 ? ((stats.authAccepts / authTotal) * 100).toFixed(1) : '0.0';
const tiles: IStatsTile[] = [
{
id: 'radiusStatus',
title: 'RADIUS Status',
value: stats.running ? 'Running' : 'Stopped',
type: 'text',
icon: 'lucide:ShieldCheck',
color: stats.running ? '#22c55e' : '#ef4444',
description: stats.running ? `Uptime: ${this.formatUptime(stats.uptime / 1000)}` : undefined,
},
{
id: 'authRequests',
title: 'Auth Requests',
value: stats.authRequests,
type: 'number',
icon: 'lucide:KeyRound',
color: '#3b82f6',
description: `Accept rate: ${acceptRate}% (${stats.authAccepts} / ${stats.authRejects} rejected)`,
},
{
id: 'activeSessions',
title: 'Active Sessions',
value: stats.activeSessions,
type: 'number',
icon: 'lucide:Users',
color: '#8b5cf6',
},
{
id: 'radiusTraffic',
title: 'Data Transfer',
value: this.formatBytes(stats.totalInputBytes + stats.totalOutputBytes),
type: 'text',
icon: 'lucide:ArrowLeftRight',
color: '#f59e0b',
description: `In: ${this.formatBytes(stats.totalInputBytes)} / Out: ${this.formatBytes(stats.totalOutputBytes)}`,
},
];
return html`
<h2>RADIUS Statistics</h2>
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
`;
}
private renderVpnStats(): TemplateResult {
if (!this.statsState.vpnStats) return html``;
const stats = this.statsState.vpnStats;
const tiles: IStatsTile[] = [
{
id: 'vpnStatus',
title: 'VPN Status',
value: stats.running ? 'Running' : 'Stopped',
type: 'text',
icon: 'lucide:Shield',
color: stats.running ? '#22c55e' : '#ef4444',
description: `Subnet: ${stats.subnet}`,
},
{
id: 'connectedClients',
title: 'Connected Clients',
value: stats.connectedClients,
type: 'number',
icon: 'lucide:Wifi',
color: '#3b82f6',
description: `${stats.registeredClients} registered`,
},
{
id: 'wgPort',
title: 'WireGuard Port',
value: stats.wgListenPort,
type: 'number',
icon: 'lucide:Network',
color: '#8b5cf6',
},
];
return html`
<h2>VPN Statistics</h2>
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
`;
}
// --- Chart data helpers ---
private getRecentEventEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {

View File

@@ -20,6 +20,8 @@ export class OpsViewSecurity extends DeesElement {
emailStats: null,
dnsStats: null,
securityMetrics: null,
radiusStats: null,
vpnStats: null,
lastUpdated: 0,
isLoading: false,
error: null,