@serve.zone/dcrouter 13.9.2 → 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 (56) hide show
  1. package/dist_serve/bundle.js +1306 -1180
  2. package/dist_ts/00_commitinfo_data.js +2 -2
  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 +2 -2
  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/domains/ops-view-certificates.js +17 -96
  31. package/dist_ts_web/elements/email/index.d.ts +1 -0
  32. package/dist_ts_web/elements/email/index.js +2 -1
  33. package/dist_ts_web/elements/email/ops-view-email-domains.d.ts +19 -0
  34. package/dist_ts_web/elements/email/ops-view-email-domains.js +403 -0
  35. package/dist_ts_web/elements/email/ops-view-email-security.d.ts +1 -1
  36. package/dist_ts_web/elements/email/ops-view-email-security.js +39 -58
  37. package/dist_ts_web/elements/ops-dashboard.js +3 -1
  38. package/dist_ts_web/router.js +2 -2
  39. package/package.json +2 -2
  40. package/ts/00_commitinfo_data.ts +1 -1
  41. package/ts/classes.dcrouter.ts +17 -0
  42. package/ts/db/documents/classes.email-domain.doc.ts +53 -0
  43. package/ts/db/documents/index.ts +3 -0
  44. package/ts/email/classes.email-domain.manager.ts +316 -0
  45. package/ts/email/index.ts +1 -0
  46. package/ts/opsserver/classes.opsserver.ts +2 -0
  47. package/ts/opsserver/handlers/email-domain.handler.ts +194 -0
  48. package/ts/opsserver/handlers/index.ts +2 -1
  49. package/ts_web/00_commitinfo_data.ts +1 -1
  50. package/ts_web/appstate.ts +123 -0
  51. package/ts_web/elements/domains/ops-view-certificates.ts +16 -95
  52. package/ts_web/elements/email/index.ts +1 -0
  53. package/ts_web/elements/email/ops-view-email-domains.ts +389 -0
  54. package/ts_web/elements/email/ops-view-email-security.ts +38 -57
  55. package/ts_web/elements/ops-dashboard.ts +2 -0
  56. package/ts_web/router.ts +1 -1
@@ -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
+ }
@@ -37,25 +37,10 @@ export class OpsViewEmailSecurity extends DeesElement {
37
37
  cssManager.defaultStyles,
38
38
  viewHostCss,
39
39
  css`
40
- h2 {
41
- margin: 32px 0 16px 0;
42
- font-size: 24px;
43
- font-weight: 600;
44
- color: ${cssManager.bdTheme('#333', '#ccc')};
45
- }
46
- dees-statsgrid {
47
- margin-bottom: 32px;
48
- }
49
- .securityCard {
50
- background: ${cssManager.bdTheme('#fff', '#222')};
51
- border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
52
- border-radius: 8px;
53
- padding: 24px;
54
- position: relative;
55
- overflow: hidden;
56
- }
57
- .actionButton {
58
- margin-top: 16px;
40
+ .securityContainer {
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: 24px;
59
44
  }
60
45
  `,
61
46
  ];
@@ -113,48 +98,44 @@ export class OpsViewEmailSecurity extends DeesElement {
113
98
  return html`
114
99
  <dees-heading level="3">Email Security</dees-heading>
115
100
 
116
- <dees-statsgrid
117
- .tiles=${tiles}
118
- .minTileWidth=${200}
119
- ></dees-statsgrid>
101
+ <div class="securityContainer">
102
+ <dees-statsgrid
103
+ .tiles=${tiles}
104
+ .minTileWidth=${200}
105
+ ></dees-statsgrid>
120
106
 
121
- <h2>Email Security Configuration</h2>
122
- <div class="securityCard">
123
- <dees-form>
124
- <dees-input-checkbox
125
- .key=${'enableSPF'}
126
- .label=${'Enable SPF checking'}
127
- .value=${true}
128
- ></dees-input-checkbox>
129
- <dees-input-checkbox
130
- .key=${'enableDKIM'}
131
- .label=${'Enable DKIM validation'}
132
- .value=${true}
133
- ></dees-input-checkbox>
134
- <dees-input-checkbox
135
- .key=${'enableDMARC'}
136
- .label=${'Enable DMARC policy enforcement'}
137
- .value=${true}
138
- ></dees-input-checkbox>
139
- <dees-input-checkbox
140
- .key=${'enableSpamFilter'}
141
- .label=${'Enable spam filtering'}
142
- .value=${true}
143
- ></dees-input-checkbox>
144
- </dees-form>
145
- <dees-button
146
- class="actionButton"
147
- type="highlighted"
148
- @click=${() => this.saveEmailSecuritySettings()}
149
- >
150
- Save Settings
151
- </dees-button>
107
+ <dees-settings
108
+ .heading=${'Security Configuration'}
109
+ .settingsFields=${[
110
+ { key: 'spf', label: 'SPF checking', value: 'enabled' },
111
+ { key: 'dkim', label: 'DKIM validation', value: 'enabled' },
112
+ { key: 'dmarc', label: 'DMARC policy', value: 'enabled' },
113
+ { key: 'spam', label: 'Spam filtering', value: 'enabled' },
114
+ ]}
115
+ .actions=${[{ name: 'Edit', action: () => this.showEditSecurityDialog() }]}
116
+ ></dees-settings>
152
117
  </div>
153
118
  `;
154
119
  }
155
120
 
156
- private async saveEmailSecuritySettings() {
157
- // Config is read-only from the UI for now
158
- alert('Email security settings are read-only. Update the dcrouter configuration file to change these settings.');
121
+ private async showEditSecurityDialog() {
122
+ const { DeesModal } = await import('@design.estate/dees-catalog');
123
+ DeesModal.createAndShow({
124
+ heading: 'Edit Security Configuration',
125
+ content: html`
126
+ <dees-form>
127
+ <dees-input-checkbox .key=${'enableSPF'} .label=${'SPF checking'} .value=${true}></dees-input-checkbox>
128
+ <dees-input-checkbox .key=${'enableDKIM'} .label=${'DKIM validation'} .value=${true}></dees-input-checkbox>
129
+ <dees-input-checkbox .key=${'enableDMARC'} .label=${'DMARC policy enforcement'} .value=${true}></dees-input-checkbox>
130
+ <dees-input-checkbox .key=${'enableSpamFilter'} .label=${'Spam filtering'} .value=${true}></dees-input-checkbox>
131
+ </dees-form>
132
+ <p style="margin-top: 12px; font-size: 12px; opacity: 0.7;">
133
+ These settings are read-only for now. Update the dcrouter configuration to change them.
134
+ </p>
135
+ `,
136
+ menuOptions: [
137
+ { name: 'Close', action: async (modalArg: any) => modalArg.destroy() },
138
+ ],
139
+ });
159
140
  }
160
141
  }
@@ -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,