Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 15da996e70 | |||
| 582e19e6a6 | |||
| 79765d6729 | |||
| ffc93eb9d3 |
21
changelog.md
21
changelog.md
@@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-21 - 7.4.1 - fix(dcrouter)
|
||||
replace console logging with structured logger, improve metrics logging, add terminal-ready wait in ops UI, bump dees-catalog patch
|
||||
|
||||
- Replace console.log/console.error calls in classes.dcrouter.ts with structured logger.log (info/debug/error) including contextual data and stringified errors
|
||||
- MetricsManager: create a dedicated Smartlog instance (metricsLogger) for SmartMetrics and use shared logger for lifecycle events (start/stop)
|
||||
- SmartProxy/ACME: convert startup/stop/cert events and error logging to structured logs; include generated route and cert metadata where relevant
|
||||
- Shutdown/startup flows: unify service start/stop/error messages through logger to provide consistent, structured output
|
||||
- UI change: ops-view-logs now waits for xterm terminalReady before pushing initial logs to avoid race conditions
|
||||
- Bump dependency @design.estate/dees-catalog from 3.43.0 to 3.43.1
|
||||
|
||||
## 2026-02-21 - 7.4.0 - feat(opsserver)
|
||||
add real-time log push to ops dashboard and recent DNS query tracking
|
||||
|
||||
- Export baseLogger and add a log destination that pushes log entries to connected ops_dashboard TypedSocket clients (ts/opsserver/handlers/logs.handler.ts, ts/logger.ts).
|
||||
- Introduce a new TypedRequest (pushLogEntry) interface for server→client log pushes (ts_interfaces/requests/logs.ts) and wire client handling in the web UI (ts_web/appstate.ts, ts_web/plugins.ts).
|
||||
- Add TypedSocket client connection lifecycle to the web app, stream pushed log entries into app state and update log views incrementally (ts_web/appstate.ts, ts_web/elements/ops-view-logs.ts).
|
||||
- MetricsManager now records recent DNS queries (timestamp, domain, type, answered, responseTimeMs) and exposes them via stats endpoints for display in the UI (ts/monitoring/classes.metricsmanager.ts, ts/opsserver/handlers/stats.handler.ts, ts_interfaces/data/stats.ts).
|
||||
- UI overview now displays DNS query entries and uses answered flag to set log level (ts_web/elements/ops-view-overview.ts).
|
||||
- Add import/export for typedsocket in web plugins to enable real-time push (ts_web/plugins.ts).
|
||||
- Bump dependency @push.rocks/smartproxy patch version ^25.7.8 → ^25.7.9 (package.json).
|
||||
|
||||
## 2026-02-20 - 7.3.0 - feat(dcrouter)
|
||||
Wire DNS server 'query' events to MetricsManager for time-series tracking and bump @push.rocks/smartdns to ^7.9.0
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "7.3.0",
|
||||
"version": "7.4.1",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -32,7 +32,7 @@
|
||||
"@api.global/typedserver": "^8.3.0",
|
||||
"@api.global/typedsocket": "^4.1.0",
|
||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||
"@design.estate/dees-catalog": "^3.43.0",
|
||||
"@design.estate/dees-catalog": "^3.43.1",
|
||||
"@design.estate/dees-element": "^2.1.6",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
@@ -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": "^25.7.8",
|
||||
"@push.rocks/smartproxy": "^25.7.9",
|
||||
"@push.rocks/smartradius": "^1.1.1",
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
|
||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
'@design.estate/dees-catalog':
|
||||
specifier: ^3.43.0
|
||||
version: 3.43.0(@tiptap/pm@2.27.2)
|
||||
specifier: ^3.43.1
|
||||
version: 3.43.1(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-element':
|
||||
specifier: ^2.1.6
|
||||
version: 2.1.6
|
||||
@@ -75,8 +75,8 @@ importers:
|
||||
specifier: ^4.2.3
|
||||
version: 4.2.3
|
||||
'@push.rocks/smartproxy':
|
||||
specifier: ^25.7.8
|
||||
version: 25.7.8
|
||||
specifier: ^25.7.9
|
||||
version: 25.7.9
|
||||
'@push.rocks/smartradius':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
@@ -351,8 +351,8 @@ packages:
|
||||
'@configvault.io/interfaces@1.0.17':
|
||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||
|
||||
'@design.estate/dees-catalog@3.43.0':
|
||||
resolution: {integrity: sha512-UFW8oThP9Mc4L0wVVgmuGux868Ct/TwZ1WP8hZCe4e/+5gmxDc+4EArnt5hePHENboe1Soobh9mmrMN6kQZ3xQ==}
|
||||
'@design.estate/dees-catalog@3.43.1':
|
||||
resolution: {integrity: sha512-WAWOV8dIgdKfAbS4Ciek8oDVIWC0OSPODhpQdLlsGBXERcFaBPaYxcpywmrjXB/TFeoAQPxBxhS7jb9/p2Rprg==}
|
||||
|
||||
'@design.estate/dees-comms@1.0.30':
|
||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||
@@ -1030,8 +1030,8 @@ packages:
|
||||
'@push.rocks/smartpromise@4.2.3':
|
||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||
|
||||
'@push.rocks/smartproxy@25.7.8':
|
||||
resolution: {integrity: sha512-rKuC/5DgCBQmk1iCY2mZd+ZdH2mBOfcP1hWMARTP4Je4KqnNTJ2STM1tJmc9FmKVXxtEQCxWJnEnq1wNqwQFRA==}
|
||||
'@push.rocks/smartproxy@25.7.9':
|
||||
resolution: {integrity: sha512-5esFvD72TEyveaEQbDYRgD7C5hDfWMSBvurNx3KPi02CBKG1gnhx/WWT7RHDS3KRF5fEQh9YxvI9aMkOwjc7sQ==}
|
||||
|
||||
'@push.rocks/smartpuppeteer@2.0.5':
|
||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||
@@ -4218,13 +4218,11 @@ packages:
|
||||
|
||||
xterm-addon-fit@0.8.0:
|
||||
resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==}
|
||||
deprecated: This package is now deprecated. Move to @xterm/addon-fit instead.
|
||||
peerDependencies:
|
||||
xterm: ^5.0.0
|
||||
|
||||
xterm@5.3.0:
|
||||
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==}
|
||||
deprecated: This package is now deprecated. Move to @xterm/xterm instead.
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
@@ -4336,7 +4334,7 @@ snapshots:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
|
||||
'@cloudflare/workers-types': 4.20260210.0
|
||||
'@design.estate/dees-catalog': 3.43.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-catalog': 3.43.1(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-comms': 1.0.30
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
@@ -4934,7 +4932,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
|
||||
'@design.estate/dees-catalog@3.43.0(@tiptap/pm@2.27.2)':
|
||||
'@design.estate/dees-catalog@3.43.1(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.3.8
|
||||
'@design.estate/dees-element': 2.1.6
|
||||
@@ -6317,7 +6315,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/smartpromise@4.2.3': {}
|
||||
|
||||
'@push.rocks/smartproxy@25.7.8':
|
||||
'@push.rocks/smartproxy@25.7.9':
|
||||
dependencies:
|
||||
'@push.rocks/smartcrypto': 2.0.4
|
||||
'@push.rocks/smartlog': 3.2.1
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '7.3.0',
|
||||
version: '7.4.1',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -252,9 +252,7 @@ export class DcRouter {
|
||||
}
|
||||
|
||||
public async start() {
|
||||
console.log('╔═══════════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ Starting DcRouter Services ║');
|
||||
console.log('╚═══════════════════════════════════════════════════════════════════╝');
|
||||
logger.log('info', 'Starting DcRouter Services');
|
||||
|
||||
|
||||
this.opsServer = new OpsServer(this);
|
||||
@@ -296,7 +294,7 @@ export class DcRouter {
|
||||
|
||||
this.logStartupSummary();
|
||||
} catch (error) {
|
||||
console.error('❌ Error starting DcRouter:', error);
|
||||
logger.log('error', 'Error starting DcRouter', { error: String(error) });
|
||||
// Try to clean up any services that may have started
|
||||
await this.stop();
|
||||
throw error;
|
||||
@@ -307,104 +305,60 @@ export class DcRouter {
|
||||
* Log comprehensive startup summary
|
||||
*/
|
||||
private logStartupSummary(): void {
|
||||
console.log('\n╔═══════════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ DcRouter Started Successfully ║');
|
||||
console.log('╚═══════════════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
logger.log('info', 'DcRouter Started Successfully');
|
||||
|
||||
// Metrics summary
|
||||
if (this.metricsManager) {
|
||||
console.log('📊 Metrics Service:');
|
||||
console.log(' ├─ SmartMetrics: Active');
|
||||
console.log(' ├─ SmartProxy Stats: Active');
|
||||
console.log(' └─ Real-time tracking: Enabled');
|
||||
logger.log('info', 'Metrics Service: SmartMetrics active, SmartProxy stats active, real-time tracking enabled');
|
||||
}
|
||||
|
||||
|
||||
// SmartProxy summary
|
||||
if (this.smartProxy) {
|
||||
console.log('🌐 SmartProxy Service:');
|
||||
const routeCount = this.options.smartProxyConfig?.routes?.length || 0;
|
||||
console.log(` ├─ Routes configured: ${routeCount}`);
|
||||
console.log(` ├─ ACME enabled: ${this.options.smartProxyConfig?.acme?.enabled || false}`);
|
||||
if (this.options.smartProxyConfig?.acme?.enabled) {
|
||||
console.log(` ├─ ACME email: ${this.options.smartProxyConfig.acme.email || 'not set'}`);
|
||||
console.log(` └─ ACME mode: ${this.options.smartProxyConfig.acme.useProduction ? 'production' : 'staging'}`);
|
||||
} else {
|
||||
console.log(' └─ ACME: disabled');
|
||||
}
|
||||
const acmeEnabled = this.options.smartProxyConfig?.acme?.enabled || false;
|
||||
const acmeMode = acmeEnabled
|
||||
? `email=${this.options.smartProxyConfig!.acme!.email || 'not set'}, mode=${this.options.smartProxyConfig!.acme!.useProduction ? 'production' : 'staging'}`
|
||||
: 'disabled';
|
||||
logger.log('info', `SmartProxy Service: ${routeCount} routes, ACME: ${acmeMode}`);
|
||||
}
|
||||
|
||||
|
||||
// Email service summary
|
||||
if (this.emailServer && this.options.emailConfig) {
|
||||
console.log('\n📧 Email Service:');
|
||||
const ports = this.options.emailConfig.ports || [];
|
||||
console.log(` ├─ Ports: ${ports.join(', ')}`);
|
||||
console.log(` ├─ Hostname: ${this.options.emailConfig.hostname || 'localhost'}`);
|
||||
console.log(` ├─ Domains configured: ${this.options.emailConfig.domains?.length || 0}`);
|
||||
if (this.options.emailConfig.domains && this.options.emailConfig.domains.length > 0) {
|
||||
this.options.emailConfig.domains.forEach((domain, index) => {
|
||||
const isLast = index === this.options.emailConfig!.domains!.length - 1;
|
||||
console.log(` ${isLast ? '└─' : '├─'} ${domain.domain} (${domain.dnsMode || 'default'})`);
|
||||
});
|
||||
}
|
||||
console.log(` └─ DKIM: Initialized for all domains`);
|
||||
const domainCount = this.options.emailConfig.domains?.length || 0;
|
||||
const domainNames = this.options.emailConfig.domains?.map(d => `${d.domain} (${d.dnsMode || 'default'})`).join(', ') || 'none';
|
||||
logger.log('info', `Email Service: ports=[${ports.join(', ')}], hostname=${this.options.emailConfig.hostname || 'localhost'}, domains=${domainCount} [${domainNames}], DKIM initialized`);
|
||||
}
|
||||
|
||||
|
||||
// DNS service summary
|
||||
if (this.dnsServer && this.options.dnsNsDomains && this.options.dnsScopes) {
|
||||
console.log('\n🌍 DNS Service:');
|
||||
console.log(` ├─ Nameservers: ${this.options.dnsNsDomains.join(', ')}`);
|
||||
console.log(` ├─ Primary NS: ${this.options.dnsNsDomains[0]}`);
|
||||
console.log(` ├─ Authoritative for: ${this.options.dnsScopes.length} domains`);
|
||||
console.log(` ├─ UDP Port: 53`);
|
||||
console.log(` ├─ DNS-over-HTTPS: Enabled via socket handler`);
|
||||
console.log(` └─ DNSSEC: ${this.options.dnsNsDomains[0] ? 'Enabled' : 'Disabled'}`);
|
||||
|
||||
// Show authoritative domains
|
||||
if (this.options.dnsScopes.length > 0) {
|
||||
console.log('\n Authoritative Domains:');
|
||||
this.options.dnsScopes.forEach((domain, index) => {
|
||||
const isLast = index === this.options.dnsScopes!.length - 1;
|
||||
console.log(` ${isLast ? '└─' : '├─'} ${domain}`);
|
||||
});
|
||||
}
|
||||
logger.log('info', `DNS Service: nameservers=[${this.options.dnsNsDomains.join(', ')}], authoritative for ${this.options.dnsScopes.length} domains [${this.options.dnsScopes.join(', ')}], UDP:53, DoH enabled`);
|
||||
}
|
||||
|
||||
|
||||
// RADIUS service summary
|
||||
if (this.radiusServer && this.options.radiusConfig) {
|
||||
console.log('\n🔐 RADIUS Service:');
|
||||
console.log(` ├─ Auth Port: ${this.options.radiusConfig.authPort || 1812}`);
|
||||
console.log(` ├─ Acct Port: ${this.options.radiusConfig.acctPort || 1813}`);
|
||||
console.log(` ├─ Clients configured: ${this.options.radiusConfig.clients?.length || 0}`);
|
||||
const vlanStats = this.radiusServer.getVlanManager().getStats();
|
||||
console.log(` ├─ VLAN mappings: ${vlanStats.totalMappings}`);
|
||||
console.log(` └─ Accounting: ${this.options.radiusConfig.accounting?.enabled ? 'Enabled' : 'Disabled'}`);
|
||||
logger.log('info', `RADIUS Service: auth=${this.options.radiusConfig.authPort || 1812}, acct=${this.options.radiusConfig.acctPort || 1813}, clients=${this.options.radiusConfig.clients?.length || 0}, VLANs=${vlanStats.totalMappings}, accounting=${this.options.radiusConfig.accounting?.enabled ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
// Remote Ingress summary
|
||||
if (this.tunnelManager && this.options.remoteIngressConfig?.enabled) {
|
||||
console.log('\n🌐 Remote Ingress:');
|
||||
console.log(` ├─ Tunnel Port: ${this.options.remoteIngressConfig.tunnelPort || 8443}`);
|
||||
const edgeCount = this.remoteIngressManager?.getAllEdges().length || 0;
|
||||
const connectedCount = this.tunnelManager.getConnectedCount();
|
||||
console.log(` ├─ Registered Edges: ${edgeCount}`);
|
||||
console.log(` └─ Connected Edges: ${connectedCount}`);
|
||||
logger.log('info', `Remote Ingress: tunnel port=${this.options.remoteIngressConfig.tunnelPort || 8443}, edges=${edgeCount} registered/${connectedCount} connected`);
|
||||
}
|
||||
|
||||
// Storage summary
|
||||
if (this.storageManager && this.options.storage) {
|
||||
console.log('\n💾 Storage:');
|
||||
console.log(` └─ Path: ${this.options.storage.fsPath || 'default'}`);
|
||||
logger.log('info', `Storage: path=${this.options.storage.fsPath || 'default'}`);
|
||||
}
|
||||
|
||||
// Cache database summary
|
||||
if (this.cacheDb) {
|
||||
console.log('\n🗄️ Cache Database (smartdata + LocalTsmDb):');
|
||||
console.log(` ├─ Storage: ${this.cacheDb.getStoragePath()}`);
|
||||
console.log(` ├─ Database: ${this.cacheDb.getDbName()}`);
|
||||
console.log(` └─ Cleaner: ${this.cacheCleaner?.isActive() ? 'Active' : 'Inactive'} (${(this.options.cacheConfig?.cleanupIntervalHours || 1)}h interval)`);
|
||||
logger.log('info', `Cache Database: storage=${this.cacheDb.getStoragePath()}, db=${this.cacheDb.getDbName()}, cleaner=${this.cacheCleaner?.isActive() ? 'active' : 'inactive'} (${(this.options.cacheConfig?.cleanupIntervalHours || 1)}h interval)`);
|
||||
}
|
||||
|
||||
console.log('\n✅ All services are running\n');
|
||||
logger.log('info', 'All services are running');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,7 +393,7 @@ export class DcRouter {
|
||||
* Set up SmartProxy with direct configuration and automatic email routes
|
||||
*/
|
||||
private async setupSmartProxy(): Promise<void> {
|
||||
console.log('[DcRouter] Setting up SmartProxy...');
|
||||
logger.log('info', 'Setting up SmartProxy...');
|
||||
let routes: plugins.smartproxy.IRouteConfig[] = [];
|
||||
let acmeConfig: plugins.smartproxy.IAcmeOptions | undefined;
|
||||
|
||||
@@ -447,22 +401,20 @@ export class DcRouter {
|
||||
if (this.options.smartProxyConfig) {
|
||||
routes = this.options.smartProxyConfig.routes || [];
|
||||
acmeConfig = this.options.smartProxyConfig.acme;
|
||||
console.log(`[DcRouter] Found ${routes.length} routes in config`);
|
||||
console.log(`[DcRouter] ACME config present: ${!!acmeConfig}`);
|
||||
logger.log('info', `Found ${routes.length} routes in config, ACME config present: ${!!acmeConfig}`);
|
||||
}
|
||||
|
||||
// If email config exists, automatically add email routes
|
||||
if (this.options.emailConfig) {
|
||||
const emailRoutes = this.generateEmailRoutes(this.options.emailConfig);
|
||||
console.log(`Email Routes are:`)
|
||||
console.log(emailRoutes)
|
||||
logger.log('debug', 'Email routes generated', { routes: JSON.stringify(emailRoutes) });
|
||||
routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy
|
||||
}
|
||||
|
||||
// If DNS is configured, add DNS routes
|
||||
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
|
||||
const dnsRoutes = this.generateDnsRoutes();
|
||||
console.log(`DNS Routes for nameservers ${this.options.dnsNsDomains.join(', ')}:`, dnsRoutes);
|
||||
logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(dnsRoutes) });
|
||||
routes = [...routes, ...dnsRoutes];
|
||||
}
|
||||
|
||||
@@ -480,7 +432,7 @@ export class DcRouter {
|
||||
// Configure DNS challenge if available
|
||||
let challengeHandlers: any[] = [];
|
||||
if (this.options.dnsChallenge?.cloudflareApiKey) {
|
||||
console.log('Configuring Cloudflare DNS challenge for ACME');
|
||||
logger.log('info', 'Configuring Cloudflare DNS challenge for ACME');
|
||||
const cloudflareAccount = new plugins.cloudflare.CloudflareAccount(this.options.dnsChallenge.cloudflareApiKey);
|
||||
const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(cloudflareAccount);
|
||||
challengeHandlers.push(dns01Handler);
|
||||
@@ -488,7 +440,7 @@ export class DcRouter {
|
||||
|
||||
// If we have routes or need a basic SmartProxy instance, create it
|
||||
if (routes.length > 0 || this.options.smartProxyConfig) {
|
||||
console.log('Setting up SmartProxy with combined configuration');
|
||||
logger.log('info', 'Setting up SmartProxy with combined configuration');
|
||||
|
||||
// Track cert entries loaded from cert store so we can populate certificateStatusMap after start
|
||||
const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = [];
|
||||
@@ -537,7 +489,7 @@ export class DcRouter {
|
||||
// Stop old SmartAcme if it exists (e.g., during updateSmartProxyConfig)
|
||||
if (this.smartAcme) {
|
||||
await this.smartAcme.stop().catch(err =>
|
||||
console.error('[DcRouter] Error stopping old SmartAcme:', err)
|
||||
logger.log('error', 'Error stopping old SmartAcme', { error: String(err) })
|
||||
);
|
||||
}
|
||||
this.smartAcme = new plugins.smartacme.SmartAcme({
|
||||
@@ -600,25 +552,19 @@ export class DcRouter {
|
||||
}
|
||||
|
||||
// Create SmartProxy instance
|
||||
console.log('[DcRouter] Creating SmartProxy instance with config:', JSON.stringify({
|
||||
routeCount: smartProxyConfig.routes?.length,
|
||||
acmeEnabled: smartProxyConfig.acme?.enabled,
|
||||
acmeEmail: smartProxyConfig.acme?.email,
|
||||
certProvisionFunction: !!smartProxyConfig.certProvisionFunction
|
||||
}, null, 2));
|
||||
logger.log('info', `Creating SmartProxy instance: routes=${smartProxyConfig.routes?.length}, acme=${smartProxyConfig.acme?.enabled}, certProvisionFunction=${!!smartProxyConfig.certProvisionFunction}`);
|
||||
|
||||
this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
|
||||
|
||||
// Set up event listeners
|
||||
this.smartProxy.on('error', (err) => {
|
||||
console.error('[DcRouter] SmartProxy error:', err);
|
||||
console.error('[DcRouter] Error stack:', err.stack);
|
||||
logger.log('error', `SmartProxy error: ${err.message}`, { stack: err.stack });
|
||||
});
|
||||
|
||||
// Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
|
||||
// Events are keyed by domain for domain-centric certificate tracking
|
||||
this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
||||
console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||
logger.log('info', `Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||
const routeNames = this.findRouteNamesForDomain(event.domain);
|
||||
this.certificateStatusMap.set(event.domain, {
|
||||
status: 'valid', routeNames,
|
||||
@@ -628,7 +574,7 @@ export class DcRouter {
|
||||
});
|
||||
|
||||
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
||||
console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||
logger.log('info', `Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||
const routeNames = this.findRouteNamesForDomain(event.domain);
|
||||
this.certificateStatusMap.set(event.domain, {
|
||||
status: 'valid', routeNames,
|
||||
@@ -638,7 +584,7 @@ export class DcRouter {
|
||||
});
|
||||
|
||||
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
|
||||
console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error);
|
||||
logger.log('error', `Certificate failed for ${event.domain} (${event.source}): ${event.error}`);
|
||||
const routeNames = this.findRouteNamesForDomain(event.domain);
|
||||
this.certificateStatusMap.set(event.domain, {
|
||||
status: 'failed', routeNames, error: event.error,
|
||||
@@ -647,9 +593,9 @@ export class DcRouter {
|
||||
});
|
||||
|
||||
// Start SmartProxy
|
||||
console.log('[DcRouter] Starting SmartProxy...');
|
||||
logger.log('info', 'Starting SmartProxy...');
|
||||
await this.smartProxy.start();
|
||||
console.log('[DcRouter] SmartProxy started successfully');
|
||||
logger.log('info', 'SmartProxy started successfully');
|
||||
|
||||
// Populate certificateStatusMap for certs loaded from store at startup
|
||||
for (const entry of loadedCertEntries) {
|
||||
@@ -701,10 +647,10 @@ export class DcRouter {
|
||||
}
|
||||
}
|
||||
if (loadedCertEntries.length > 0) {
|
||||
console.log(`[DcRouter] Populated certificate status for ${loadedCertEntries.length} store-loaded domain(s)`);
|
||||
logger.log('info', `Populated certificate status for ${loadedCertEntries.length} store-loaded domain(s)`);
|
||||
}
|
||||
|
||||
console.log(`SmartProxy started with ${routes.length} routes`);
|
||||
logger.log('info', `SmartProxy started with ${routes.length} routes`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -907,7 +853,7 @@ export class DcRouter {
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
console.log('Stopping DcRouter services...');
|
||||
logger.log('info', 'Stopping DcRouter services...');
|
||||
|
||||
await this.opsServer.stop();
|
||||
|
||||
@@ -918,36 +864,36 @@ export class DcRouter {
|
||||
this.cacheCleaner ? Promise.resolve(this.cacheCleaner.stop()) : Promise.resolve(),
|
||||
|
||||
// Stop metrics manager if running
|
||||
this.metricsManager ? this.metricsManager.stop().catch(err => console.error('Error stopping MetricsManager:', err)) : Promise.resolve(),
|
||||
this.metricsManager ? this.metricsManager.stop().catch(err => logger.log('error', 'Error stopping MetricsManager', { error: String(err) })) : Promise.resolve(),
|
||||
|
||||
// Stop unified email server if running
|
||||
this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', err)) : Promise.resolve(),
|
||||
this.emailServer ? this.emailServer.stop().catch(err => logger.log('error', 'Error stopping email server', { error: String(err) })) : Promise.resolve(),
|
||||
|
||||
// Stop SmartAcme if running
|
||||
this.smartAcme ? this.smartAcme.stop().catch(err => console.error('Error stopping SmartAcme:', err)) : Promise.resolve(),
|
||||
this.smartAcme ? this.smartAcme.stop().catch(err => logger.log('error', 'Error stopping SmartAcme', { error: String(err) })) : Promise.resolve(),
|
||||
|
||||
// Stop HTTP SmartProxy if running
|
||||
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(),
|
||||
this.smartProxy ? this.smartProxy.stop().catch(err => logger.log('error', 'Error stopping SmartProxy', { error: String(err) })) : Promise.resolve(),
|
||||
|
||||
// Stop DNS server if running
|
||||
this.dnsServer ?
|
||||
this.dnsServer.stop().catch(err => console.error('Error stopping DNS server:', err)) :
|
||||
this.dnsServer.stop().catch(err => logger.log('error', 'Error stopping DNS server', { error: String(err) })) :
|
||||
Promise.resolve(),
|
||||
|
||||
// Stop RADIUS server if running
|
||||
this.radiusServer ?
|
||||
this.radiusServer.stop().catch(err => console.error('Error stopping RADIUS server:', err)) :
|
||||
this.radiusServer.stop().catch(err => logger.log('error', 'Error stopping RADIUS server', { error: String(err) })) :
|
||||
Promise.resolve(),
|
||||
|
||||
// Stop Remote Ingress tunnel manager if running
|
||||
this.tunnelManager ?
|
||||
this.tunnelManager.stop().catch(err => console.error('Error stopping TunnelManager:', err)) :
|
||||
this.tunnelManager.stop().catch(err => logger.log('error', 'Error stopping TunnelManager', { error: String(err) })) :
|
||||
Promise.resolve()
|
||||
]);
|
||||
|
||||
// Stop cache database after other services (they may need it during shutdown)
|
||||
if (this.cacheDb) {
|
||||
await this.cacheDb.stop().catch(err => console.error('Error stopping CacheDb:', err));
|
||||
await this.cacheDb.stop().catch(err => logger.log('error', 'Error stopping CacheDb', { error: String(err) }));
|
||||
}
|
||||
|
||||
// Clear backoff cache in cert scheduler
|
||||
@@ -969,9 +915,9 @@ export class DcRouter {
|
||||
this.remoteIngressManager = undefined;
|
||||
this.certificateStatusMap.clear();
|
||||
|
||||
console.log('All DcRouter services stopped');
|
||||
logger.log('info', 'All DcRouter services stopped');
|
||||
} catch (error) {
|
||||
console.error('Error during DcRouter shutdown:', error);
|
||||
logger.log('error', 'Error during DcRouter shutdown', { error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -998,7 +944,7 @@ export class DcRouter {
|
||||
// Start new SmartProxy with updated configuration (will include email routes if configured)
|
||||
await this.setupSmartProxy();
|
||||
|
||||
console.log('SmartProxy configuration updated');
|
||||
logger.log('info', 'SmartProxy configuration updated');
|
||||
}
|
||||
|
||||
|
||||
@@ -1091,7 +1037,7 @@ export class DcRouter {
|
||||
// Start email handling with new configuration
|
||||
await this.setupUnifiedEmailHandling();
|
||||
|
||||
console.log('Unified email configuration updated');
|
||||
logger.log('info', 'Unified email configuration updated');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1131,7 +1077,7 @@ export class DcRouter {
|
||||
this.emailServer.updateEmailRoutes(routes);
|
||||
}
|
||||
|
||||
console.log(`Email routes updated with ${routes.length} routes`);
|
||||
logger.log('info', `Email routes updated with ${routes.length} routes`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1266,6 +1212,7 @@ export class DcRouter {
|
||||
question.name,
|
||||
false,
|
||||
event.responseTimeMs,
|
||||
event.answered,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ const envMap: Record<string, 'local' | 'test' | 'staging' | 'production'> = {
|
||||
// In-memory log buffer for the OpsServer UI
|
||||
export const logBuffer = new SmartlogDestinationBuffer({ maxEntries: 2000 });
|
||||
|
||||
// Default Smartlog instance
|
||||
const baseLogger = new plugins.smartlog.Smartlog({
|
||||
// Default Smartlog instance (exported so OpsServer can add push destinations)
|
||||
export const baseLogger = new plugins.smartlog.Smartlog({
|
||||
logContext: {
|
||||
environment: envMap[nodeEnv] || 'production',
|
||||
runtime: 'node',
|
||||
|
||||
@@ -2,9 +2,10 @@ import * as plugins from '../plugins.js';
|
||||
import { DcRouter } from '../classes.dcrouter.js';
|
||||
import { MetricsCache } from './classes.metricscache.js';
|
||||
import { SecurityLogger, SecurityEventType } from '../security/classes.securitylogger.js';
|
||||
import { logger } from '../logger.js';
|
||||
|
||||
export class MetricsManager {
|
||||
private logger: plugins.smartlog.Smartlog;
|
||||
private metricsLogger: plugins.smartlog.Smartlog;
|
||||
private smartMetrics: plugins.smartmetrics.SmartMetrics;
|
||||
private dcRouter: DcRouter;
|
||||
private resetInterval?: NodeJS.Timeout;
|
||||
@@ -36,6 +37,7 @@ export class MetricsManager {
|
||||
lastResetDate: new Date().toDateString(),
|
||||
queryTimestamps: [] as number[], // Track query timestamps for rate calculation
|
||||
responseTimes: [] as number[], // Track response times in ms
|
||||
recentQueries: [] as Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>,
|
||||
};
|
||||
|
||||
// Per-minute time-series buckets for charts
|
||||
@@ -55,15 +57,15 @@ export class MetricsManager {
|
||||
|
||||
constructor(dcRouter: DcRouter) {
|
||||
this.dcRouter = dcRouter;
|
||||
// Create a new Smartlog instance for metrics
|
||||
this.logger = new plugins.smartlog.Smartlog({
|
||||
// Create a Smartlog instance for SmartMetrics (requires its own instance)
|
||||
this.metricsLogger = new plugins.smartlog.Smartlog({
|
||||
logContext: {
|
||||
environment: 'production',
|
||||
runtime: 'node',
|
||||
zone: 'dcrouter-metrics',
|
||||
}
|
||||
});
|
||||
this.smartMetrics = new plugins.smartmetrics.SmartMetrics(this.logger, 'dcrouter');
|
||||
this.smartMetrics = new plugins.smartmetrics.SmartMetrics(this.metricsLogger, 'dcrouter');
|
||||
// Initialize metrics cache with 500ms TTL
|
||||
this.metricsCache = new MetricsCache(500);
|
||||
}
|
||||
@@ -95,6 +97,7 @@ export class MetricsManager {
|
||||
this.dnsMetrics.topDomains.clear();
|
||||
this.dnsMetrics.queryTimestamps = [];
|
||||
this.dnsMetrics.responseTimes = [];
|
||||
this.dnsMetrics.recentQueries = [];
|
||||
this.dnsMetrics.lastResetDate = currentDate;
|
||||
}
|
||||
|
||||
@@ -109,18 +112,18 @@ export class MetricsManager {
|
||||
}
|
||||
}, 60000); // Check every minute
|
||||
|
||||
this.logger.log('info', 'MetricsManager started');
|
||||
logger.log('info', 'MetricsManager started');
|
||||
}
|
||||
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
// Clear the reset interval
|
||||
if (this.resetInterval) {
|
||||
clearInterval(this.resetInterval);
|
||||
this.resetInterval = undefined;
|
||||
}
|
||||
|
||||
|
||||
this.smartMetrics.stop();
|
||||
this.logger.log('info', 'MetricsManager stopped');
|
||||
logger.log('info', 'MetricsManager stopped');
|
||||
}
|
||||
|
||||
// Get server metrics from SmartMetrics and SmartProxy
|
||||
@@ -228,6 +231,7 @@ export class MetricsManager {
|
||||
queryTypes: this.dnsMetrics.queryTypes,
|
||||
averageResponseTime: Math.round(avgResponseTime),
|
||||
activeDomains: this.dnsMetrics.topDomains.size,
|
||||
recentQueries: this.dnsMetrics.recentQueries.slice(),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -392,9 +396,21 @@ export class MetricsManager {
|
||||
}
|
||||
|
||||
// DNS event tracking methods
|
||||
public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number): void {
|
||||
public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number, answered?: boolean): void {
|
||||
this.dnsMetrics.totalQueries++;
|
||||
this.incrementDnsBucket();
|
||||
|
||||
// Store recent query entry
|
||||
this.dnsMetrics.recentQueries.push({
|
||||
timestamp: Date.now(),
|
||||
domain,
|
||||
type: queryType,
|
||||
answered: answered ?? true,
|
||||
responseTimeMs: responseTimeMs ?? 0,
|
||||
});
|
||||
if (this.dnsMetrics.recentQueries.length > 100) {
|
||||
this.dnsMetrics.recentQueries.shift();
|
||||
}
|
||||
|
||||
if (cacheHit) {
|
||||
this.dnsMetrics.cacheHits++;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
import { logBuffer } from '../../logger.js';
|
||||
import { logBuffer, baseLogger } from '../../logger.js';
|
||||
|
||||
export class LogsHandler {
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
|
||||
constructor(private opsServerRef: OpsServer) {
|
||||
// Add this handler's router to the parent
|
||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
this.registerHandlers();
|
||||
this.setupLogPushDestination();
|
||||
}
|
||||
|
||||
private registerHandlers(): void {
|
||||
@@ -165,6 +166,50 @@ export class LogsHandler {
|
||||
return mapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a log destination to the base logger that pushes entries
|
||||
* to all connected ops_dashboard TypedSocket clients.
|
||||
*/
|
||||
private setupLogPushDestination(): void {
|
||||
const opsServerRef = this.opsServerRef;
|
||||
|
||||
baseLogger.addLogDestination({
|
||||
async handleLog(logPackage: any) {
|
||||
// Access the TypedSocket server instance from OpsServer
|
||||
const typedsocket = opsServerRef.server?.typedserver?.typedsocket;
|
||||
if (!typedsocket) return;
|
||||
|
||||
let connections: any[];
|
||||
try {
|
||||
connections = await typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (connections.length === 0) return;
|
||||
|
||||
const entry: interfaces.data.ILogEntry = {
|
||||
timestamp: logPackage.timestamp || Date.now(),
|
||||
level: LogsHandler.mapLogLevel(logPackage.level),
|
||||
category: LogsHandler.deriveCategory(logPackage.context?.zone, logPackage.message),
|
||||
message: logPackage.message,
|
||||
metadata: logPackage.data,
|
||||
};
|
||||
|
||||
for (const conn of connections) {
|
||||
try {
|
||||
const push = typedsocket.createTypedRequest<interfaces.requests.IReq_PushLogEntry>(
|
||||
'pushLogEntry',
|
||||
conn,
|
||||
);
|
||||
push.fire({ entry }).catch(() => {}); // fire-and-forget
|
||||
} catch {
|
||||
// connection may have closed
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private setupLogStream(
|
||||
virtualStream: plugins.typedrequest.VirtualStream<Uint8Array>,
|
||||
levelFilter?: string[],
|
||||
|
||||
@@ -241,6 +241,7 @@ export class StatsHandler {
|
||||
averageResponseTime: 0,
|
||||
queryTypes: stats.queryTypes,
|
||||
timeSeries,
|
||||
recentQueries: stats.recentQueries,
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -422,6 +423,7 @@ export class StatsHandler {
|
||||
count: number;
|
||||
}>;
|
||||
queryTypes: { [key: string]: number };
|
||||
recentQueries?: Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>;
|
||||
domainBreakdown?: { [domain: string]: interfaces.data.IDnsStats };
|
||||
}> {
|
||||
// Get metrics from MetricsManager if available
|
||||
@@ -435,9 +437,10 @@ export class StatsHandler {
|
||||
cacheHitRate: dnsStats.cacheHitRate,
|
||||
topDomains: dnsStats.topDomains,
|
||||
queryTypes: dnsStats.queryTypes,
|
||||
recentQueries: dnsStats.recentQueries,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Fallback if MetricsManager not available
|
||||
return {
|
||||
queriesPerSecond: 0,
|
||||
|
||||
@@ -60,6 +60,13 @@ export interface IDnsStats {
|
||||
timeSeries?: {
|
||||
queries: ITimeSeriesPoint[];
|
||||
};
|
||||
recentQueries?: Array<{
|
||||
timestamp: number;
|
||||
domain: string;
|
||||
type: string;
|
||||
answered: boolean;
|
||||
responseTimeMs: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IRateLimitInfo {
|
||||
|
||||
@@ -41,4 +41,16 @@ export interface IReq_GetLogStream extends plugins.typedrequestInterfaces.implem
|
||||
response: {
|
||||
logStream: plugins.typedrequestInterfaces.IVirtualStream;
|
||||
};
|
||||
}
|
||||
|
||||
// Push Log Entry (server → client via TypedSocket)
|
||||
export interface IReq_PushLogEntry extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_PushLogEntry
|
||||
> {
|
||||
method: 'pushLogEntry';
|
||||
request: {
|
||||
entry: statsInterfaces.ILogEntry;
|
||||
};
|
||||
response: {};
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '7.3.0',
|
||||
version: '7.4.1',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -1075,6 +1075,55 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// TypedSocket Client for Real-time Log Streaming
|
||||
// ============================================================================
|
||||
|
||||
let socketClient: plugins.typedsocket.TypedSocket | null = null;
|
||||
const socketRouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
|
||||
|
||||
// Register handler for pushed log entries from the server
|
||||
socketRouter.addTypedHandler(
|
||||
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushLogEntry>(
|
||||
'pushLogEntry',
|
||||
async (dataArg) => {
|
||||
const current = logStatePart.getState();
|
||||
const updated = [...current.recentLogs, dataArg.entry];
|
||||
// Cap at 2000 entries
|
||||
if (updated.length > 2000) {
|
||||
updated.splice(0, updated.length - 2000);
|
||||
}
|
||||
logStatePart.setState({ ...current, recentLogs: updated });
|
||||
return {};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
async function connectSocket() {
|
||||
if (socketClient) return;
|
||||
try {
|
||||
socketClient = await plugins.typedsocket.TypedSocket.createClient(
|
||||
socketRouter,
|
||||
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl(),
|
||||
);
|
||||
await socketClient.setTag('role', 'ops_dashboard');
|
||||
} catch (err) {
|
||||
console.error('TypedSocket connection failed:', err);
|
||||
socketClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnectSocket() {
|
||||
if (socketClient) {
|
||||
try {
|
||||
await socketClient.stop();
|
||||
} catch {
|
||||
// ignore disconnect errors
|
||||
}
|
||||
socketClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Combined refresh action for efficient polling
|
||||
async function dispatchCombinedRefreshAction() {
|
||||
const context = getActionContext();
|
||||
@@ -1237,9 +1286,21 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
|
||||
if (state.isLoggedIn !== previousIsLoggedIn) {
|
||||
previousIsLoggedIn = state.isLoggedIn;
|
||||
startAutoRefresh();
|
||||
|
||||
// Connect/disconnect TypedSocket based on login state
|
||||
if (state.isLoggedIn) {
|
||||
connectSocket();
|
||||
} else {
|
||||
disconnectSocket();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initial start
|
||||
startAutoRefresh();
|
||||
|
||||
// Connect TypedSocket if already logged in (e.g., persistent session)
|
||||
if (loginStatePart.getState().isLoggedIn) {
|
||||
connectSocket();
|
||||
}
|
||||
})();
|
||||
@@ -29,6 +29,8 @@ export class OpsViewLogs extends DeesElement {
|
||||
@state()
|
||||
accessor filterLimit: number = 100;
|
||||
|
||||
private lastPushedCount = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subscription = appstate.logStatePart
|
||||
@@ -110,7 +112,11 @@ export class OpsViewLogs extends DeesElement {
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.fetchLogs();
|
||||
this.lastPushedCount = 0;
|
||||
// Only fetch if state is empty (streaming will handle new entries)
|
||||
if (this.logState.recentLogs.length === 0) {
|
||||
this.fetchLogs();
|
||||
}
|
||||
}
|
||||
|
||||
async updated(changedProperties: Map<string, any>) {
|
||||
@@ -127,10 +133,27 @@ export class OpsViewLogs extends DeesElement {
|
||||
// Ensure the chart element has finished its own initialization
|
||||
await chartLog.updateComplete;
|
||||
|
||||
chartLog.clearLogs();
|
||||
const entries = this.getMappedLogEntries();
|
||||
if (entries.length > 0) {
|
||||
chartLog.updateLog(entries);
|
||||
// Wait for xterm terminal to finish initializing (CDN load)
|
||||
if (!chartLog.terminalReady) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const check = () => {
|
||||
if (chartLog.terminalReady) { resolve(); return; }
|
||||
setTimeout(check, 50);
|
||||
};
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
const allEntries = this.getMappedLogEntries();
|
||||
if (this.lastPushedCount === 0 && allEntries.length > 0) {
|
||||
// Initial load: push all entries
|
||||
chartLog.updateLog(allEntries);
|
||||
this.lastPushedCount = allEntries.length;
|
||||
} else if (allEntries.length > this.lastPushedCount) {
|
||||
// Incremental: only push new entries
|
||||
const newEntries = allEntries.slice(this.lastPushedCount);
|
||||
chartLog.updateLog(newEntries);
|
||||
this.lastPushedCount = allEntries.length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,8 +133,8 @@ export class OpsViewOverview extends DeesElement {
|
||||
.logEntries=${this.getRecentEventEntries()}
|
||||
></dees-chart-log>
|
||||
<dees-chart-log
|
||||
.label=${'Security Alerts'}
|
||||
.logEntries=${this.getSecurityAlertEntries()}
|
||||
.label=${'DNS Queries'}
|
||||
.logEntries=${this.getDnsQueryEntries()}
|
||||
></dees-chart-log>
|
||||
</div>
|
||||
`}
|
||||
@@ -395,6 +395,16 @@ export class OpsViewOverview extends DeesElement {
|
||||
}));
|
||||
}
|
||||
|
||||
private getDnsQueryEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
|
||||
const queries: any[] = (this.statsState.dnsStats as any)?.recentQueries || [];
|
||||
return queries.map((q: any) => ({
|
||||
timestamp: new Date(q.timestamp).toISOString(),
|
||||
level: q.answered ? 'info' as const : 'warn' as const,
|
||||
message: `${q.type} ${q.domain} (${q.responseTimeMs}ms)`,
|
||||
source: 'dns',
|
||||
}));
|
||||
}
|
||||
|
||||
private getEmailTrafficSeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
|
||||
const ts = this.statsState.emailStats?.timeSeries;
|
||||
if (!ts) return [];
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
import * as deesElement from '@design.estate/dees-element';
|
||||
import * as deesCatalog from '@design.estate/dees-catalog';
|
||||
|
||||
// TypedSocket for real-time push communication
|
||||
import * as typedsocket from '@api.global/typedsocket';
|
||||
|
||||
export {
|
||||
deesElement,
|
||||
deesCatalog
|
||||
deesCatalog,
|
||||
typedsocket,
|
||||
}
|
||||
|
||||
// domtools gives us TypedRequest and other utilities
|
||||
|
||||
Reference in New Issue
Block a user