@serve.zone/dcrouter 13.10.0 → 13.11.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 +1060 -958
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +2 -0
  4. package/dist_ts/classes.dcrouter.js +15 -1
  5. package/dist_ts/db/documents/classes.email-domain.doc.d.ts +16 -0
  6. package/dist_ts/db/documents/classes.email-domain.doc.js +118 -0
  7. package/dist_ts/db/documents/index.d.ts +1 -0
  8. package/dist_ts/db/documents/index.js +3 -1
  9. package/dist_ts/email/classes.email-domain.manager.d.ts +45 -0
  10. package/dist_ts/email/classes.email-domain.manager.js +272 -0
  11. package/dist_ts/email/index.d.ts +1 -0
  12. package/dist_ts/email/index.js +2 -0
  13. package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
  14. package/dist_ts/opsserver/classes.opsserver.js +3 -1
  15. package/dist_ts/opsserver/handlers/email-domain.handler.d.ts +16 -0
  16. package/dist_ts/opsserver/handlers/email-domain.handler.js +149 -0
  17. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  18. package/dist_ts/opsserver/handlers/index.js +2 -1
  19. package/dist_ts_interfaces/data/email-domain.d.ts +68 -0
  20. package/dist_ts_interfaces/data/email-domain.js +2 -0
  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/requests/email-domains.d.ts +140 -0
  24. package/dist_ts_interfaces/requests/email-domains.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_web/00_commitinfo_data.js +1 -1
  28. package/dist_ts_web/appstate.d.ts +20 -0
  29. package/dist_ts_web/appstate.js +81 -1
  30. package/dist_ts_web/elements/email/index.d.ts +1 -0
  31. package/dist_ts_web/elements/email/index.js +2 -1
  32. package/dist_ts_web/elements/email/ops-view-email-domains.d.ts +19 -0
  33. package/dist_ts_web/elements/email/ops-view-email-domains.js +403 -0
  34. package/dist_ts_web/elements/ops-dashboard.js +3 -1
  35. package/dist_ts_web/router.js +2 -2
  36. package/package.json +1 -1
  37. package/ts/00_commitinfo_data.ts +1 -1
  38. package/ts/classes.dcrouter.ts +17 -0
  39. package/ts/db/documents/classes.email-domain.doc.ts +53 -0
  40. package/ts/db/documents/index.ts +3 -0
  41. package/ts/email/classes.email-domain.manager.ts +316 -0
  42. package/ts/email/index.ts +1 -0
  43. package/ts/opsserver/classes.opsserver.ts +2 -0
  44. package/ts/opsserver/handlers/email-domain.handler.ts +194 -0
  45. package/ts/opsserver/handlers/index.ts +2 -1
  46. package/ts_web/00_commitinfo_data.ts +1 -1
  47. package/ts_web/appstate.ts +123 -0
  48. package/ts_web/elements/email/index.ts +1 -0
  49. package/ts_web/elements/email/ops-view-email-domains.ts +389 -0
  50. package/ts_web/elements/ops-dashboard.ts +2 -0
  51. package/ts_web/router.ts +1 -1
@@ -2377,6 +2377,129 @@ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
2377
2377
  }
2378
2378
  });
2379
2379
 
2380
+ // ============================================================================
2381
+ // Email Domains State
2382
+ // ============================================================================
2383
+
2384
+ export interface IEmailDomainsState {
2385
+ domains: interfaces.data.IEmailDomain[];
2386
+ isLoading: boolean;
2387
+ lastUpdated: number;
2388
+ }
2389
+
2390
+ export const emailDomainsStatePart = await appState.getStatePart<IEmailDomainsState>(
2391
+ 'emailDomains',
2392
+ {
2393
+ domains: [],
2394
+ isLoading: false,
2395
+ lastUpdated: 0,
2396
+ },
2397
+ 'soft',
2398
+ );
2399
+
2400
+ export const fetchEmailDomainsAction = emailDomainsStatePart.createAction(
2401
+ async (statePartArg): Promise<IEmailDomainsState> => {
2402
+ const context = getActionContext();
2403
+ const currentState = statePartArg.getState()!;
2404
+ if (!context.identity) return currentState;
2405
+
2406
+ try {
2407
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2408
+ interfaces.requests.IReq_GetEmailDomains
2409
+ >('/typedrequest', 'getEmailDomains');
2410
+ const response = await request.fire({ identity: context.identity });
2411
+ return {
2412
+ ...currentState,
2413
+ domains: response.domains,
2414
+ isLoading: false,
2415
+ lastUpdated: Date.now(),
2416
+ };
2417
+ } catch {
2418
+ return { ...currentState, isLoading: false };
2419
+ }
2420
+ },
2421
+ );
2422
+
2423
+ export const createEmailDomainAction = emailDomainsStatePart.createAction<{
2424
+ linkedDomainId: string;
2425
+ dkimSelector?: string;
2426
+ dkimKeySize?: number;
2427
+ rotateKeys?: boolean;
2428
+ rotationIntervalDays?: number;
2429
+ }>(async (statePartArg, args, actionContext) => {
2430
+ const context = getActionContext();
2431
+ const currentState = statePartArg.getState()!;
2432
+ try {
2433
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2434
+ interfaces.requests.IReq_CreateEmailDomain
2435
+ >('/typedrequest', 'createEmailDomain');
2436
+ await request.fire({ identity: context.identity!, ...args });
2437
+ return await actionContext!.dispatch(fetchEmailDomainsAction, null);
2438
+ } catch {
2439
+ return currentState;
2440
+ }
2441
+ });
2442
+
2443
+ export const deleteEmailDomainAction = emailDomainsStatePart.createAction<string>(
2444
+ async (statePartArg, id, actionContext) => {
2445
+ const context = getActionContext();
2446
+ const currentState = statePartArg.getState()!;
2447
+ try {
2448
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2449
+ interfaces.requests.IReq_DeleteEmailDomain
2450
+ >('/typedrequest', 'deleteEmailDomain');
2451
+ await request.fire({ identity: context.identity!, id });
2452
+ return await actionContext!.dispatch(fetchEmailDomainsAction, null);
2453
+ } catch {
2454
+ return currentState;
2455
+ }
2456
+ },
2457
+ );
2458
+
2459
+ export const validateEmailDomainAction = emailDomainsStatePart.createAction<string>(
2460
+ async (statePartArg, id, actionContext) => {
2461
+ const context = getActionContext();
2462
+ const currentState = statePartArg.getState()!;
2463
+ try {
2464
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2465
+ interfaces.requests.IReq_ValidateEmailDomain
2466
+ >('/typedrequest', 'validateEmailDomain');
2467
+ await request.fire({ identity: context.identity!, id });
2468
+ return await actionContext!.dispatch(fetchEmailDomainsAction, null);
2469
+ } catch {
2470
+ return currentState;
2471
+ }
2472
+ },
2473
+ );
2474
+
2475
+ export const provisionEmailDomainDnsAction = emailDomainsStatePart.createAction<string>(
2476
+ async (statePartArg, id, actionContext) => {
2477
+ const context = getActionContext();
2478
+ const currentState = statePartArg.getState()!;
2479
+ try {
2480
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2481
+ interfaces.requests.IReq_ProvisionEmailDomainDns
2482
+ >('/typedrequest', 'provisionEmailDomainDns');
2483
+ await request.fire({ identity: context.identity!, id });
2484
+ return await actionContext!.dispatch(fetchEmailDomainsAction, null);
2485
+ } catch {
2486
+ return currentState;
2487
+ }
2488
+ },
2489
+ );
2490
+
2491
+ // ============================================================================
2492
+ // Email Domain Standalone Functions
2493
+ // ============================================================================
2494
+
2495
+ export async function fetchEmailDomainDnsRecords(id: string) {
2496
+ const context = getActionContext();
2497
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2498
+ interfaces.requests.IReq_GetEmailDomainDnsRecords
2499
+ >('/typedrequest', 'getEmailDomainDnsRecords');
2500
+ return request.fire({ identity: context.identity!, id });
2501
+ }
2502
+
2380
2503
  // ============================================================================
2381
2504
  // TypedSocket Client for Real-time Log Streaming
2382
2505
  // ============================================================================
@@ -1,2 +1,3 @@
1
1
  export * from './ops-view-emails.js';
2
2
  export * from './ops-view-email-security.js';
3
+ export * from './ops-view-email-domains.js';
@@ -0,0 +1,389 @@
1
+ import {
2
+ DeesElement,
3
+ html,
4
+ customElement,
5
+ type TemplateResult,
6
+ css,
7
+ state,
8
+ cssManager,
9
+ } from '@design.estate/dees-element';
10
+ import * as appstate from '../../appstate.js';
11
+ import * as interfaces from '../../../dist_ts_interfaces/index.js';
12
+ import { viewHostCss } from '../shared/css.js';
13
+ import { type IStatsTile } from '@design.estate/dees-catalog';
14
+
15
+ declare global {
16
+ interface HTMLElementTagNameMap {
17
+ 'ops-view-email-domains': OpsViewEmailDomains;
18
+ }
19
+ }
20
+
21
+ @customElement('ops-view-email-domains')
22
+ export class OpsViewEmailDomains extends DeesElement {
23
+ @state()
24
+ accessor emailDomainsState: appstate.IEmailDomainsState =
25
+ appstate.emailDomainsStatePart.getState()!;
26
+
27
+ @state()
28
+ accessor domainsState: appstate.IDomainsState = appstate.domainsStatePart.getState()!;
29
+
30
+ constructor() {
31
+ super();
32
+ const sub = appstate.emailDomainsStatePart.select().subscribe((s) => {
33
+ this.emailDomainsState = s;
34
+ });
35
+ this.rxSubscriptions.push(sub);
36
+ const domSub = appstate.domainsStatePart.select().subscribe((s) => {
37
+ this.domainsState = s;
38
+ });
39
+ this.rxSubscriptions.push(domSub);
40
+ }
41
+
42
+ async connectedCallback() {
43
+ await super.connectedCallback();
44
+ await appstate.emailDomainsStatePart.dispatchAction(appstate.fetchEmailDomainsAction, null);
45
+ await appstate.domainsStatePart.dispatchAction(appstate.fetchDomainsAndProvidersAction, null);
46
+ }
47
+
48
+ public static styles = [
49
+ cssManager.defaultStyles,
50
+ viewHostCss,
51
+ css`
52
+ .emailDomainsContainer {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 24px;
56
+ }
57
+
58
+ .statusBadge {
59
+ display: inline-flex;
60
+ align-items: center;
61
+ padding: 3px 10px;
62
+ border-radius: 12px;
63
+ font-size: 12px;
64
+ font-weight: 600;
65
+ letter-spacing: 0.02em;
66
+ text-transform: uppercase;
67
+ }
68
+
69
+ .statusBadge.valid {
70
+ background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
71
+ color: ${cssManager.bdTheme('#166534', '#4ade80')};
72
+ }
73
+
74
+ .statusBadge.missing {
75
+ background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
76
+ color: ${cssManager.bdTheme('#991b1b', '#f87171')};
77
+ }
78
+
79
+ .statusBadge.invalid {
80
+ background: ${cssManager.bdTheme('#fff7ed', '#431407')};
81
+ color: ${cssManager.bdTheme('#9a3412', '#fb923c')};
82
+ }
83
+
84
+ .statusBadge.unchecked {
85
+ background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
86
+ color: ${cssManager.bdTheme('#4b5563', '#9ca3af')};
87
+ }
88
+
89
+ .sourceBadge {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ padding: 3px 8px;
93
+ border-radius: 4px;
94
+ font-size: 11px;
95
+ font-weight: 500;
96
+ background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
97
+ color: ${cssManager.bdTheme('#374151', '#d1d5db')};
98
+ }
99
+ `,
100
+ ];
101
+
102
+ public render(): TemplateResult {
103
+ const domains = this.emailDomainsState.domains;
104
+ const validCount = domains.filter(
105
+ (d) =>
106
+ d.dnsStatus.mx === 'valid' &&
107
+ d.dnsStatus.spf === 'valid' &&
108
+ d.dnsStatus.dkim === 'valid' &&
109
+ d.dnsStatus.dmarc === 'valid',
110
+ ).length;
111
+ const issueCount = domains.length - validCount;
112
+
113
+ const tiles: IStatsTile[] = [
114
+ {
115
+ id: 'total',
116
+ title: 'Total Domains',
117
+ value: domains.length,
118
+ type: 'number',
119
+ icon: 'lucide:globe',
120
+ color: '#3b82f6',
121
+ },
122
+ {
123
+ id: 'valid',
124
+ title: 'Valid DNS',
125
+ value: validCount,
126
+ type: 'number',
127
+ icon: 'lucide:Check',
128
+ color: '#22c55e',
129
+ },
130
+ {
131
+ id: 'issues',
132
+ title: 'Issues',
133
+ value: issueCount,
134
+ type: 'number',
135
+ icon: 'lucide:TriangleAlert',
136
+ color: issueCount > 0 ? '#ef4444' : '#22c55e',
137
+ },
138
+ {
139
+ id: 'dkim',
140
+ title: 'DKIM Active',
141
+ value: domains.filter((d) => d.dkim.publicKey).length,
142
+ type: 'number',
143
+ icon: 'lucide:KeyRound',
144
+ color: '#8b5cf6',
145
+ },
146
+ ];
147
+
148
+ return html`
149
+ <dees-heading level="3">Email Domains</dees-heading>
150
+
151
+ <div class="emailDomainsContainer">
152
+ <dees-statsgrid
153
+ .tiles=${tiles}
154
+ .minTileWidth=${200}
155
+ .gridActions=${[
156
+ {
157
+ name: 'Refresh',
158
+ iconName: 'lucide:RefreshCw',
159
+ action: async () => {
160
+ await appstate.emailDomainsStatePart.dispatchAction(
161
+ appstate.fetchEmailDomainsAction,
162
+ null,
163
+ );
164
+ },
165
+ },
166
+ ]}
167
+ ></dees-statsgrid>
168
+
169
+ <dees-table
170
+ .heading1=${'Email Domains'}
171
+ .heading2=${'DKIM, SPF, DMARC and MX management'}
172
+ .data=${domains}
173
+ .showColumnFilters=${true}
174
+ .displayFunction=${(d: interfaces.data.IEmailDomain) => ({
175
+ Domain: d.domain,
176
+ Source: this.renderSourceBadge(d.linkedDomainId),
177
+ MX: this.renderDnsStatus(d.dnsStatus.mx),
178
+ SPF: this.renderDnsStatus(d.dnsStatus.spf),
179
+ DKIM: this.renderDnsStatus(d.dnsStatus.dkim),
180
+ DMARC: this.renderDnsStatus(d.dnsStatus.dmarc),
181
+ })}
182
+ .dataActions=${[
183
+ {
184
+ name: 'Add Email Domain',
185
+ iconName: 'lucide:plus',
186
+ type: ['header'] as any,
187
+ actionFunc: async () => {
188
+ await this.showCreateDialog();
189
+ },
190
+ },
191
+ {
192
+ name: 'Validate DNS',
193
+ iconName: 'lucide:search-check',
194
+ type: ['inRow', 'contextmenu'] as any,
195
+ actionFunc: async (actionData: any) => {
196
+ const d = actionData.item as interfaces.data.IEmailDomain;
197
+ await appstate.emailDomainsStatePart.dispatchAction(
198
+ appstate.validateEmailDomainAction,
199
+ d.id,
200
+ );
201
+ const { DeesToast } = await import('@design.estate/dees-catalog');
202
+ DeesToast.show({ message: `DNS validated for ${d.domain}`, type: 'success', duration: 2500 });
203
+ },
204
+ },
205
+ {
206
+ name: 'Provision DNS',
207
+ iconName: 'lucide:wand-sparkles',
208
+ type: ['inRow', 'contextmenu'] as any,
209
+ actionFunc: async (actionData: any) => {
210
+ const d = actionData.item as interfaces.data.IEmailDomain;
211
+ await appstate.emailDomainsStatePart.dispatchAction(
212
+ appstate.provisionEmailDomainDnsAction,
213
+ d.id,
214
+ );
215
+ const { DeesToast } = await import('@design.estate/dees-catalog');
216
+ DeesToast.show({ message: `DNS records provisioned for ${d.domain}`, type: 'success', duration: 2500 });
217
+ },
218
+ },
219
+ {
220
+ name: 'View DNS Records',
221
+ iconName: 'lucide:list',
222
+ type: ['inRow', 'contextmenu'] as any,
223
+ actionFunc: async (actionData: any) => {
224
+ const d = actionData.item as interfaces.data.IEmailDomain;
225
+ await this.showDnsRecordsDialog(d);
226
+ },
227
+ },
228
+ {
229
+ name: 'Delete',
230
+ iconName: 'lucide:trash2',
231
+ type: ['inRow', 'contextmenu'] as any,
232
+ actionFunc: async (actionData: any) => {
233
+ const d = actionData.item as interfaces.data.IEmailDomain;
234
+ await appstate.emailDomainsStatePart.dispatchAction(
235
+ appstate.deleteEmailDomainAction,
236
+ d.id,
237
+ );
238
+ },
239
+ },
240
+ ]}
241
+ dataName="email domain"
242
+ ></dees-table>
243
+ </div>
244
+ `;
245
+ }
246
+
247
+ private renderDnsStatus(status: interfaces.data.TDnsRecordStatus): TemplateResult {
248
+ return html`<span class="statusBadge ${status}">${status}</span>`;
249
+ }
250
+
251
+ private renderSourceBadge(linkedDomainId: string): TemplateResult {
252
+ const domain = this.domainsState.domains.find((d) => d.id === linkedDomainId);
253
+ if (!domain) return html`<span class="sourceBadge">unknown</span>`;
254
+ const label =
255
+ domain.source === 'dcrouter'
256
+ ? 'dcrouter'
257
+ : this.domainsState.providers.find((p) => p.id === domain.providerId)?.name || 'provider';
258
+ return html`<span class="sourceBadge">${label}</span>`;
259
+ }
260
+
261
+ private async showCreateDialog() {
262
+ const { DeesModal } = await import('@design.estate/dees-catalog');
263
+ const domainOptions = this.domainsState.domains.map((d) => ({
264
+ option: `${d.name} (${d.source})`,
265
+ key: d.id,
266
+ }));
267
+
268
+ DeesModal.createAndShow({
269
+ heading: 'Add Email Domain',
270
+ content: html`
271
+ <dees-form>
272
+ <dees-input-dropdown
273
+ .key=${'linkedDomainId'}
274
+ .label=${'Domain'}
275
+ .description=${'Select an existing DNS domain'}
276
+ .options=${domainOptions}
277
+ .required=${true}
278
+ ></dees-input-dropdown>
279
+ <dees-input-text
280
+ .key=${'dkimSelector'}
281
+ .label=${'DKIM Selector'}
282
+ .description=${'Identifier used in DNS record name'}
283
+ .value=${'default'}
284
+ ></dees-input-text>
285
+ <dees-input-dropdown
286
+ .key=${'dkimKeySize'}
287
+ .label=${'DKIM Key Size'}
288
+ .options=${[
289
+ { option: '2048 (recommended)', key: '2048' },
290
+ { option: '1024', key: '1024' },
291
+ { option: '4096', key: '4096' },
292
+ ]}
293
+ .selectedOption=${{ option: '2048 (recommended)', key: '2048' }}
294
+ ></dees-input-dropdown>
295
+ <dees-input-checkbox
296
+ .key=${'rotateKeys'}
297
+ .label=${'Auto-rotate DKIM keys'}
298
+ .value=${false}
299
+ ></dees-input-checkbox>
300
+ </dees-form>
301
+ `,
302
+ menuOptions: [
303
+ { name: 'Cancel', action: async (m: any) => m.destroy() },
304
+ {
305
+ name: 'Create',
306
+ action: async (m: any) => {
307
+ const form = m.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
308
+ if (!form) return;
309
+ const data = await form.collectFormData();
310
+ const linkedDomainId =
311
+ typeof data.linkedDomainId === 'object'
312
+ ? data.linkedDomainId.key
313
+ : data.linkedDomainId;
314
+ const keySize =
315
+ typeof data.dkimKeySize === 'object'
316
+ ? parseInt(data.dkimKeySize.key, 10)
317
+ : parseInt(data.dkimKeySize || '2048', 10);
318
+
319
+ await appstate.emailDomainsStatePart.dispatchAction(
320
+ appstate.createEmailDomainAction,
321
+ {
322
+ linkedDomainId,
323
+ dkimSelector: data.dkimSelector || 'default',
324
+ dkimKeySize: keySize,
325
+ rotateKeys: Boolean(data.rotateKeys),
326
+ },
327
+ );
328
+ m.destroy();
329
+ },
330
+ },
331
+ ],
332
+ });
333
+ }
334
+
335
+ private async showDnsRecordsDialog(emailDomain: interfaces.data.IEmailDomain) {
336
+ const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
337
+
338
+ // Fetch required DNS records
339
+ let records: interfaces.data.IEmailDnsRecord[] = [];
340
+ try {
341
+ const response = await appstate.fetchEmailDomainDnsRecords(emailDomain.id);
342
+ records = response.records;
343
+ } catch {
344
+ records = [];
345
+ }
346
+
347
+ DeesModal.createAndShow({
348
+ heading: `DNS Records: ${emailDomain.domain}`,
349
+ content: html`
350
+ <dees-table
351
+ .data=${records}
352
+ .displayFunction=${(r: interfaces.data.IEmailDnsRecord) => ({
353
+ Type: r.type,
354
+ Name: r.name,
355
+ Value: r.value,
356
+ Status: html`<span class="statusBadge ${r.status}">${r.status}</span>`,
357
+ })}
358
+ .dataActions=${[
359
+ {
360
+ name: 'Copy Value',
361
+ iconName: 'lucide:copy',
362
+ type: ['inRow'] as any,
363
+ actionFunc: async (actionData: any) => {
364
+ const rec = actionData.item as interfaces.data.IEmailDnsRecord;
365
+ await navigator.clipboard.writeText(rec.value);
366
+ DeesToast.show({ message: 'Copied to clipboard', type: 'success', duration: 1500 });
367
+ },
368
+ },
369
+ ]}
370
+ dataName="DNS record"
371
+ ></dees-table>
372
+ `,
373
+ menuOptions: [
374
+ {
375
+ name: 'Auto-Provision All',
376
+ action: async (m: any) => {
377
+ await appstate.emailDomainsStatePart.dispatchAction(
378
+ appstate.provisionEmailDomainDnsAction,
379
+ emailDomain.id,
380
+ );
381
+ DeesToast.show({ message: 'DNS records provisioned', type: 'success', duration: 2500 });
382
+ m.destroy();
383
+ },
384
+ },
385
+ { name: 'Close', action: async (m: any) => m.destroy() },
386
+ ],
387
+ });
388
+ }
389
+ }
@@ -32,6 +32,7 @@ import { OpsViewVpn } from './network/ops-view-vpn.js';
32
32
  // Email group
33
33
  import { OpsViewEmails } from './email/ops-view-emails.js';
34
34
  import { OpsViewEmailSecurity } from './email/ops-view-email-security.js';
35
+ import { OpsViewEmailDomains } from './email/ops-view-email-domains.js';
35
36
 
36
37
  // Access group
37
38
  import { OpsViewApiTokens } from './access/ops-view-apitokens.js';
@@ -108,6 +109,7 @@ export class OpsDashboard extends DeesElement {
108
109
  subViews: [
109
110
  { slug: 'log', name: 'Email Log', iconName: 'lucide:scrollText', element: OpsViewEmails },
110
111
  { slug: 'security', name: 'Email Security', iconName: 'lucide:shieldCheck', element: OpsViewEmailSecurity },
112
+ { slug: 'domains', name: 'Email Domains', iconName: 'lucide:globe', element: OpsViewEmailDomains },
111
113
  ],
112
114
  },
113
115
  {
package/ts_web/router.ts CHANGED
@@ -10,7 +10,7 @@ const flatViews = ['logs'] as const;
10
10
  const subviewMap: Record<string, readonly string[]> = {
11
11
  overview: ['stats', 'configuration'] as const,
12
12
  network: ['activity', 'routes', 'sourceprofiles', 'networktargets', 'targetprofiles', 'remoteingress', 'vpn'] as const,
13
- email: ['log', 'security'] as const,
13
+ email: ['log', 'security', 'domains'] as const,
14
14
  access: ['apitokens', 'users'] as const,
15
15
  security: ['overview', 'blocked', 'authentication'] as const,
16
16
  domains: ['providers', 'domains', 'dns', 'certificates'] as const,