@serve.zone/dcrouter 7.0.1 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_serve/bundle.js +520 -506
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +18 -1
- package/dist_ts/logger.d.ts +2 -0
- package/dist_ts/logger.js +6 -1
- package/dist_ts/monitoring/classes.metricsmanager.d.ts +37 -0
- package/dist_ts/monitoring/classes.metricsmanager.js +101 -1
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +2 -0
- package/dist_ts/opsserver/handlers/logs.handler.js +70 -25
- package/dist_ts/opsserver/handlers/stats.handler.js +25 -1
- package/dist_ts_interfaces/data/stats.d.ts +23 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/ops-view-logs.d.ts +4 -0
- package/dist_ts_web/elements/ops-view-logs.js +48 -12
- package/dist_ts_web/elements/ops-view-overview.d.ts +6 -0
- package/dist_ts_web/elements/ops-view-overview.js +82 -8
- package/dist_ts_web/elements/ops-view-security.js +42 -36
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +19 -1
- package/ts/logger.ts +7 -0
- package/ts/monitoring/classes.metricsmanager.ts +123 -3
- package/ts/opsserver/handlers/logs.handler.ts +77 -28
- package/ts/opsserver/handlers/stats.handler.ts +27 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/ops-view-logs.ts +34 -11
- package/ts_web/elements/ops-view-overview.ts +80 -6
- package/ts_web/elements/ops-view-security.ts +47 -36
package/ts/logger.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { SmartlogDestinationBuffer } from '@push.rocks/smartlog/destination-buffer';
|
|
3
4
|
|
|
4
5
|
// Map NODE_ENV to valid TEnvironment
|
|
5
6
|
const nodeEnv = process.env.NODE_ENV || 'production';
|
|
@@ -10,6 +11,9 @@ const envMap: Record<string, 'local' | 'test' | 'staging' | 'production'> = {
|
|
|
10
11
|
'production': 'production'
|
|
11
12
|
};
|
|
12
13
|
|
|
14
|
+
// In-memory log buffer for the OpsServer UI
|
|
15
|
+
export const logBuffer = new SmartlogDestinationBuffer({ maxEntries: 2000 });
|
|
16
|
+
|
|
13
17
|
// Default Smartlog instance
|
|
14
18
|
const baseLogger = new plugins.smartlog.Smartlog({
|
|
15
19
|
logContext: {
|
|
@@ -19,6 +23,9 @@ const baseLogger = new plugins.smartlog.Smartlog({
|
|
|
19
23
|
}
|
|
20
24
|
});
|
|
21
25
|
|
|
26
|
+
// Wire the buffer destination so all logs are captured
|
|
27
|
+
baseLogger.addLogDestination(logBuffer);
|
|
28
|
+
|
|
22
29
|
// Extended logger compatible with the original enhanced logger API
|
|
23
30
|
class StandardLogger {
|
|
24
31
|
private defaultContext: Record<string, any> = {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import { DcRouter } from '../classes.dcrouter.js';
|
|
3
3
|
import { MetricsCache } from './classes.metricscache.js';
|
|
4
|
+
import { SecurityLogger, SecurityEventType } from '../security/classes.securitylogger.js';
|
|
4
5
|
|
|
5
6
|
export class MetricsManager {
|
|
6
7
|
private logger: plugins.smartlog.Smartlog;
|
|
@@ -37,6 +38,10 @@ export class MetricsManager {
|
|
|
37
38
|
responseTimes: [] as number[], // Track response times in ms
|
|
38
39
|
};
|
|
39
40
|
|
|
41
|
+
// Per-minute time-series buckets for charts
|
|
42
|
+
private emailMinuteBuckets = new Map<number, { sent: number; received: number; failed: number }>();
|
|
43
|
+
private dnsMinuteBuckets = new Map<number, { queries: number }>();
|
|
44
|
+
|
|
40
45
|
// Track security-specific metrics
|
|
41
46
|
private securityMetrics = {
|
|
42
47
|
blockedIPs: 0,
|
|
@@ -227,20 +232,45 @@ export class MetricsManager {
|
|
|
227
232
|
});
|
|
228
233
|
}
|
|
229
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Sync security metrics from the SecurityLogger singleton (last 24h).
|
|
237
|
+
* Called before returning security stats so counters reflect real events.
|
|
238
|
+
*/
|
|
239
|
+
private syncFromSecurityLogger(): void {
|
|
240
|
+
try {
|
|
241
|
+
const securityLogger = SecurityLogger.getInstance();
|
|
242
|
+
const summary = securityLogger.getEventsSummary(86400000); // last 24h
|
|
243
|
+
|
|
244
|
+
this.securityMetrics.spamDetected = summary.byType[SecurityEventType.SPAM] || 0;
|
|
245
|
+
this.securityMetrics.malwareDetected = summary.byType[SecurityEventType.MALWARE] || 0;
|
|
246
|
+
this.securityMetrics.phishingDetected = summary.byType[SecurityEventType.DMARC] || 0; // phishing via DMARC
|
|
247
|
+
this.securityMetrics.authFailures =
|
|
248
|
+
summary.byType[SecurityEventType.AUTHENTICATION] || 0;
|
|
249
|
+
this.securityMetrics.blockedIPs =
|
|
250
|
+
(summary.byType[SecurityEventType.IP_REPUTATION] || 0) +
|
|
251
|
+
(summary.byType[SecurityEventType.REJECTED_CONNECTION] || 0);
|
|
252
|
+
} catch {
|
|
253
|
+
// SecurityLogger may not be initialized yet — ignore
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
230
257
|
// Get security metrics
|
|
231
258
|
public async getSecurityStats() {
|
|
232
259
|
return this.metricsCache.get('securityStats', () => {
|
|
260
|
+
// Sync counters from the real SecurityLogger events
|
|
261
|
+
this.syncFromSecurityLogger();
|
|
262
|
+
|
|
233
263
|
// Get recent incidents (last 20)
|
|
234
264
|
const recentIncidents = this.securityMetrics.incidents.slice(-20);
|
|
235
|
-
|
|
265
|
+
|
|
236
266
|
return {
|
|
237
267
|
blockedIPs: this.securityMetrics.blockedIPs,
|
|
238
268
|
authFailures: this.securityMetrics.authFailures,
|
|
239
269
|
spamDetected: this.securityMetrics.spamDetected,
|
|
240
270
|
malwareDetected: this.securityMetrics.malwareDetected,
|
|
241
271
|
phishingDetected: this.securityMetrics.phishingDetected,
|
|
242
|
-
totalThreatsBlocked: this.securityMetrics.spamDetected +
|
|
243
|
-
this.securityMetrics.malwareDetected +
|
|
272
|
+
totalThreatsBlocked: this.securityMetrics.spamDetected +
|
|
273
|
+
this.securityMetrics.malwareDetected +
|
|
244
274
|
this.securityMetrics.phishingDetected,
|
|
245
275
|
recentIncidents,
|
|
246
276
|
};
|
|
@@ -275,6 +305,7 @@ export class MetricsManager {
|
|
|
275
305
|
// Email event tracking methods
|
|
276
306
|
public trackEmailSent(recipient?: string, deliveryTimeMs?: number): void {
|
|
277
307
|
this.emailMetrics.sentToday++;
|
|
308
|
+
this.incrementEmailBucket('sent');
|
|
278
309
|
|
|
279
310
|
if (recipient) {
|
|
280
311
|
const count = this.emailMetrics.recipients.get(recipient) || 0;
|
|
@@ -311,6 +342,7 @@ export class MetricsManager {
|
|
|
311
342
|
|
|
312
343
|
public trackEmailReceived(sender?: string): void {
|
|
313
344
|
this.emailMetrics.receivedToday++;
|
|
345
|
+
this.incrementEmailBucket('received');
|
|
314
346
|
|
|
315
347
|
this.emailMetrics.recentActivity.push({
|
|
316
348
|
timestamp: Date.now(),
|
|
@@ -326,6 +358,7 @@ export class MetricsManager {
|
|
|
326
358
|
|
|
327
359
|
public trackEmailFailed(recipient?: string, reason?: string): void {
|
|
328
360
|
this.emailMetrics.failedToday++;
|
|
361
|
+
this.incrementEmailBucket('failed');
|
|
329
362
|
|
|
330
363
|
this.emailMetrics.recentActivity.push({
|
|
331
364
|
timestamp: Date.now(),
|
|
@@ -361,6 +394,7 @@ export class MetricsManager {
|
|
|
361
394
|
// DNS event tracking methods
|
|
362
395
|
public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number): void {
|
|
363
396
|
this.dnsMetrics.totalQueries++;
|
|
397
|
+
this.incrementDnsBucket();
|
|
364
398
|
|
|
365
399
|
if (cacheHit) {
|
|
366
400
|
this.dnsMetrics.cacheHits++;
|
|
@@ -547,4 +581,90 @@ export class MetricsManager {
|
|
|
547
581
|
};
|
|
548
582
|
}, 200); // Use 200ms cache for more frequent updates
|
|
549
583
|
}
|
|
584
|
+
|
|
585
|
+
// --- Time-series helpers ---
|
|
586
|
+
|
|
587
|
+
private static minuteKey(ts: number = Date.now()): number {
|
|
588
|
+
return Math.floor(ts / 60000) * 60000;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private incrementEmailBucket(field: 'sent' | 'received' | 'failed'): void {
|
|
592
|
+
const key = MetricsManager.minuteKey();
|
|
593
|
+
let bucket = this.emailMinuteBuckets.get(key);
|
|
594
|
+
if (!bucket) {
|
|
595
|
+
bucket = { sent: 0, received: 0, failed: 0 };
|
|
596
|
+
this.emailMinuteBuckets.set(key, bucket);
|
|
597
|
+
}
|
|
598
|
+
bucket[field]++;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private incrementDnsBucket(): void {
|
|
602
|
+
const key = MetricsManager.minuteKey();
|
|
603
|
+
let bucket = this.dnsMinuteBuckets.get(key);
|
|
604
|
+
if (!bucket) {
|
|
605
|
+
bucket = { queries: 0 };
|
|
606
|
+
this.dnsMinuteBuckets.set(key, bucket);
|
|
607
|
+
}
|
|
608
|
+
bucket.queries++;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private pruneOldBuckets(): void {
|
|
612
|
+
const cutoff = Date.now() - 86400000; // 24h
|
|
613
|
+
for (const key of this.emailMinuteBuckets.keys()) {
|
|
614
|
+
if (key < cutoff) this.emailMinuteBuckets.delete(key);
|
|
615
|
+
}
|
|
616
|
+
for (const key of this.dnsMinuteBuckets.keys()) {
|
|
617
|
+
if (key < cutoff) this.dnsMinuteBuckets.delete(key);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Get email time-series data for the last N hours, aggregated per minute.
|
|
623
|
+
*/
|
|
624
|
+
public getEmailTimeSeries(hours: number = 24): {
|
|
625
|
+
sent: Array<{ timestamp: number; value: number }>;
|
|
626
|
+
received: Array<{ timestamp: number; value: number }>;
|
|
627
|
+
failed: Array<{ timestamp: number; value: number }>;
|
|
628
|
+
} {
|
|
629
|
+
this.pruneOldBuckets();
|
|
630
|
+
const cutoff = Date.now() - hours * 3600000;
|
|
631
|
+
const sent: Array<{ timestamp: number; value: number }> = [];
|
|
632
|
+
const received: Array<{ timestamp: number; value: number }> = [];
|
|
633
|
+
const failed: Array<{ timestamp: number; value: number }> = [];
|
|
634
|
+
|
|
635
|
+
const sortedKeys = Array.from(this.emailMinuteBuckets.keys())
|
|
636
|
+
.filter((k) => k >= cutoff)
|
|
637
|
+
.sort((a, b) => a - b);
|
|
638
|
+
|
|
639
|
+
for (const key of sortedKeys) {
|
|
640
|
+
const bucket = this.emailMinuteBuckets.get(key)!;
|
|
641
|
+
sent.push({ timestamp: key, value: bucket.sent });
|
|
642
|
+
received.push({ timestamp: key, value: bucket.received });
|
|
643
|
+
failed.push({ timestamp: key, value: bucket.failed });
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return { sent, received, failed };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Get DNS time-series data for the last N hours, aggregated per minute.
|
|
651
|
+
*/
|
|
652
|
+
public getDnsTimeSeries(hours: number = 24): {
|
|
653
|
+
queries: Array<{ timestamp: number; value: number }>;
|
|
654
|
+
} {
|
|
655
|
+
this.pruneOldBuckets();
|
|
656
|
+
const cutoff = Date.now() - hours * 3600000;
|
|
657
|
+
const queries: Array<{ timestamp: number; value: number }> = [];
|
|
658
|
+
|
|
659
|
+
const sortedKeys = Array.from(this.dnsMinuteBuckets.keys())
|
|
660
|
+
.filter((k) => k >= cutoff)
|
|
661
|
+
.sort((a, b) => a - b);
|
|
662
|
+
|
|
663
|
+
for (const key of sortedKeys) {
|
|
664
|
+
const bucket = this.dnsMinuteBuckets.get(key)!;
|
|
665
|
+
queries.push({ timestamp: key, value: bucket.queries });
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return { queries };
|
|
669
|
+
}
|
|
550
670
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
import { logBuffer } from '../../logger.js';
|
|
4
5
|
|
|
5
6
|
export class LogsHandler {
|
|
6
7
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
@@ -64,6 +65,32 @@ export class LogsHandler {
|
|
|
64
65
|
);
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
private static mapLogLevel(smartlogLevel: string): 'debug' | 'info' | 'warn' | 'error' {
|
|
69
|
+
switch (smartlogLevel) {
|
|
70
|
+
case 'silly':
|
|
71
|
+
case 'debug':
|
|
72
|
+
return 'debug';
|
|
73
|
+
case 'warn':
|
|
74
|
+
return 'warn';
|
|
75
|
+
case 'error':
|
|
76
|
+
return 'error';
|
|
77
|
+
default:
|
|
78
|
+
return 'info';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private static deriveCategory(
|
|
83
|
+
zone?: string,
|
|
84
|
+
message?: string
|
|
85
|
+
): 'smtp' | 'dns' | 'security' | 'system' | 'email' {
|
|
86
|
+
const msg = (message || '').toLowerCase();
|
|
87
|
+
if (msg.includes('[security:') || msg.includes('security')) return 'security';
|
|
88
|
+
if (zone === 'email' || msg.includes('email') || msg.includes('smtp') || msg.includes('mta')) return 'email';
|
|
89
|
+
if (zone === 'dns' || msg.includes('dns')) return 'dns';
|
|
90
|
+
if (msg.includes('smtp')) return 'smtp';
|
|
91
|
+
return 'system';
|
|
92
|
+
}
|
|
93
|
+
|
|
67
94
|
private async getRecentLogs(
|
|
68
95
|
level?: 'error' | 'warn' | 'info' | 'debug',
|
|
69
96
|
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email',
|
|
@@ -78,42 +105,64 @@ export class LogsHandler {
|
|
|
78
105
|
message: string;
|
|
79
106
|
metadata?: any;
|
|
80
107
|
}>> {
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
108
|
+
// Compute a timestamp cutoff from timeRange
|
|
109
|
+
let since: number | undefined;
|
|
110
|
+
if (timeRange) {
|
|
111
|
+
const rangeMs: Record<string, number> = {
|
|
112
|
+
'1h': 3600000,
|
|
113
|
+
'6h': 21600000,
|
|
114
|
+
'24h': 86400000,
|
|
115
|
+
'7d': 604800000,
|
|
116
|
+
'30d': 2592000000,
|
|
117
|
+
};
|
|
118
|
+
since = Date.now() - (rangeMs[timeRange] || 86400000);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Map the UI level to smartlog levels for filtering
|
|
122
|
+
const smartlogLevels: string[] | undefined = level
|
|
123
|
+
? level === 'debug'
|
|
124
|
+
? ['debug', 'silly']
|
|
125
|
+
: level === 'info'
|
|
126
|
+
? ['info', 'ok', 'success', 'note', 'lifecycle']
|
|
127
|
+
: [level]
|
|
128
|
+
: undefined;
|
|
129
|
+
|
|
130
|
+
// Fetch a larger batch from buffer, then apply category filter client-side
|
|
131
|
+
const rawEntries = logBuffer.getEntries({
|
|
132
|
+
level: smartlogLevels as any,
|
|
133
|
+
search,
|
|
134
|
+
since,
|
|
135
|
+
limit: limit * 3, // over-fetch to compensate for category filtering
|
|
136
|
+
offset: 0,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Map ILogPackage → UI log format and apply category filter
|
|
140
|
+
const mapped: Array<{
|
|
84
141
|
timestamp: number;
|
|
85
142
|
level: 'debug' | 'info' | 'warn' | 'error';
|
|
86
143
|
category: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
|
87
144
|
message: string;
|
|
88
145
|
metadata?: any;
|
|
89
146
|
}> = [];
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
mockLogs.push({
|
|
105
|
-
timestamp: now - (i * 60000), // 1 minute apart
|
|
106
|
-
level: mockLevel,
|
|
107
|
-
category: mockCategory,
|
|
108
|
-
message: `Sample log message ${i} from ${mockCategory}`,
|
|
109
|
-
metadata: {
|
|
110
|
-
requestId: plugins.uuid.v4(),
|
|
111
|
-
},
|
|
147
|
+
|
|
148
|
+
for (const pkg of rawEntries) {
|
|
149
|
+
const uiLevel = LogsHandler.mapLogLevel(pkg.level);
|
|
150
|
+
const uiCategory = LogsHandler.deriveCategory(pkg.context?.zone, pkg.message);
|
|
151
|
+
|
|
152
|
+
if (category && uiCategory !== category) continue;
|
|
153
|
+
|
|
154
|
+
mapped.push({
|
|
155
|
+
timestamp: pkg.timestamp,
|
|
156
|
+
level: uiLevel,
|
|
157
|
+
category: uiCategory,
|
|
158
|
+
message: pkg.message,
|
|
159
|
+
metadata: pkg.data,
|
|
112
160
|
});
|
|
161
|
+
|
|
162
|
+
if (mapped.length >= limit) break;
|
|
113
163
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return mockLogs.slice(offset, offset + limit);
|
|
164
|
+
|
|
165
|
+
return mapped;
|
|
117
166
|
}
|
|
118
167
|
|
|
119
168
|
private setupLogStream(
|
|
@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
|
|
|
2
2
|
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
4
|
import { MetricsManager } from '../../monitoring/index.js';
|
|
5
|
+
import { SecurityLogger } from '../../security/classes.securitylogger.js';
|
|
5
6
|
|
|
6
7
|
export class StatsHandler {
|
|
7
8
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
@@ -203,6 +204,11 @@ export class StatsHandler {
|
|
|
203
204
|
if (sections.email) {
|
|
204
205
|
promises.push(
|
|
205
206
|
this.collectEmailStats().then(stats => {
|
|
207
|
+
// Get time-series data from MetricsManager
|
|
208
|
+
const timeSeries = this.opsServerRef.dcRouterRef.metricsManager
|
|
209
|
+
? this.opsServerRef.dcRouterRef.metricsManager.getEmailTimeSeries(24)
|
|
210
|
+
: undefined;
|
|
211
|
+
|
|
206
212
|
metrics.email = {
|
|
207
213
|
sent: stats.sentToday,
|
|
208
214
|
received: stats.receivedToday,
|
|
@@ -212,6 +218,7 @@ export class StatsHandler {
|
|
|
212
218
|
averageDeliveryTime: 0,
|
|
213
219
|
deliveryRate: stats.deliveryRate,
|
|
214
220
|
bounceRate: stats.bounceRate,
|
|
221
|
+
timeSeries,
|
|
215
222
|
};
|
|
216
223
|
})
|
|
217
224
|
);
|
|
@@ -220,6 +227,11 @@ export class StatsHandler {
|
|
|
220
227
|
if (sections.dns) {
|
|
221
228
|
promises.push(
|
|
222
229
|
this.collectDnsStats().then(stats => {
|
|
230
|
+
// Get time-series data from MetricsManager
|
|
231
|
+
const timeSeries = this.opsServerRef.dcRouterRef.metricsManager
|
|
232
|
+
? this.opsServerRef.dcRouterRef.metricsManager.getDnsTimeSeries(24)
|
|
233
|
+
: undefined;
|
|
234
|
+
|
|
223
235
|
metrics.dns = {
|
|
224
236
|
totalQueries: stats.totalQueries,
|
|
225
237
|
cacheHits: stats.cacheHits,
|
|
@@ -228,6 +240,7 @@ export class StatsHandler {
|
|
|
228
240
|
activeDomains: stats.topDomains.length,
|
|
229
241
|
averageResponseTime: 0,
|
|
230
242
|
queryTypes: stats.queryTypes,
|
|
243
|
+
timeSeries,
|
|
231
244
|
};
|
|
232
245
|
})
|
|
233
246
|
);
|
|
@@ -236,6 +249,19 @@ export class StatsHandler {
|
|
|
236
249
|
if (sections.security && this.opsServerRef.dcRouterRef.metricsManager) {
|
|
237
250
|
promises.push(
|
|
238
251
|
this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats().then(stats => {
|
|
252
|
+
// Get recent events from the SecurityLogger singleton
|
|
253
|
+
const securityLogger = SecurityLogger.getInstance();
|
|
254
|
+
const recentEvents = securityLogger.getRecentEvents(50).map((evt) => ({
|
|
255
|
+
timestamp: evt.timestamp,
|
|
256
|
+
level: evt.level,
|
|
257
|
+
type: evt.type,
|
|
258
|
+
message: evt.message,
|
|
259
|
+
details: evt.details,
|
|
260
|
+
ipAddress: evt.ipAddress,
|
|
261
|
+
domain: evt.domain,
|
|
262
|
+
success: evt.success,
|
|
263
|
+
}));
|
|
264
|
+
|
|
239
265
|
metrics.security = {
|
|
240
266
|
blockedIPs: stats.blockedIPs,
|
|
241
267
|
reputationScores: {},
|
|
@@ -244,6 +270,7 @@ export class StatsHandler {
|
|
|
244
270
|
phishingDetected: stats.phishingDetected,
|
|
245
271
|
authenticationFailures: stats.authFailures,
|
|
246
272
|
suspiciousActivities: stats.totalThreatsBlocked,
|
|
273
|
+
recentEvents,
|
|
247
274
|
};
|
|
248
275
|
})
|
|
249
276
|
);
|
|
@@ -20,6 +20,15 @@ export class OpsViewLogs extends DeesElement {
|
|
|
20
20
|
filters: {},
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
@state()
|
|
24
|
+
accessor filterLevel: string | undefined;
|
|
25
|
+
|
|
26
|
+
@state()
|
|
27
|
+
accessor filterCategory: string | undefined;
|
|
28
|
+
|
|
29
|
+
@state()
|
|
30
|
+
accessor filterLimit: number = 100;
|
|
31
|
+
|
|
23
32
|
constructor() {
|
|
24
33
|
super();
|
|
25
34
|
const subscription = appstate.logStatePart
|
|
@@ -174,29 +183,43 @@ export class OpsViewLogs extends DeesElement {
|
|
|
174
183
|
`;
|
|
175
184
|
}
|
|
176
185
|
|
|
186
|
+
async connectedCallback() {
|
|
187
|
+
super.connectedCallback();
|
|
188
|
+
// Auto-fetch logs when the view mounts
|
|
189
|
+
this.fetchLogs();
|
|
190
|
+
}
|
|
191
|
+
|
|
177
192
|
private async fetchLogs() {
|
|
178
|
-
const filters = this.getActiveFilters();
|
|
179
193
|
await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, {
|
|
180
|
-
limit:
|
|
181
|
-
level:
|
|
182
|
-
category:
|
|
194
|
+
limit: this.filterLimit,
|
|
195
|
+
level: this.filterLevel as 'debug' | 'info' | 'warn' | 'error' | undefined,
|
|
196
|
+
category: this.filterCategory as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
|
|
183
197
|
});
|
|
184
198
|
}
|
|
185
199
|
|
|
186
200
|
private updateFilter(type: string, value: string) {
|
|
187
|
-
|
|
188
|
-
|
|
201
|
+
const resolved = value === 'all' ? undefined : value;
|
|
202
|
+
|
|
203
|
+
switch (type) {
|
|
204
|
+
case 'level':
|
|
205
|
+
this.filterLevel = resolved;
|
|
206
|
+
break;
|
|
207
|
+
case 'category':
|
|
208
|
+
this.filterCategory = resolved;
|
|
209
|
+
break;
|
|
210
|
+
case 'limit':
|
|
211
|
+
this.filterLimit = resolved ? parseInt(resolved, 10) : 100;
|
|
212
|
+
break;
|
|
189
213
|
}
|
|
190
|
-
|
|
191
|
-
// Update filters then fetch logs
|
|
214
|
+
|
|
192
215
|
this.fetchLogs();
|
|
193
216
|
}
|
|
194
217
|
|
|
195
218
|
private getActiveFilters() {
|
|
196
219
|
return {
|
|
197
|
-
level: this.
|
|
198
|
-
category: this.
|
|
199
|
-
limit:
|
|
220
|
+
level: this.filterLevel,
|
|
221
|
+
category: this.filterCategory,
|
|
222
|
+
limit: this.filterLimit,
|
|
200
223
|
};
|
|
201
224
|
}
|
|
202
225
|
|
|
@@ -26,14 +26,36 @@ export class OpsViewOverview extends DeesElement {
|
|
|
26
26
|
error: null,
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
@state()
|
|
30
|
+
accessor logState: appstate.ILogState = {
|
|
31
|
+
recentLogs: [],
|
|
32
|
+
isStreaming: false,
|
|
33
|
+
filters: {},
|
|
34
|
+
};
|
|
35
|
+
|
|
29
36
|
constructor() {
|
|
30
37
|
super();
|
|
31
|
-
const
|
|
38
|
+
const statsSub = appstate.statsStatePart
|
|
32
39
|
.select((stateArg) => stateArg)
|
|
33
40
|
.subscribe((statsState) => {
|
|
34
41
|
this.statsState = statsState;
|
|
35
42
|
});
|
|
36
|
-
this.rxSubscriptions.push(
|
|
43
|
+
this.rxSubscriptions.push(statsSub);
|
|
44
|
+
|
|
45
|
+
const logSub = appstate.logStatePart
|
|
46
|
+
.select((stateArg) => stateArg)
|
|
47
|
+
.subscribe((logState) => {
|
|
48
|
+
this.logState = logState;
|
|
49
|
+
});
|
|
50
|
+
this.rxSubscriptions.push(logSub);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async connectedCallback() {
|
|
54
|
+
super.connectedCallback();
|
|
55
|
+
// Ensure logs are fetched for the overview charts
|
|
56
|
+
if (this.logState.recentLogs.length === 0) {
|
|
57
|
+
appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, { limit: 100 });
|
|
58
|
+
}
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
public static styles = [
|
|
@@ -96,10 +118,24 @@ export class OpsViewOverview extends DeesElement {
|
|
|
96
118
|
${this.renderDnsStats()}
|
|
97
119
|
|
|
98
120
|
<div class="chartGrid">
|
|
99
|
-
<dees-chart-area
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
121
|
+
<dees-chart-area
|
|
122
|
+
.label=${'Email Traffic (24h)'}
|
|
123
|
+
.series=${this.getEmailTrafficSeries()}
|
|
124
|
+
.yAxisFormatter=${(val: number) => `${val}`}
|
|
125
|
+
></dees-chart-area>
|
|
126
|
+
<dees-chart-area
|
|
127
|
+
.label=${'DNS Queries (24h)'}
|
|
128
|
+
.series=${this.getDnsQuerySeries()}
|
|
129
|
+
.yAxisFormatter=${(val: number) => `${val}`}
|
|
130
|
+
></dees-chart-area>
|
|
131
|
+
<dees-chart-log
|
|
132
|
+
.label=${'Recent Events'}
|
|
133
|
+
.logEntries=${this.getRecentEventEntries()}
|
|
134
|
+
></dees-chart-log>
|
|
135
|
+
<dees-chart-log
|
|
136
|
+
.label=${'Security Alerts'}
|
|
137
|
+
.logEntries=${this.getSecurityAlertEntries()}
|
|
138
|
+
></dees-chart-log>
|
|
103
139
|
</div>
|
|
104
140
|
`}
|
|
105
141
|
`;
|
|
@@ -337,4 +373,42 @@ export class OpsViewOverview extends DeesElement {
|
|
|
337
373
|
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
|
|
338
374
|
`;
|
|
339
375
|
}
|
|
376
|
+
|
|
377
|
+
// --- Chart data helpers ---
|
|
378
|
+
|
|
379
|
+
private getRecentEventEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
|
|
380
|
+
return this.logState.recentLogs.map((log) => ({
|
|
381
|
+
timestamp: new Date(log.timestamp).toISOString(),
|
|
382
|
+
level: log.level as 'debug' | 'info' | 'warn' | 'error',
|
|
383
|
+
message: log.message,
|
|
384
|
+
source: log.category,
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private getSecurityAlertEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
|
|
389
|
+
const events: any[] = this.statsState.securityMetrics?.recentEvents || [];
|
|
390
|
+
return events.map((evt: any) => ({
|
|
391
|
+
timestamp: new Date(evt.timestamp).toISOString(),
|
|
392
|
+
level: evt.level === 'critical' || evt.level === 'error' ? 'error' as const : evt.level === 'warn' ? 'warn' as const : 'info' as const,
|
|
393
|
+
message: evt.message,
|
|
394
|
+
source: evt.type,
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private getEmailTrafficSeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
|
|
399
|
+
const ts = this.statsState.emailStats?.timeSeries;
|
|
400
|
+
if (!ts) return [];
|
|
401
|
+
return [
|
|
402
|
+
{ name: 'Sent', color: '#22c55e', data: (ts.sent || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
|
403
|
+
{ name: 'Received', color: '#3b82f6', data: (ts.received || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
|
404
|
+
];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private getDnsQuerySeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
|
|
408
|
+
const ts = this.statsState.dnsStats?.timeSeries;
|
|
409
|
+
if (!ts) return [];
|
|
410
|
+
return [
|
|
411
|
+
{ name: 'Queries', color: '#8b5cf6', data: (ts.queries || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
|
412
|
+
];
|
|
413
|
+
}
|
|
340
414
|
}
|