@serve.zone/dcrouter 7.0.1 → 7.2.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.
@@ -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.2.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
@@ -46,87 +55,20 @@ export class OpsViewLogs extends DeesElement {
46
55
  align-items: center;
47
56
  gap: 8px;
48
57
  }
49
-
50
- .logContainer {
51
- background: ${cssManager.bdTheme('#f8f9fa', '#1e1e1e')};
52
- border-radius: 8px;
53
- padding: 16px;
54
- max-height: 600px;
55
- overflow-y: auto;
56
- font-family: 'Consolas', 'Monaco', monospace;
57
- font-size: 13px;
58
- }
59
-
60
- .logEntry {
61
- margin-bottom: 8px;
62
- line-height: 1.5;
63
- }
64
-
65
- .logTimestamp {
66
- color: ${cssManager.bdTheme('#7a7a7a', '#7a7a7a')};
67
- margin-right: 8px;
68
- }
69
-
70
- .logLevel {
71
- font-weight: bold;
72
- margin-right: 8px;
73
- padding: 2px 6px;
74
- border-radius: 3px;
75
- font-size: 11px;
76
- }
77
-
78
- .logLevel.debug {
79
- color: ${cssManager.bdTheme('#6a9955', '#6a9955')};
80
- background: ${cssManager.bdTheme('rgba(106, 153, 85, 0.1)', 'rgba(106, 153, 85, 0.1)')};
81
- }
82
- .logLevel.info {
83
- color: ${cssManager.bdTheme('#569cd6', '#569cd6')};
84
- background: ${cssManager.bdTheme('rgba(86, 156, 214, 0.1)', 'rgba(86, 156, 214, 0.1)')};
85
- }
86
- .logLevel.warn {
87
- color: ${cssManager.bdTheme('#ce9178', '#ce9178')};
88
- background: ${cssManager.bdTheme('rgba(206, 145, 120, 0.1)', 'rgba(206, 145, 120, 0.1)')};
89
- }
90
- .logLevel.error {
91
- color: ${cssManager.bdTheme('#f44747', '#f44747')};
92
- background: ${cssManager.bdTheme('rgba(244, 71, 71, 0.1)', 'rgba(244, 71, 71, 0.1)')};
93
- }
94
-
95
- .logCategory {
96
- color: ${cssManager.bdTheme('#c586c0', '#c586c0')};
97
- margin-right: 8px;
98
- }
99
-
100
- .logMessage {
101
- color: ${cssManager.bdTheme('#333', '#d4d4d4')};
102
- }
103
-
104
- .noLogs {
105
- color: ${cssManager.bdTheme('#7a7a7a', '#7a7a7a')};
106
- text-align: center;
107
- padding: 40px;
108
- }
109
58
  `,
110
59
  ];
111
60
 
112
61
  public render() {
113
62
  return html`
114
63
  <ops-sectionheading>Logs</ops-sectionheading>
115
-
64
+
116
65
  <div class="controls">
117
66
  <div class="filterGroup">
118
- <dees-button
67
+ <dees-button
119
68
  @click=${() => this.fetchLogs()}
120
69
  >
121
70
  Refresh Logs
122
71
  </dees-button>
123
-
124
- <dees-button
125
- @click=${() => this.toggleStreaming()}
126
- .type=${this.logState.isStreaming ? 'highlighted' : 'normal'}
127
- >
128
- ${this.logState.isStreaming ? 'Stop Streaming' : 'Start Streaming'}
129
- </dees-button>
130
72
  </div>
131
73
 
132
74
  <div class="filterGroup">
@@ -134,7 +76,7 @@ export class OpsViewLogs extends DeesElement {
134
76
  <dees-input-dropdown
135
77
  .options=${['all', 'debug', 'info', 'warn', 'error']}
136
78
  .selectedOption=${'all'}
137
- @selectedOption=${(e) => this.updateFilter('level', e.detail)}
79
+ @selectedOption=${(e: any) => this.updateFilter('level', e.detail)}
138
80
  ></dees-input-dropdown>
139
81
  </div>
140
82
 
@@ -143,7 +85,7 @@ export class OpsViewLogs extends DeesElement {
143
85
  <dees-input-dropdown
144
86
  .options=${['all', 'smtp', 'dns', 'security', 'system', 'email']}
145
87
  .selectedOption=${'all'}
146
- @selectedOption=${(e) => this.updateFilter('category', e.detail)}
88
+ @selectedOption=${(e: any) => this.updateFilter('category', e.detail)}
147
89
  ></dees-input-dropdown>
148
90
  </div>
149
91
 
@@ -152,56 +94,78 @@ export class OpsViewLogs extends DeesElement {
152
94
  <dees-input-dropdown
153
95
  .options=${['50', '100', '200', '500']}
154
96
  .selectedOption=${'100'}
155
- @selectedOption=${(e) => this.updateFilter('limit', e.detail)}
97
+ @selectedOption=${(e: any) => this.updateFilter('limit', e.detail)}
156
98
  ></dees-input-dropdown>
157
99
  </div>
158
100
  </div>
159
101
 
160
- <div class="logContainer">
161
- ${this.logState.recentLogs.length > 0 ?
162
- this.logState.recentLogs.map(log => html`
163
- <div class="logEntry">
164
- <span class="logTimestamp">${new Date(log.timestamp).toLocaleTimeString()}</span>
165
- <span class="logLevel ${log.level}">${log.level.toUpperCase()}</span>
166
- <span class="logCategory">[${log.category}]</span>
167
- <span class="logMessage">${log.message}</span>
168
- </div>
169
- `) : html`
170
- <div class="noLogs">No logs to display</div>
171
- `
172
- }
173
- </div>
102
+ <dees-chart-log
103
+ .label=${'Application Logs'}
104
+ .autoScroll=${true}
105
+ .maxEntries=${2000}
106
+ .showMetrics=${true}
107
+ ></dees-chart-log>
174
108
  `;
175
109
  }
176
110
 
111
+ async connectedCallback() {
112
+ super.connectedCallback();
113
+ this.fetchLogs();
114
+ }
115
+
116
+ async updated(changedProperties: Map<string, any>) {
117
+ super.updated(changedProperties);
118
+ if (changedProperties.has('logState')) {
119
+ this.pushLogsToChart();
120
+ }
121
+ }
122
+
123
+ private async pushLogsToChart() {
124
+ const chartLog = this.shadowRoot?.querySelector('dees-chart-log') as any;
125
+ if (!chartLog) return;
126
+
127
+ // Ensure the chart element has finished its own initialization
128
+ await chartLog.updateComplete;
129
+
130
+ chartLog.clearLogs();
131
+ const entries = this.getMappedLogEntries();
132
+ if (entries.length > 0) {
133
+ chartLog.updateLog(entries);
134
+ }
135
+ }
136
+
137
+ private getMappedLogEntries() {
138
+ return this.logState.recentLogs.map((log) => ({
139
+ timestamp: new Date(log.timestamp).toISOString(),
140
+ level: log.level as 'debug' | 'info' | 'warn' | 'error',
141
+ message: log.message,
142
+ source: log.category,
143
+ }));
144
+ }
145
+
177
146
  private async fetchLogs() {
178
- const filters = this.getActiveFilters();
179
147
  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,
148
+ limit: this.filterLimit,
149
+ level: this.filterLevel as 'debug' | 'info' | 'warn' | 'error' | undefined,
150
+ category: this.filterCategory as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
183
151
  });
184
152
  }
185
153
 
186
154
  private updateFilter(type: string, value: string) {
187
- if (value === 'all') {
188
- value = undefined;
155
+ const resolved = value === 'all' ? undefined : value;
156
+
157
+ switch (type) {
158
+ case 'level':
159
+ this.filterLevel = resolved;
160
+ break;
161
+ case 'category':
162
+ this.filterCategory = resolved;
163
+ break;
164
+ case 'limit':
165
+ this.filterLimit = resolved ? parseInt(resolved, 10) : 100;
166
+ break;
189
167
  }
190
-
191
- // Update filters then fetch logs
192
- this.fetchLogs();
193
- }
194
168
 
195
- private getActiveFilters() {
196
- return {
197
- level: this.logState.filters.level?.[0],
198
- category: this.logState.filters.category?.[0],
199
- limit: 100,
200
- };
201
- }
202
-
203
- private toggleStreaming() {
204
- // TODO: Implement log streaming with VirtualStream
205
- console.log('Streaming toggle not yet implemented');
169
+ this.fetchLogs();
206
170
  }
207
- }
171
+ }
@@ -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
  }
@@ -249,7 +249,14 @@ export class OpsViewSecurity extends DeesElement {
249
249
  private renderOverview(metrics: any) {
250
250
  const threatLevel = this.calculateThreatLevel(metrics);
251
251
  const threatScore = this.getThreatScore(metrics);
252
-
252
+
253
+ // Derive active sessions from recent successful auth events (last hour)
254
+ const allEvents: any[] = metrics.recentEvents || [];
255
+ const oneHourAgo = Date.now() - 3600000;
256
+ const recentAuthSuccesses = allEvents.filter(
257
+ (evt: any) => evt.type === 'authentication' && evt.success === true && evt.timestamp >= oneHourAgo
258
+ ).length;
259
+
253
260
  const tiles: IStatsTile[] = [
254
261
  {
255
262
  id: 'threatLevel',
@@ -271,7 +278,7 @@ export class OpsViewSecurity extends DeesElement {
271
278
  {
272
279
  id: 'blockedThreats',
273
280
  title: 'Blocked Threats',
274
- value: metrics.blockedIPs.length + metrics.spamDetected,
281
+ value: (metrics.blockedIPs?.length || 0) + metrics.spamDetected,
275
282
  type: 'number',
276
283
  icon: 'lucide:ShieldCheck',
277
284
  color: '#ef4444',
@@ -280,11 +287,11 @@ export class OpsViewSecurity extends DeesElement {
280
287
  {
281
288
  id: 'activeSessions',
282
289
  title: 'Active Sessions',
283
- value: 0,
290
+ value: recentAuthSuccesses,
284
291
  type: 'number',
285
292
  icon: 'lucide:Users',
286
293
  color: '#22c55e',
287
- description: 'Current authenticated sessions',
294
+ description: 'Authenticated in last hour',
288
295
  },
289
296
  {
290
297
  id: 'authFailures',
@@ -349,6 +356,11 @@ export class OpsViewSecurity extends DeesElement {
349
356
  }
350
357
 
351
358
  private renderAuthentication(metrics: any) {
359
+ // Derive auth events from recentEvents
360
+ const allEvents: any[] = metrics.recentEvents || [];
361
+ const authEvents = allEvents.filter((evt: any) => evt.type === 'authentication');
362
+ const successfulLogins = authEvents.filter((evt: any) => evt.success === true).length;
363
+
352
364
  const tiles: IStatsTile[] = [
353
365
  {
354
366
  id: 'authFailures',
@@ -362,7 +374,7 @@ export class OpsViewSecurity extends DeesElement {
362
374
  {
363
375
  id: 'successfulLogins',
364
376
  title: 'Successful Logins',
365
- value: 0,
377
+ value: successfulLogins,
366
378
  type: 'number',
367
379
  icon: 'lucide:Lock',
368
380
  color: '#22c55e',
@@ -370,6 +382,15 @@ export class OpsViewSecurity extends DeesElement {
370
382
  },
371
383
  ];
372
384
 
385
+ // Map auth events to login history table data
386
+ const loginHistory = authEvents.map((evt: any) => ({
387
+ timestamp: evt.timestamp,
388
+ username: evt.details?.username || 'unknown',
389
+ ipAddress: evt.ipAddress || 'unknown',
390
+ success: evt.success ?? false,
391
+ reason: evt.success ? '' : evt.message || 'Authentication failed',
392
+ }));
393
+
373
394
  return html`
374
395
  <dees-statsgrid
375
396
  .tiles=${tiles}
@@ -380,7 +401,7 @@ export class OpsViewSecurity extends DeesElement {
380
401
  <dees-table
381
402
  .heading1=${'Login History'}
382
403
  .heading2=${'Recent authentication attempts'}
383
- .data=${[]}
404
+ .data=${loginHistory}
384
405
  .displayFunction=${(item) => ({
385
406
  'Time': new Date(item.timestamp).toLocaleString(),
386
407
  'Username': item.username,
@@ -483,48 +504,38 @@ export class OpsViewSecurity extends DeesElement {
483
504
  private getThreatScore(metrics: any): number {
484
505
  // Simple scoring algorithm
485
506
  let score = 100;
486
- score -= metrics.blockedIPs.length * 2;
487
- score -= metrics.authenticationFailures * 1;
488
- score -= metrics.spamDetected * 0.5;
489
- score -= metrics.malwareDetected * 3;
490
- score -= metrics.phishingDetected * 3;
491
- score -= metrics.suspiciousActivities * 2;
507
+ const blockedCount = Array.isArray(metrics.blockedIPs) ? metrics.blockedIPs.length : (metrics.blockedIPs || 0);
508
+ score -= blockedCount * 2;
509
+ score -= (metrics.authenticationFailures || 0) * 1;
510
+ score -= (metrics.spamDetected || 0) * 0.5;
511
+ score -= (metrics.malwareDetected || 0) * 3;
512
+ score -= (metrics.phishingDetected || 0) * 3;
513
+ score -= (metrics.suspiciousActivities || 0) * 2;
492
514
  return Math.max(0, Math.min(100, Math.round(score)));
493
515
  }
494
516
 
495
517
  private getSecurityEvents(metrics: any): any[] {
496
- // Mock data - in real implementation, this would come from the server
497
- return [
498
- {
499
- timestamp: Date.now() - 1000 * 60 * 5,
500
- event: 'Multiple failed login attempts',
501
- severity: 'warning',
502
- details: 'IP: 192.168.1.100',
503
- },
504
- {
505
- timestamp: Date.now() - 1000 * 60 * 15,
506
- event: 'SPF check failed',
507
- severity: 'medium',
508
- details: 'Domain: example.com',
509
- },
510
- {
511
- timestamp: Date.now() - 1000 * 60 * 30,
512
- event: 'IP blocked due to spam',
513
- severity: 'high',
514
- details: 'IP: 10.0.0.1',
515
- },
516
- ];
518
+ const events: any[] = metrics.recentEvents || [];
519
+ return events.map((evt: any) => ({
520
+ timestamp: evt.timestamp,
521
+ event: evt.message,
522
+ severity: evt.level === 'critical' ? 'critical' : evt.level === 'error' ? 'high' : evt.level === 'warn' ? 'warning' : 'info',
523
+ details: evt.ipAddress ? `IP: ${evt.ipAddress}` : evt.domain ? `Domain: ${evt.domain}` : evt.type,
524
+ }));
517
525
  }
518
526
 
519
527
  private async clearBlockedIPs() {
520
- console.log('Clear blocked IPs');
528
+ // SmartProxy manages IP blocking — not yet exposed via API
529
+ alert('Clearing blocked IPs is not yet supported from the UI.');
521
530
  }
522
531
 
523
532
  private async unblockIP(ip: string) {
524
- console.log('Unblock IP:', ip);
533
+ // SmartProxy manages IP blocking — not yet exposed via API
534
+ alert(`Unblocking IP ${ip} is not yet supported from the UI.`);
525
535
  }
526
536
 
527
537
  private async saveEmailSecuritySettings() {
528
- console.log('Save email security settings');
538
+ // Config is read-only from the UI for now
539
+ alert('Email security settings are read-only. Update the dcrouter configuration file to change these settings.');
529
540
  }
530
541
  }