@serve.zone/dcrouter 13.22.0 → 13.24.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.
Files changed (51) hide show
  1. package/dist_serve/bundle.js +952 -792
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +4 -0
  4. package/dist_ts/classes.dcrouter.js +69 -2
  5. package/dist_ts/db/documents/classes.ip-intelligence.doc.d.ts +25 -0
  6. package/dist_ts/db/documents/classes.ip-intelligence.doc.js +175 -0
  7. package/dist_ts/db/documents/classes.security-block-rule.doc.d.ts +17 -0
  8. package/dist_ts/db/documents/classes.security-block-rule.doc.js +124 -0
  9. package/dist_ts/db/documents/classes.security-policy-audit.doc.d.ts +11 -0
  10. package/dist_ts/db/documents/classes.security-policy-audit.doc.js +95 -0
  11. package/dist_ts/db/documents/index.d.ts +3 -0
  12. package/dist_ts/db/documents/index.js +4 -1
  13. package/dist_ts/monitoring/classes.metricsmanager.js +2 -1
  14. package/dist_ts/opsserver/handlers/security.handler.js +63 -1
  15. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +10 -0
  16. package/dist_ts/remoteingress/classes.remoteingress-manager.js +9 -1
  17. package/dist_ts/security/classes.security-policy-manager.d.ts +46 -0
  18. package/dist_ts/security/classes.security-policy-manager.js +304 -0
  19. package/dist_ts/security/index.d.ts +1 -0
  20. package/dist_ts/security/index.js +2 -1
  21. package/dist_ts_interfaces/data/index.d.ts +1 -0
  22. package/dist_ts_interfaces/data/index.js +2 -1
  23. package/dist_ts_interfaces/data/security-policy.d.ts +32 -0
  24. package/dist_ts_interfaces/data/security-policy.js +2 -0
  25. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  26. package/dist_ts_interfaces/requests/index.js +2 -1
  27. package/dist_ts_interfaces/requests/security-policy.d.ts +95 -0
  28. package/dist_ts_interfaces/requests/security-policy.js +2 -0
  29. package/dist_ts_web/00_commitinfo_data.js +1 -1
  30. package/dist_ts_web/appstate.d.ts +28 -0
  31. package/dist_ts_web/appstate.js +171 -4
  32. package/dist_ts_web/elements/network/ops-view-network-activity.d.ts +9 -0
  33. package/dist_ts_web/elements/network/ops-view-network-activity.js +210 -3
  34. package/dist_ts_web/elements/security/ops-view-security-blocked.d.ts +12 -3
  35. package/dist_ts_web/elements/security/ops-view-security-blocked.js +407 -52
  36. package/package.json +3 -3
  37. package/ts/00_commitinfo_data.ts +1 -1
  38. package/ts/classes.dcrouter.ts +91 -3
  39. package/ts/db/documents/classes.ip-intelligence.doc.ts +75 -0
  40. package/ts/db/documents/classes.security-block-rule.doc.ts +52 -0
  41. package/ts/db/documents/classes.security-policy-audit.doc.ts +33 -0
  42. package/ts/db/documents/index.ts +3 -0
  43. package/ts/monitoring/classes.metricsmanager.ts +2 -0
  44. package/ts/opsserver/handlers/security.handler.ts +107 -0
  45. package/ts/remoteingress/classes.remoteingress-manager.ts +15 -2
  46. package/ts/security/classes.security-policy-manager.ts +340 -0
  47. package/ts/security/index.ts +7 -1
  48. package/ts_web/00_commitinfo_data.ts +1 -1
  49. package/ts_web/appstate.ts +236 -3
  50. package/ts_web/elements/network/ops-view-network-activity.ts +219 -2
  51. package/ts_web/elements/security/ops-view-security-blocked.ts +414 -51
@@ -1,4 +1,5 @@
1
1
  import * as appstate from '../../appstate.js';
2
+ import * as interfaces from '../../../dist_ts_interfaces/index.js';
2
3
  import { viewHostCss } from '../shared/css.js';
3
4
 
4
5
  import {
@@ -21,18 +22,23 @@ declare global {
21
22
  @customElement('ops-view-security-blocked')
22
23
  export class OpsViewSecurityBlocked extends DeesElement {
23
24
  @state()
24
- accessor statsState: appstate.IStatsState = appstate.statsStatePart.getState()!;
25
+ accessor securityPolicyState: appstate.ISecurityPolicyState = appstate.securityPolicyStatePart.getState()!;
25
26
 
26
27
  constructor() {
27
28
  super();
28
- const sub = appstate.statsStatePart
29
+ const sub = appstate.securityPolicyStatePart
29
30
  .select((s) => s)
30
31
  .subscribe((s) => {
31
- this.statsState = s;
32
+ this.securityPolicyState = s;
32
33
  });
33
34
  this.rxSubscriptions.push(sub);
34
35
  }
35
36
 
37
+ public async connectedCallback() {
38
+ await super.connectedCallback();
39
+ await appstate.securityPolicyStatePart.dispatchAction(appstate.fetchSecurityPolicyAction, null);
40
+ }
41
+
36
42
  public static styles = [
37
43
  cssManager.defaultStyles,
38
44
  viewHostCss,
@@ -40,79 +46,436 @@ export class OpsViewSecurityBlocked extends DeesElement {
40
46
  dees-statsgrid {
41
47
  margin-bottom: 32px;
42
48
  }
49
+
50
+ .sectionStack {
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: 32px;
54
+ }
55
+
56
+ .statusBadge {
57
+ display: inline-flex;
58
+ align-items: center;
59
+ padding: 4px 8px;
60
+ border-radius: 4px;
61
+ font-size: 12px;
62
+ font-weight: 500;
63
+ }
64
+
65
+ .statusBadge.enabled {
66
+ background: ${cssManager.bdTheme('#e8f5e9', '#1a3a1a')};
67
+ color: ${cssManager.bdTheme('#388e3c', '#66bb6a')};
68
+ }
69
+
70
+ .statusBadge.disabled {
71
+ background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')};
72
+ color: ${cssManager.bdTheme('#757575', '#999')};
73
+ }
74
+
75
+ .typeBadge {
76
+ display: inline-flex;
77
+ align-items: center;
78
+ padding: 4px 8px;
79
+ border-radius: 999px;
80
+ font-size: 12px;
81
+ font-weight: 500;
82
+ background: ${cssManager.bdTheme('#eef2ff', '#1e1b4b')};
83
+ color: ${cssManager.bdTheme('#4338ca', '#a5b4fc')};
84
+ }
85
+
86
+ .errorMessage {
87
+ padding: 12px 16px;
88
+ border-radius: 8px;
89
+ background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
90
+ color: ${cssManager.bdTheme('#b91c1c', '#fca5a5')};
91
+ }
43
92
  `,
44
93
  ];
45
94
 
46
95
  public render(): TemplateResult {
47
- const metrics = this.statsState.securityMetrics;
48
-
49
- if (!metrics) {
50
- return html`
51
- <div class="loadingMessage">
52
- <p>Loading security metrics...</p>
53
- </div>
54
- `;
55
- }
56
-
57
- const blockedIPs: string[] = metrics.blockedIPs || [];
96
+ const state = this.securityPolicyState;
97
+ const activeRules = state.rules.filter((rule) => rule.enabled);
98
+ const disabledRules = state.rules.length - activeRules.length;
99
+ const compiledPolicy = state.compiledPolicy || { blockedIps: [], blockedCidrs: [] };
58
100
 
59
101
  const tiles: IStatsTile[] = [
60
102
  {
61
- id: 'totalBlocked',
62
- title: 'Blocked IPs',
63
- value: blockedIPs.length,
103
+ id: 'activeRules',
104
+ title: 'Active Rules',
105
+ value: activeRules.length,
64
106
  type: 'number',
65
- icon: 'lucide:ShieldBan',
66
- color: blockedIPs.length > 0 ? '#ef4444' : '#22c55e',
67
- description: 'Currently blocked addresses',
107
+ icon: 'lucide:shield-check',
108
+ color: activeRules.length > 0 ? '#ef4444' : '#22c55e',
109
+ description: `${disabledRules} disabled`,
110
+ },
111
+ {
112
+ id: 'compiledIps',
113
+ title: 'Compiled IPs',
114
+ value: compiledPolicy.blockedIps.length,
115
+ type: 'number',
116
+ icon: 'lucide:server-off',
117
+ color: '#ef4444',
118
+ description: 'Direct IP blocks enforced by SmartProxy',
119
+ },
120
+ {
121
+ id: 'compiledCidrs',
122
+ title: 'Compiled CIDRs',
123
+ value: compiledPolicy.blockedCidrs.length,
124
+ type: 'number',
125
+ icon: 'lucide:network',
126
+ color: '#f97316',
127
+ description: 'Network ranges pushed to enforcement layers',
128
+ },
129
+ {
130
+ id: 'intelligenceRecords',
131
+ title: 'IP Intelligence',
132
+ value: state.ipIntelligence.length,
133
+ type: 'number',
134
+ icon: 'lucide:radar',
135
+ color: '#6366f1',
136
+ description: 'Observed public IPs with enrichment',
68
137
  },
69
138
  ];
70
139
 
71
140
  return html`
72
- <dees-heading level="3">Blocked IPs</dees-heading>
141
+ <dees-heading level="3">Security Blocking</dees-heading>
142
+
143
+ ${state.error ? html`<div class="errorMessage">${state.error}</div>` : html``}
73
144
 
74
145
  <dees-statsgrid
75
146
  .tiles=${tiles}
76
147
  .minTileWidth=${200}
77
148
  ></dees-statsgrid>
78
149
 
150
+ <div class="sectionStack">
151
+ ${this.renderRulesTable()}
152
+ ${this.renderCompiledPolicyTable()}
153
+ ${this.renderIpIntelligenceTable()}
154
+ ${this.renderAuditTable()}
155
+ </div>
156
+ `;
157
+ }
158
+
159
+ private renderRulesTable(): TemplateResult {
160
+ return html`
79
161
  <dees-table
80
- .heading1=${'Blocked IP Addresses'}
81
- .heading2=${'IPs blocked due to suspicious activity'}
82
- .data=${blockedIPs.map((ip) => ({ ip }))}
83
- .displayFunction=${(item) => ({
84
- 'IP Address': item.ip,
85
- 'Reason': 'Suspicious activity',
162
+ .heading1=${'Managed Block Rules'}
163
+ .heading2=${'Rules compiled into SmartProxy policy and remote ingress edge firewall snapshots'}
164
+ .data=${this.securityPolicyState.rules}
165
+ .rowKey=${'id'}
166
+ .displayFunction=${(rule: interfaces.data.ISecurityBlockRule) => ({
167
+ 'Type': html`<span class="typeBadge">${rule.type}</span>`,
168
+ 'Value': rule.value,
169
+ 'Match': rule.type === 'organization' ? (rule.matchMode || 'contains') : '-',
170
+ 'Reason': rule.reason || '-',
171
+ 'Status': html`<span class="statusBadge ${rule.enabled ? 'enabled' : 'disabled'}">${rule.enabled ? 'Enabled' : 'Disabled'}</span>`,
172
+ 'Created': this.formatDateTime(rule.createdAt),
173
+ 'Updated': this.formatDateTime(rule.updatedAt),
86
174
  })}
87
- .dataActions=${[
88
- {
89
- name: 'Unblock',
90
- iconName: 'lucide:shield-off',
91
- type: ['contextmenu' as const],
92
- actionFunc: async (item) => {
93
- await this.unblockIP(item.ip);
94
- },
95
- },
96
- {
97
- name: 'Clear All',
98
- iconName: 'lucide:trash-2',
99
- type: ['header' as const],
100
- actionFunc: async () => {
101
- await this.clearBlockedIPs();
102
- },
103
- },
104
- ]}
175
+ .dataActions=${this.getRuleActions()}
176
+ searchable
177
+ .showColumnFilters=${true}
178
+ dataName="rule"
179
+ ></dees-table>
180
+ `;
181
+ }
182
+
183
+ private renderCompiledPolicyTable(): TemplateResult {
184
+ const policy = this.securityPolicyState.compiledPolicy || { blockedIps: [], blockedCidrs: [] };
185
+ const rows = [
186
+ ...policy.blockedIps.map((value) => ({ type: 'ip', value })),
187
+ ...policy.blockedCidrs.map((value) => ({ type: 'cidr', value })),
188
+ ];
189
+
190
+ return html`
191
+ <dees-table
192
+ .heading1=${'Compiled Enforcement Policy'}
193
+ .heading2=${'Concrete IPs and CIDRs currently sent to SmartProxy and remote ingress'}
194
+ .data=${rows}
195
+ .rowKey=${'value'}
196
+ .displayFunction=${(row: { type: string; value: string }) => ({
197
+ 'Enforcement Type': html`<span class="typeBadge">${row.type}</span>`,
198
+ 'Value': row.value,
199
+ })}
200
+ searchable
201
+ .showColumnFilters=${true}
202
+ dataName="compiled rule"
105
203
  ></dees-table>
106
204
  `;
107
205
  }
108
206
 
109
- private async clearBlockedIPs() {
110
- // SmartProxy manages IP blocking — not yet exposed via API
111
- alert('Clearing blocked IPs is not yet supported from the UI.');
207
+ private renderIpIntelligenceTable(): TemplateResult {
208
+ return html`
209
+ <dees-table
210
+ .heading1=${'Observed IP Intelligence'}
211
+ .heading2=${'Public IPs observed in network metrics and enriched for ASN / organization matching'}
212
+ .data=${this.securityPolicyState.ipIntelligence}
213
+ .rowKey=${'ipAddress'}
214
+ .displayFunction=${(record: interfaces.data.IIpIntelligenceRecord) => ({
215
+ 'IP Address': record.ipAddress,
216
+ 'ASN': record.asn ? `AS${record.asn}` : '-',
217
+ 'ASN Org': record.asnOrg || '-',
218
+ 'Registrant Org': record.registrantOrg || '-',
219
+ 'Country': record.countryCode || record.country || '-',
220
+ 'Network Range': record.networkRange || '-',
221
+ 'Abuse Contact': record.abuseContact || '-',
222
+ 'Seen': record.seenCount,
223
+ 'Last Seen': this.formatDateTime(record.lastSeenAt),
224
+ })}
225
+ .dataActions=${this.getIpIntelligenceActions()}
226
+ searchable
227
+ .showColumnFilters=${true}
228
+ dataName="ip intelligence record"
229
+ ></dees-table>
230
+ `;
231
+ }
232
+
233
+ private renderAuditTable(): TemplateResult {
234
+ return html`
235
+ <dees-table
236
+ .heading1=${'Policy Audit'}
237
+ .heading2=${'Recent security policy changes'}
238
+ .data=${this.securityPolicyState.auditEvents}
239
+ .rowKey=${'id'}
240
+ .displayFunction=${(event: interfaces.data.ISecurityPolicyAuditEvent) => ({
241
+ 'Time': this.formatDateTime(event.createdAt),
242
+ 'Action': event.action,
243
+ 'Actor': event.actor,
244
+ 'Details': this.formatAuditDetails(event.details),
245
+ })}
246
+ searchable
247
+ .showColumnFilters=${true}
248
+ dataName="audit event"
249
+ ></dees-table>
250
+ `;
251
+ }
252
+
253
+ private getRuleActions() {
254
+ return [
255
+ {
256
+ name: 'Create Rule',
257
+ iconName: 'lucide:plus',
258
+ type: ['header'] as any,
259
+ actionFunc: async () => this.showRuleDialog(),
260
+ },
261
+ {
262
+ name: 'Edit',
263
+ iconName: 'lucide:pencil',
264
+ type: ['inRow', 'contextmenu'] as any,
265
+ actionFunc: async (actionData: any) => this.showRuleDialog(actionData.item),
266
+ },
267
+ {
268
+ name: 'Enable',
269
+ iconName: 'lucide:play',
270
+ type: ['contextmenu'] as any,
271
+ actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled,
272
+ actionFunc: async (actionData: any) => {
273
+ const rule = actionData.item as interfaces.data.ISecurityBlockRule;
274
+ await appstate.securityPolicyStatePart.dispatchAction(appstate.updateSecurityBlockRuleAction, {
275
+ id: rule.id,
276
+ enabled: true,
277
+ });
278
+ },
279
+ },
280
+ {
281
+ name: 'Disable',
282
+ iconName: 'lucide:pause',
283
+ type: ['contextmenu'] as any,
284
+ actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled,
285
+ actionFunc: async (actionData: any) => {
286
+ const rule = actionData.item as interfaces.data.ISecurityBlockRule;
287
+ await appstate.securityPolicyStatePart.dispatchAction(appstate.updateSecurityBlockRuleAction, {
288
+ id: rule.id,
289
+ enabled: false,
290
+ });
291
+ },
292
+ },
293
+ {
294
+ name: 'Delete',
295
+ iconName: 'lucide:trash-2',
296
+ type: ['contextmenu'] as any,
297
+ actionFunc: async (actionData: any) => {
298
+ const rule = actionData.item as interfaces.data.ISecurityBlockRule;
299
+ if (!window.confirm(`Delete block rule ${rule.type}:${rule.value}?`)) return;
300
+ await appstate.securityPolicyStatePart.dispatchAction(appstate.deleteSecurityBlockRuleAction, rule.id);
301
+ },
302
+ },
303
+ ];
304
+ }
305
+
306
+ private getIpIntelligenceActions() {
307
+ return [
308
+ {
309
+ name: 'Refresh Intelligence',
310
+ iconName: 'lucide:refresh-cw',
311
+ type: ['inRow', 'contextmenu'] as any,
312
+ actionFunc: async (actionData: any) => {
313
+ const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
314
+ await appstate.securityPolicyStatePart.dispatchAction(appstate.refreshIpIntelligenceAction, record.ipAddress);
315
+ },
316
+ },
317
+ {
318
+ name: 'Block IP',
319
+ iconName: 'lucide:shield-ban',
320
+ type: ['contextmenu'] as any,
321
+ actionFunc: async (actionData: any) => {
322
+ const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
323
+ await this.showRuleDialog(undefined, {
324
+ type: 'ip',
325
+ value: record.ipAddress,
326
+ reason: 'Blocked from IP intelligence table',
327
+ });
328
+ },
329
+ },
330
+ {
331
+ name: 'Block Network Range',
332
+ iconName: 'lucide:network',
333
+ type: ['contextmenu'] as any,
334
+ actionRelevancyCheckFunc: (actionData: any) => Boolean(actionData.item.networkRange),
335
+ actionFunc: async (actionData: any) => {
336
+ const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
337
+ await this.showRuleDialog(undefined, {
338
+ type: 'cidr',
339
+ value: record.networkRange || '',
340
+ reason: 'Blocked network range from IP intelligence table',
341
+ });
342
+ },
343
+ },
344
+ {
345
+ name: 'Block ASN',
346
+ iconName: 'lucide:radio-tower',
347
+ type: ['contextmenu'] as any,
348
+ actionRelevancyCheckFunc: (actionData: any) => Boolean(actionData.item.asn),
349
+ actionFunc: async (actionData: any) => {
350
+ const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
351
+ await this.showRuleDialog(undefined, {
352
+ type: 'asn',
353
+ value: String(record.asn),
354
+ reason: 'Blocked ASN from IP intelligence table',
355
+ });
356
+ },
357
+ },
358
+ {
359
+ name: 'Block Organization',
360
+ iconName: 'lucide:building-2',
361
+ type: ['contextmenu'] as any,
362
+ actionRelevancyCheckFunc: (actionData: any) => Boolean(actionData.item.asnOrg || actionData.item.registrantOrg),
363
+ actionFunc: async (actionData: any) => {
364
+ const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
365
+ await this.showRuleDialog(undefined, {
366
+ type: 'organization',
367
+ value: record.asnOrg || record.registrantOrg || '',
368
+ reason: 'Blocked organization from IP intelligence table',
369
+ });
370
+ },
371
+ },
372
+ ];
373
+ }
374
+
375
+ private async showRuleDialog(
376
+ rule?: interfaces.data.ISecurityBlockRule,
377
+ defaults: Partial<interfaces.data.ISecurityBlockRule> = {},
378
+ ): Promise<void> {
379
+ const { DeesModal } = await import('@design.estate/dees-catalog');
380
+ const typeOptions = [
381
+ { key: 'ip', option: 'IP address' },
382
+ { key: 'cidr', option: 'CIDR / network range' },
383
+ { key: 'asn', option: 'ASN' },
384
+ { key: 'organization', option: 'Organization' },
385
+ ];
386
+ const matchModeOptions = [
387
+ { key: 'contains', option: 'Organization contains value' },
388
+ { key: 'exact', option: 'Organization exactly matches value' },
389
+ ];
390
+ const selectedType = rule?.type || defaults.type || 'ip';
391
+ const selectedMatchMode = rule?.matchMode || defaults.matchMode || 'contains';
392
+
393
+ await DeesModal.createAndShow({
394
+ heading: rule ? `Edit Block Rule: ${rule.type}:${rule.value}` : 'Create Block Rule',
395
+ content: html`
396
+ <dees-form>
397
+ ${rule ? html`` : html`
398
+ <dees-input-dropdown
399
+ .key=${'type'}
400
+ .label=${'Rule Type'}
401
+ .options=${typeOptions}
402
+ .selectedOption=${typeOptions.find((option) => option.key === selectedType)}
403
+ ></dees-input-dropdown>
404
+ `}
405
+ <dees-input-text
406
+ .key=${'value'}
407
+ .label=${'Value'}
408
+ .value=${rule?.value || defaults.value || ''}
409
+ .required=${true}
410
+ ></dees-input-text>
411
+ <dees-input-dropdown
412
+ .key=${'matchMode'}
413
+ .label=${'Organization Match Mode'}
414
+ .description=${'Only used for organization rules'}
415
+ .options=${matchModeOptions}
416
+ .selectedOption=${matchModeOptions.find((option) => option.key === selectedMatchMode)}
417
+ ></dees-input-dropdown>
418
+ <dees-input-text
419
+ .key=${'reason'}
420
+ .label=${'Reason'}
421
+ .value=${rule?.reason || defaults.reason || ''}
422
+ ></dees-input-text>
423
+ <dees-input-checkbox
424
+ .key=${'enabled'}
425
+ .label=${'Enabled'}
426
+ .value=${rule ? rule.enabled : defaults.enabled !== false}
427
+ ></dees-input-checkbox>
428
+ </dees-form>
429
+ `,
430
+ menuOptions: [
431
+ { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => modalArg.destroy() },
432
+ {
433
+ name: rule ? 'Save' : 'Create',
434
+ iconName: rule ? 'lucide:check' : 'lucide:plus',
435
+ action: async (modalArg: any) => {
436
+ const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
437
+ if (!form) return;
438
+ const data = await form.collectFormData();
439
+ const type = (rule?.type || this.getDropdownKey(data.type)) as interfaces.data.TSecurityBlockRuleType;
440
+ const value = String(data.value || '').trim();
441
+ if (!type || !value) return;
442
+ const matchMode = type === 'organization'
443
+ ? this.getDropdownKey(data.matchMode) as interfaces.data.TSecurityBlockRuleMatchMode
444
+ : undefined;
445
+ const payload = {
446
+ value,
447
+ matchMode,
448
+ reason: String(data.reason || '').trim() || undefined,
449
+ enabled: data.enabled !== false,
450
+ };
451
+ if (rule) {
452
+ await appstate.securityPolicyStatePart.dispatchAction(appstate.updateSecurityBlockRuleAction, {
453
+ id: rule.id,
454
+ ...payload,
455
+ });
456
+ } else {
457
+ await appstate.securityPolicyStatePart.dispatchAction(appstate.createSecurityBlockRuleAction, {
458
+ type,
459
+ ...payload,
460
+ });
461
+ }
462
+ await modalArg.destroy();
463
+ },
464
+ },
465
+ ],
466
+ });
467
+ }
468
+
469
+ private getDropdownKey(value: any): string {
470
+ return typeof value === 'string' ? value : value?.key || '';
471
+ }
472
+
473
+ private formatDateTime(timestamp?: number): string {
474
+ return timestamp ? new Date(timestamp).toLocaleString() : '-';
112
475
  }
113
476
 
114
- private async unblockIP(ip: string) {
115
- // SmartProxy manages IP blocking — not yet exposed via API
116
- alert(`Unblocking IP ${ip} is not yet supported from the UI.`);
477
+ private formatAuditDetails(details: Record<string, unknown>): string {
478
+ const text = JSON.stringify(details);
479
+ return text.length > 160 ? `${text.slice(0, 157)}...` : text;
117
480
  }
118
481
  }