Compare commits

...

10 Commits

Author SHA1 Message Date
a77ec6884a v12.8.0
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 19:08:46 +00:00
6112e4e884 feat(certificates): add force renew option for domain certificate reprovisioning 2026-04-03 19:08:46 +00:00
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
15 changed files with 289 additions and 67 deletions

View File

@@ -1,5 +1,35 @@
# Changelog
## 2026-04-03 - 12.8.0 - feat(certificates)
add force renew option for domain certificate reprovisioning
- pass an optional forceRenew flag through certificate reprovision requests from the UI to the ops handler
- use smartacme forceRenew support and return renewal-specific success messages
- update the SmartAcme dependency to version ^9.4.0
## 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

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "12.6.3",
"version": "12.8.0",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -35,12 +35,12 @@
"@api.global/typedserver": "^8.4.6",
"@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.51.2",
"@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",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^9.3.1",
"@push.rocks/smartacme": "^9.4.0",
"@push.rocks/smartdata": "^7.1.3",
"@push.rocks/smartdb": "^2.1.1",
"@push.rocks/smartdns": "^7.9.0",

24
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.2
version: 3.51.2(@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
@@ -39,8 +39,8 @@ importers:
specifier: ^6.1.3
version: 6.1.3
'@push.rocks/smartacme':
specifier: ^9.3.1
version: 9.3.1(socks@2.8.7)
specifier: ^9.4.0
version: 9.4.0(socks@2.8.7)
'@push.rocks/smartdata':
specifier: ^7.1.3
version: 7.1.3(socks@2.8.7)
@@ -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.2':
resolution: {integrity: sha512-yEkHJ+fo1Igm4V9g+Tbs7/0Cofk9D0Y88p7d3Ms0GWBJMXuPhR3p7TKPn4nmqBtCN9UTeN6fcRCepWunFEPnVg==}
'@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==}
@@ -1108,8 +1108,8 @@ packages:
'@push.rocks/qenv@6.1.3':
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
'@push.rocks/smartacme@9.3.1':
resolution: {integrity: sha512-Cl1DVQ+rfpaYkk6VVm/KYVeUYzWfXzSfTXybHfCZ5SuiACuTVHZ6jK8TouELaV1RgrdYnIp0MrbiY2Kqi8ayAw==}
'@push.rocks/smartacme@9.4.0':
resolution: {integrity: sha512-mSqsI859mHI9fCZxLfayzPf/WvukDFzVHOh02vXq3ujxbb5M+ArMnXe0MmC2egR9GeXmQTm3DTENaETX5ffMtw==}
'@push.rocks/smartarchive@4.2.4':
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
@@ -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.2(@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.2(@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
@@ -5960,7 +5960,7 @@ snapshots:
'@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartacme@9.3.1(socks@2.8.7)':
'@push.rocks/smartacme@9.4.0(socks@2.8.7)':
dependencies:
'@apiclient.xyz/cloudflare': 7.1.0
'@peculiar/x509': 2.0.0
@@ -6909,7 +6909,7 @@ snapshots:
'@serve.zone/catalog@2.11.0(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-catalog': 3.51.2(@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

View File

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

View File

@@ -43,7 +43,7 @@ export class CertificateHandler {
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
'reprovisionCertificateDomain',
async (dataArg) => {
return this.reprovisionCertificateDomain(dataArg.domain);
return this.reprovisionCertificateDomain(dataArg.domain, dataArg.forceRenew);
}
)
);
@@ -318,7 +318,7 @@ export class CertificateHandler {
/**
* Domain-based reprovisioning — clears backoff first, then triggers provision
*/
private async reprovisionCertificateDomain(domain: string): Promise<{ success: boolean; message?: string }> {
private async reprovisionCertificateDomain(domain: string, forceRenew?: boolean): Promise<{ success: boolean; message?: string }> {
const dcRouter = this.opsServerRef.dcRouterRef;
const smartProxy = dcRouter.smartProxy;
@@ -337,8 +337,8 @@ export class CertificateHandler {
// Try to provision via SmartAcme directly
if (dcRouter.smartAcme) {
try {
await dcRouter.smartAcme.getCertificateForDomain(domain);
return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}'` };
await dcRouter.smartAcme.getCertificateForDomain(domain, { forceRenew: forceRenew ?? false });
return { success: true, message: forceRenew ? `Certificate force-renewed for domain '${domain}'` : `Certificate reprovisioning triggered for domain '${domain}'` };
} catch (err: unknown) {
return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
}

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

@@ -68,6 +68,7 @@ export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestI
request: {
identity: authInterfaces.IIdentity;
domain: string;
forceRenew?: boolean;
};
response: {
success: boolean;

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.3',
version: '12.8.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,
@@ -597,8 +605,8 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
}
});
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
async (statePartArg, domain, actionContext): Promise<ICertificateState> => {
export const reprovisionCertificateAction = certificateStatePart.createAction<{ domain: string; forceRenew?: boolean }>(
async (statePartArg, dataArg, actionContext): Promise<ICertificateState> => {
const context = getActionContext();
const currentState = statePartArg.getState()!;
@@ -609,7 +617,8 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
await request.fire({
identity: context.identity!,
domain,
domain: dataArg.domain,
forceRenew: dataArg.forceRenew,
});
// Re-fetch overview after reprovisioning
@@ -1781,6 +1790,8 @@ async function dispatchCombinedRefreshActionInner() {
dns: true,
security: true,
network: currentView === 'network', // Only fetch network if on network view
radius: true,
vpn: true,
},
});
@@ -1792,6 +1803,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

@@ -312,14 +312,16 @@ export class OpsViewCertificates extends DeesElement {
return;
}
const doReprovision = async () => {
const doReprovision = async (forceRenew = false) => {
await appstate.certificateStatePart.dispatchAction(
appstate.reprovisionCertificateAction,
cert.domain,
{ domain: cert.domain, forceRenew },
);
const { DeesToast } = await import('@design.estate/dees-catalog');
DeesToast.show({
message: `Reprovisioning triggered for ${cert.domain}`,
message: forceRenew
? `Force renewal triggered for ${cert.domain}`
: `Reprovisioning triggered for ${cert.domain}`,
type: 'success',
duration: 3000,
});
@@ -336,7 +338,7 @@ export class OpsViewCertificates extends DeesElement {
name: 'Force Renew',
action: async (modalArg: any) => {
await modalArg.destroy();
await doReprovision();
await doReprovision(true);
},
},
],

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,