@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/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
- // TODO: Implement actual log retrieval from storage or logger
82
- // For now, return mock data
83
- const mockLogs: Array<{
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 categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
92
- const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
93
- const now = Date.now();
94
-
95
- // Generate some mock log entries
96
- for (let i = 0; i < 50; i++) {
97
- const mockCategory = categories[Math.floor(Math.random() * categories.length)];
98
- const mockLevel = levels[Math.floor(Math.random() * levels.length)];
99
-
100
- // Filter by requested criteria
101
- if (level && mockLevel !== level) continue;
102
- if (category && mockCategory !== category) continue;
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
- // Apply pagination
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
  );
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '7.0.1',
6
+ version: '7.1.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -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: filters.limit || 100,
181
- level: filters.level as 'debug' | 'info' | 'warn' | 'error' | undefined,
182
- category: filters.category as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
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
- if (value === 'all') {
188
- value = undefined;
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.logState.filters.level?.[0],
198
- category: this.logState.filters.category?.[0],
199
- limit: 100,
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 subscription = appstate.statsStatePart
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(subscription);
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 .label=${'Email Traffic (24h)'} .data=${[]}></dees-chart-area>
100
- <dees-chart-area .label=${'DNS Queries (24h)'} .data=${[]}></dees-chart-area>
101
- <dees-chart-log .label=${'Recent Events'} .data=${[]}></dees-chart-log>
102
- <dees-chart-log .label=${'Security Alerts'} .data=${[]}></dees-chart-log>
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
  }