@serve.zone/dcrouter 13.5.0 → 13.7.1

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 (136) hide show
  1. package/dist_serve/bundle.js +1705 -1365
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +2 -5
  4. package/dist_ts/classes.dcrouter.js +41 -10
  5. package/dist_ts/db/documents/classes.dns-provider.doc.d.ts +22 -0
  6. package/dist_ts/db/documents/classes.dns-provider.doc.js +134 -0
  7. package/dist_ts/db/documents/classes.dns-record.doc.d.ts +21 -0
  8. package/dist_ts/db/documents/classes.dns-record.doc.js +143 -0
  9. package/dist_ts/db/documents/classes.domain.doc.d.ts +22 -0
  10. package/dist_ts/db/documents/classes.domain.doc.js +146 -0
  11. package/dist_ts/db/documents/index.d.ts +3 -0
  12. package/dist_ts/db/documents/index.js +5 -1
  13. package/dist_ts/dns/index.d.ts +2 -0
  14. package/dist_ts/dns/index.js +3 -0
  15. package/dist_ts/dns/manager.dns.d.ts +227 -0
  16. package/dist_ts/dns/manager.dns.js +747 -0
  17. package/dist_ts/dns/providers/cloudflare.provider.d.ts +21 -0
  18. package/dist_ts/dns/providers/cloudflare.provider.js +106 -0
  19. package/dist_ts/dns/providers/factory.d.ts +23 -0
  20. package/dist_ts/dns/providers/factory.js +38 -0
  21. package/dist_ts/dns/providers/index.d.ts +3 -0
  22. package/dist_ts/dns/providers/index.js +4 -0
  23. package/dist_ts/dns/providers/interfaces.d.ts +54 -0
  24. package/dist_ts/dns/providers/interfaces.js +2 -0
  25. package/dist_ts/opsserver/classes.opsserver.d.ts +3 -0
  26. package/dist_ts/opsserver/classes.opsserver.js +7 -1
  27. package/dist_ts/opsserver/handlers/config.handler.js +11 -2
  28. package/dist_ts/opsserver/handlers/dns-provider.handler.d.ts +16 -0
  29. package/dist_ts/opsserver/handlers/dns-provider.handler.js +119 -0
  30. package/dist_ts/opsserver/handlers/dns-record.handler.d.ts +13 -0
  31. package/dist_ts/opsserver/handlers/dns-record.handler.js +98 -0
  32. package/dist_ts/opsserver/handlers/domain.handler.d.ts +13 -0
  33. package/dist_ts/opsserver/handlers/domain.handler.js +124 -0
  34. package/dist_ts/opsserver/handlers/index.d.ts +3 -0
  35. package/dist_ts/opsserver/handlers/index.js +4 -1
  36. package/dist_ts_interfaces/data/dns-provider.d.ts +112 -0
  37. package/dist_ts_interfaces/data/dns-provider.js +27 -0
  38. package/dist_ts_interfaces/data/dns-record.d.ts +40 -0
  39. package/dist_ts_interfaces/data/dns-record.js +2 -0
  40. package/dist_ts_interfaces/data/domain.d.ts +34 -0
  41. package/dist_ts_interfaces/data/domain.js +2 -0
  42. package/dist_ts_interfaces/data/index.d.ts +3 -0
  43. package/dist_ts_interfaces/data/index.js +4 -1
  44. package/dist_ts_interfaces/data/route-management.d.ts +1 -1
  45. package/dist_ts_interfaces/requests/dns-providers.d.ts +117 -0
  46. package/dist_ts_interfaces/requests/dns-providers.js +2 -0
  47. package/dist_ts_interfaces/requests/dns-records.d.ts +89 -0
  48. package/dist_ts_interfaces/requests/dns-records.js +2 -0
  49. package/dist_ts_interfaces/requests/domains.d.ts +118 -0
  50. package/dist_ts_interfaces/requests/domains.js +2 -0
  51. package/dist_ts_interfaces/requests/index.d.ts +3 -0
  52. package/dist_ts_interfaces/requests/index.js +4 -1
  53. package/dist_ts_web/00_commitinfo_data.js +1 -1
  54. package/dist_ts_web/appstate.d.ts +72 -0
  55. package/dist_ts_web/appstate.js +308 -6
  56. package/dist_ts_web/elements/access/ops-view-apitokens.js +1 -1
  57. package/dist_ts_web/elements/access/ops-view-users.js +1 -1
  58. package/dist_ts_web/elements/domains/dns-provider-form.d.ts +58 -0
  59. package/dist_ts_web/elements/domains/dns-provider-form.js +268 -0
  60. package/dist_ts_web/elements/domains/index.d.ts +5 -0
  61. package/dist_ts_web/elements/domains/index.js +6 -0
  62. package/dist_ts_web/elements/{ops-view-certificates.d.ts → domains/ops-view-certificates.d.ts} +1 -1
  63. package/dist_ts_web/elements/{ops-view-certificates.js → domains/ops-view-certificates.js} +5 -5
  64. package/dist_ts_web/elements/domains/ops-view-dns.d.ts +17 -0
  65. package/dist_ts_web/elements/domains/ops-view-dns.js +304 -0
  66. package/dist_ts_web/elements/domains/ops-view-domains.d.ts +18 -0
  67. package/dist_ts_web/elements/domains/ops-view-domains.js +361 -0
  68. package/dist_ts_web/elements/domains/ops-view-providers.d.ts +21 -0
  69. package/dist_ts_web/elements/domains/ops-view-providers.js +316 -0
  70. package/dist_ts_web/elements/email/ops-view-email-security.js +1 -1
  71. package/dist_ts_web/elements/email/ops-view-emails.js +1 -1
  72. package/dist_ts_web/elements/index.d.ts +1 -1
  73. package/dist_ts_web/elements/index.js +2 -2
  74. package/dist_ts_web/elements/network/ops-view-network-activity.js +1 -1
  75. package/dist_ts_web/elements/network/ops-view-networktargets.js +1 -1
  76. package/dist_ts_web/elements/network/ops-view-remoteingress.js +1 -1
  77. package/dist_ts_web/elements/network/ops-view-routes.js +1 -1
  78. package/dist_ts_web/elements/network/ops-view-sourceprofiles.js +1 -1
  79. package/dist_ts_web/elements/network/ops-view-targetprofiles.js +1 -1
  80. package/dist_ts_web/elements/network/ops-view-vpn.js +1 -1
  81. package/dist_ts_web/elements/ops-dashboard.js +14 -5
  82. package/dist_ts_web/elements/ops-view-logs.js +1 -1
  83. package/dist_ts_web/elements/overview/ops-view-config.js +3 -3
  84. package/dist_ts_web/elements/overview/ops-view-overview.js +1 -1
  85. package/dist_ts_web/elements/security/ops-view-security-authentication.js +1 -1
  86. package/dist_ts_web/elements/security/ops-view-security-blocked.js +1 -1
  87. package/dist_ts_web/elements/security/ops-view-security-overview.js +1 -1
  88. package/dist_ts_web/router.d.ts +1 -1
  89. package/dist_ts_web/router.js +4 -2
  90. package/package.json +2 -2
  91. package/ts/00_commitinfo_data.ts +1 -1
  92. package/ts/classes.dcrouter.ts +46 -17
  93. package/ts/db/documents/classes.dns-provider.doc.ts +63 -0
  94. package/ts/db/documents/classes.dns-record.doc.ts +62 -0
  95. package/ts/db/documents/classes.domain.doc.ts +66 -0
  96. package/ts/db/documents/index.ts +5 -0
  97. package/ts/dns/index.ts +2 -0
  98. package/ts/dns/manager.dns.ts +869 -0
  99. package/ts/dns/providers/cloudflare.provider.ts +131 -0
  100. package/ts/dns/providers/factory.ts +48 -0
  101. package/ts/dns/providers/index.ts +3 -0
  102. package/ts/dns/providers/interfaces.ts +67 -0
  103. package/ts/opsserver/classes.opsserver.ts +6 -0
  104. package/ts/opsserver/handlers/config.handler.ts +10 -1
  105. package/ts/opsserver/handlers/dns-provider.handler.ts +159 -0
  106. package/ts/opsserver/handlers/dns-record.handler.ts +127 -0
  107. package/ts/opsserver/handlers/domain.handler.ts +161 -0
  108. package/ts/opsserver/handlers/index.ts +4 -1
  109. package/ts_web/00_commitinfo_data.ts +1 -1
  110. package/ts_web/appstate.ts +403 -5
  111. package/ts_web/elements/access/ops-view-apitokens.ts +1 -1
  112. package/ts_web/elements/access/ops-view-users.ts +1 -1
  113. package/ts_web/elements/domains/dns-provider-form.ts +216 -0
  114. package/ts_web/elements/domains/index.ts +5 -0
  115. package/ts_web/elements/{ops-view-certificates.ts → domains/ops-view-certificates.ts} +4 -4
  116. package/ts_web/elements/domains/ops-view-dns.ts +273 -0
  117. package/ts_web/elements/domains/ops-view-domains.ts +335 -0
  118. package/ts_web/elements/domains/ops-view-providers.ts +284 -0
  119. package/ts_web/elements/email/ops-view-email-security.ts +1 -1
  120. package/ts_web/elements/email/ops-view-emails.ts +1 -1
  121. package/ts_web/elements/index.ts +1 -1
  122. package/ts_web/elements/network/ops-view-network-activity.ts +1 -1
  123. package/ts_web/elements/network/ops-view-networktargets.ts +1 -1
  124. package/ts_web/elements/network/ops-view-remoteingress.ts +1 -1
  125. package/ts_web/elements/network/ops-view-routes.ts +1 -1
  126. package/ts_web/elements/network/ops-view-sourceprofiles.ts +1 -1
  127. package/ts_web/elements/network/ops-view-targetprofiles.ts +1 -1
  128. package/ts_web/elements/network/ops-view-vpn.ts +1 -1
  129. package/ts_web/elements/ops-dashboard.ts +14 -4
  130. package/ts_web/elements/ops-view-logs.ts +1 -1
  131. package/ts_web/elements/overview/ops-view-config.ts +2 -2
  132. package/ts_web/elements/overview/ops-view-overview.ts +1 -1
  133. package/ts_web/elements/security/ops-view-security-authentication.ts +1 -1
  134. package/ts_web/elements/security/ops-view-security-blocked.ts +1 -1
  135. package/ts_web/elements/security/ops-view-security-overview.ts +1 -1
  136. package/ts_web/router.ts +3 -1
@@ -0,0 +1,335 @@
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 { appRouter } from '../../router.js';
14
+
15
+ declare global {
16
+ interface HTMLElementTagNameMap {
17
+ 'ops-view-domains': OpsViewDomains;
18
+ }
19
+ }
20
+
21
+ @customElement('ops-view-domains')
22
+ export class OpsViewDomains extends DeesElement {
23
+ @state()
24
+ accessor domainsState: appstate.IDomainsState = appstate.domainsStatePart.getState()!;
25
+
26
+ constructor() {
27
+ super();
28
+ const sub = appstate.domainsStatePart.select().subscribe((newState) => {
29
+ this.domainsState = newState;
30
+ });
31
+ this.rxSubscriptions.push(sub);
32
+ }
33
+
34
+ async connectedCallback() {
35
+ await super.connectedCallback();
36
+ await appstate.domainsStatePart.dispatchAction(appstate.fetchDomainsAndProvidersAction, null);
37
+ }
38
+
39
+ public static styles = [
40
+ cssManager.defaultStyles,
41
+ viewHostCss,
42
+ css`
43
+ .domainsContainer {
44
+ display: flex;
45
+ flex-direction: column;
46
+ gap: 24px;
47
+ }
48
+
49
+ .sourceBadge {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ padding: 3px 8px;
53
+ border-radius: 4px;
54
+ font-size: 11px;
55
+ font-weight: 500;
56
+ }
57
+
58
+ .sourceBadge.manual {
59
+ background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
60
+ color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
61
+ }
62
+
63
+ .sourceBadge.provider {
64
+ background: ${cssManager.bdTheme('#fef3c7', '#451a03')};
65
+ color: ${cssManager.bdTheme('#92400e', '#fde047')};
66
+ }
67
+ `,
68
+ ];
69
+
70
+ public render(): TemplateResult {
71
+ const domains = this.domainsState.domains;
72
+ const providersById = new Map(this.domainsState.providers.map((p) => [p.id, p]));
73
+
74
+ return html`
75
+ <dees-heading level="3">Domains</dees-heading>
76
+ <div class="domainsContainer">
77
+ <dees-table
78
+ .heading1=${'Domains'}
79
+ .heading2=${'Domains under management — manual (authoritative) or imported from a provider'}
80
+ .data=${domains}
81
+ .showColumnFilters=${true}
82
+ .displayFunction=${(d: interfaces.data.IDomain) => ({
83
+ Name: d.name,
84
+ Source: this.renderSourceBadge(d, providersById),
85
+ Authoritative: d.authoritative ? 'yes' : 'no',
86
+ Nameservers: d.nameservers?.join(', ') || '-',
87
+ 'Last Synced': d.lastSyncedAt
88
+ ? new Date(d.lastSyncedAt).toLocaleString()
89
+ : '-',
90
+ })}
91
+ .dataActions=${[
92
+ {
93
+ name: 'Add Manual Domain',
94
+ iconName: 'lucide:plus',
95
+ type: ['header' as const],
96
+ actionFunc: async () => {
97
+ await this.showCreateManualDialog();
98
+ },
99
+ },
100
+ {
101
+ name: 'Import from Provider',
102
+ iconName: 'lucide:download',
103
+ type: ['header' as const],
104
+ actionFunc: async () => {
105
+ await this.showImportDialog();
106
+ },
107
+ },
108
+ {
109
+ name: 'Refresh',
110
+ iconName: 'lucide:rotateCw',
111
+ type: ['header' as const],
112
+ actionFunc: async () => {
113
+ await appstate.domainsStatePart.dispatchAction(
114
+ appstate.fetchDomainsAndProvidersAction,
115
+ null,
116
+ );
117
+ },
118
+ },
119
+ {
120
+ name: 'View Records',
121
+ iconName: 'lucide:list',
122
+ type: ['inRow', 'contextmenu'] as any,
123
+ actionFunc: async (actionData: any) => {
124
+ const domain = actionData.item as interfaces.data.IDomain;
125
+ await appstate.domainsStatePart.dispatchAction(
126
+ appstate.fetchDnsRecordsForDomainAction,
127
+ { domainId: domain.id },
128
+ );
129
+ appRouter.navigateToView('domains', 'dns');
130
+ },
131
+ },
132
+ {
133
+ name: 'Sync Now',
134
+ iconName: 'lucide:rotateCw',
135
+ type: ['inRow', 'contextmenu'] as any,
136
+ actionFunc: async (actionData: any) => {
137
+ const domain = actionData.item as interfaces.data.IDomain;
138
+ if (domain.source !== 'provider') {
139
+ const { DeesToast } = await import('@design.estate/dees-catalog');
140
+ DeesToast.show({
141
+ message: 'Sync only applies to provider-managed domains',
142
+ type: 'warning',
143
+ duration: 3000,
144
+ });
145
+ return;
146
+ }
147
+ await appstate.domainsStatePart.dispatchAction(appstate.syncDomainAction, {
148
+ id: domain.id,
149
+ });
150
+ },
151
+ },
152
+ {
153
+ name: 'Delete',
154
+ iconName: 'lucide:trash2',
155
+ type: ['inRow', 'contextmenu'] as any,
156
+ actionFunc: async (actionData: any) => {
157
+ const domain = actionData.item as interfaces.data.IDomain;
158
+ await this.deleteDomain(domain);
159
+ },
160
+ },
161
+ ]}
162
+ ></dees-table>
163
+ </div>
164
+ `;
165
+ }
166
+
167
+ private renderSourceBadge(
168
+ d: interfaces.data.IDomain,
169
+ providersById: Map<string, interfaces.data.IDnsProviderPublic>,
170
+ ): TemplateResult {
171
+ if (d.source === 'manual') {
172
+ return html`<span class="sourceBadge manual">Manual</span>`;
173
+ }
174
+ const provider = d.providerId ? providersById.get(d.providerId) : undefined;
175
+ return html`<span class="sourceBadge provider">${provider?.name || 'Provider'}</span>`;
176
+ }
177
+
178
+ private async showCreateManualDialog() {
179
+ const { DeesModal } = await import('@design.estate/dees-catalog');
180
+ DeesModal.createAndShow({
181
+ heading: 'Add Manual Domain',
182
+ content: html`
183
+ <dees-form>
184
+ <dees-input-text .key=${'name'} .label=${'FQDN (e.g. example.com)'} .required=${true}></dees-input-text>
185
+ <dees-input-text .key=${'description'} .label=${'Description (optional)'}></dees-input-text>
186
+ </dees-form>
187
+ <p style="margin-top: 12px; font-size: 12px; opacity: 0.7;">
188
+ dcrouter will become the authoritative DNS server for this domain. You'll need to
189
+ delegate the domain's nameservers to dcrouter to make this effective.
190
+ </p>
191
+ `,
192
+ menuOptions: [
193
+ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
194
+ {
195
+ name: 'Create',
196
+ action: async (modalArg: any) => {
197
+ const form = modalArg.shadowRoot
198
+ ?.querySelector('.content')
199
+ ?.querySelector('dees-form');
200
+ if (!form) return;
201
+ const data = await form.collectFormData();
202
+ await appstate.domainsStatePart.dispatchAction(appstate.createManualDomainAction, {
203
+ name: String(data.name),
204
+ description: data.description ? String(data.description) : undefined,
205
+ });
206
+ modalArg.destroy();
207
+ },
208
+ },
209
+ ],
210
+ });
211
+ }
212
+
213
+ private async showImportDialog() {
214
+ const providers = this.domainsState.providers;
215
+ if (providers.length === 0) {
216
+ const { DeesToast } = await import('@design.estate/dees-catalog');
217
+ DeesToast.show({
218
+ message: 'Add a DNS provider first (Domains > Providers)',
219
+ type: 'warning',
220
+ duration: 3500,
221
+ });
222
+ return;
223
+ }
224
+
225
+ const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
226
+ DeesModal.createAndShow({
227
+ heading: 'Import Domains from Provider',
228
+ content: html`
229
+ <dees-form>
230
+ <dees-input-dropdown
231
+ .key=${'providerId'}
232
+ .label=${'Provider'}
233
+ .options=${providers.map((p) => ({ option: p.name, key: p.id }))}
234
+ .required=${true}
235
+ ></dees-input-dropdown>
236
+ <dees-input-text
237
+ .key=${'domainNames'}
238
+ .label=${'Comma-separated FQDNs to import (e.g. example.com, foo.com)'}
239
+ .required=${true}
240
+ ></dees-input-text>
241
+ </dees-form>
242
+ <p style="margin-top: 12px; font-size: 12px; opacity: 0.7;">
243
+ Tip: use "List Provider Domains" to see what's available before typing.
244
+ </p>
245
+ `,
246
+ menuOptions: [
247
+ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
248
+ {
249
+ name: 'List Provider Domains',
250
+ action: async (_modalArg: any) => {
251
+ const form = _modalArg.shadowRoot
252
+ ?.querySelector('.content')
253
+ ?.querySelector('dees-form');
254
+ if (!form) return;
255
+ const data = await form.collectFormData();
256
+ const providerKey = data.providerId?.key ?? data.providerId;
257
+ if (!providerKey) {
258
+ DeesToast.show({ message: 'Pick a provider first', type: 'warning', duration: 2500 });
259
+ return;
260
+ }
261
+ const result = await appstate.fetchProviderDomains(String(providerKey));
262
+ if (!result.success) {
263
+ DeesToast.show({
264
+ message: result.message || 'Failed to fetch domains',
265
+ type: 'error',
266
+ duration: 4000,
267
+ });
268
+ return;
269
+ }
270
+ const list = (result.domains ?? []).map((d) => d.name).join(', ');
271
+ DeesToast.show({
272
+ message: `Provider has: ${list || '(none)'}`,
273
+ type: 'info',
274
+ duration: 8000,
275
+ });
276
+ },
277
+ },
278
+ {
279
+ name: 'Import',
280
+ action: async (modalArg: any) => {
281
+ const form = modalArg.shadowRoot
282
+ ?.querySelector('.content')
283
+ ?.querySelector('dees-form');
284
+ if (!form) return;
285
+ const data = await form.collectFormData();
286
+ const providerKey = data.providerId?.key ?? data.providerId;
287
+ if (!providerKey) {
288
+ DeesToast.show({ message: 'Pick a provider', type: 'warning', duration: 2500 });
289
+ return;
290
+ }
291
+ const names = String(data.domainNames || '')
292
+ .split(',')
293
+ .map((s) => s.trim())
294
+ .filter(Boolean);
295
+ if (names.length === 0) {
296
+ DeesToast.show({ message: 'Enter at least one FQDN', type: 'warning', duration: 2500 });
297
+ return;
298
+ }
299
+ await appstate.domainsStatePart.dispatchAction(
300
+ appstate.importDomainsFromProviderAction,
301
+ { providerId: String(providerKey), domainNames: names },
302
+ );
303
+ modalArg.destroy();
304
+ },
305
+ },
306
+ ],
307
+ });
308
+ }
309
+
310
+ private async deleteDomain(domain: interfaces.data.IDomain) {
311
+ const { DeesModal } = await import('@design.estate/dees-catalog');
312
+ DeesModal.createAndShow({
313
+ heading: `Delete domain ${domain.name}?`,
314
+ content: html`
315
+ <p>
316
+ ${domain.source === 'provider'
317
+ ? 'This removes the domain and its cached records from dcrouter only. The zone at the provider is NOT touched.'
318
+ : 'This removes the domain and all of its DNS records from dcrouter. dcrouter will no longer answer queries for this domain after the next restart.'}
319
+ </p>
320
+ `,
321
+ menuOptions: [
322
+ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
323
+ {
324
+ name: 'Delete',
325
+ action: async (modalArg: any) => {
326
+ await appstate.domainsStatePart.dispatchAction(appstate.deleteDomainAction, {
327
+ id: domain.id,
328
+ });
329
+ modalArg.destroy();
330
+ },
331
+ },
332
+ ],
333
+ });
334
+ }
335
+ }
@@ -0,0 +1,284 @@
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 './dns-provider-form.js';
14
+ import type { DnsProviderForm } from './dns-provider-form.js';
15
+
16
+ declare global {
17
+ interface HTMLElementTagNameMap {
18
+ 'ops-view-providers': OpsViewProviders;
19
+ }
20
+ }
21
+
22
+ @customElement('ops-view-providers')
23
+ export class OpsViewProviders extends DeesElement {
24
+ @state()
25
+ accessor domainsState: appstate.IDomainsState = appstate.domainsStatePart.getState()!;
26
+
27
+ constructor() {
28
+ super();
29
+ const sub = appstate.domainsStatePart.select().subscribe((newState) => {
30
+ this.domainsState = newState;
31
+ });
32
+ this.rxSubscriptions.push(sub);
33
+ }
34
+
35
+ async connectedCallback() {
36
+ await super.connectedCallback();
37
+ await appstate.domainsStatePart.dispatchAction(appstate.fetchDomainsAndProvidersAction, null);
38
+ }
39
+
40
+ public static styles = [
41
+ cssManager.defaultStyles,
42
+ viewHostCss,
43
+ css`
44
+ .providersContainer {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 24px;
48
+ }
49
+
50
+ .statusBadge {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ padding: 3px 10px;
54
+ border-radius: 12px;
55
+ font-size: 12px;
56
+ font-weight: 600;
57
+ text-transform: uppercase;
58
+ }
59
+
60
+ .statusBadge.ok {
61
+ background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
62
+ color: ${cssManager.bdTheme('#166534', '#4ade80')};
63
+ }
64
+
65
+ .statusBadge.error {
66
+ background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
67
+ color: ${cssManager.bdTheme('#991b1b', '#f87171')};
68
+ }
69
+
70
+ .statusBadge.untested {
71
+ background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
72
+ color: ${cssManager.bdTheme('#4b5563', '#9ca3af')};
73
+ }
74
+ `,
75
+ ];
76
+
77
+ public render(): TemplateResult {
78
+ const providers = this.domainsState.providers;
79
+
80
+ return html`
81
+ <dees-heading level="3">DNS Providers</dees-heading>
82
+ <div class="providersContainer">
83
+ <dees-table
84
+ .heading1=${'Providers'}
85
+ .heading2=${'External DNS provider accounts'}
86
+ .data=${providers}
87
+ .showColumnFilters=${true}
88
+ .displayFunction=${(p: interfaces.data.IDnsProviderPublic) => ({
89
+ Name: p.name,
90
+ Type: this.providerTypeLabel(p.type),
91
+ Status: this.renderStatusBadge(p.status),
92
+ 'Last Tested': p.lastTestedAt ? new Date(p.lastTestedAt).toLocaleString() : 'never',
93
+ Error: p.lastError || '-',
94
+ })}
95
+ .dataActions=${[
96
+ {
97
+ name: 'Add Provider',
98
+ iconName: 'lucide:plus',
99
+ type: ['header' as const],
100
+ actionFunc: async () => {
101
+ await this.showCreateDialog();
102
+ },
103
+ },
104
+ {
105
+ name: 'Refresh',
106
+ iconName: 'lucide:rotateCw',
107
+ type: ['header' as const],
108
+ actionFunc: async () => {
109
+ await appstate.domainsStatePart.dispatchAction(
110
+ appstate.fetchDomainsAndProvidersAction,
111
+ null,
112
+ );
113
+ },
114
+ },
115
+ {
116
+ name: 'Test Connection',
117
+ iconName: 'lucide:plug',
118
+ type: ['inRow', 'contextmenu'] as any,
119
+ actionFunc: async (actionData: any) => {
120
+ const provider = actionData.item as interfaces.data.IDnsProviderPublic;
121
+ await this.testProvider(provider);
122
+ },
123
+ },
124
+ {
125
+ name: 'Edit',
126
+ iconName: 'lucide:pencil',
127
+ type: ['inRow', 'contextmenu'] as any,
128
+ actionFunc: async (actionData: any) => {
129
+ const provider = actionData.item as interfaces.data.IDnsProviderPublic;
130
+ await this.showEditDialog(provider);
131
+ },
132
+ },
133
+ {
134
+ name: 'Delete',
135
+ iconName: 'lucide:trash2',
136
+ type: ['inRow', 'contextmenu'] as any,
137
+ actionFunc: async (actionData: any) => {
138
+ const provider = actionData.item as interfaces.data.IDnsProviderPublic;
139
+ await this.deleteProvider(provider);
140
+ },
141
+ },
142
+ ]}
143
+ ></dees-table>
144
+ </div>
145
+ `;
146
+ }
147
+
148
+ private renderStatusBadge(status: interfaces.data.TDnsProviderStatus): TemplateResult {
149
+ return html`<span class="statusBadge ${status}">${status}</span>`;
150
+ }
151
+
152
+ private providerTypeLabel(type: interfaces.data.TDnsProviderType): string {
153
+ return interfaces.data.getDnsProviderTypeDescriptor(type)?.displayName ?? type;
154
+ }
155
+
156
+ private async showCreateDialog() {
157
+ const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
158
+ const formEl = document.createElement('dns-provider-form') as DnsProviderForm;
159
+ DeesModal.createAndShow({
160
+ heading: 'Add DNS Provider',
161
+ content: html`${formEl}`,
162
+ menuOptions: [
163
+ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
164
+ {
165
+ name: 'Create',
166
+ action: async (modalArg: any) => {
167
+ const data = await formEl.collectData();
168
+ if (!data) return;
169
+ if (!data.name) {
170
+ DeesToast.show({ message: 'Name is required', type: 'warning', duration: 2500 });
171
+ return;
172
+ }
173
+ if (!data.credentialsTouched) {
174
+ DeesToast.show({
175
+ message: 'Fill in the provider credentials',
176
+ type: 'warning',
177
+ duration: 2500,
178
+ });
179
+ return;
180
+ }
181
+ await appstate.domainsStatePart.dispatchAction(appstate.createDnsProviderAction, {
182
+ name: data.name,
183
+ type: data.type,
184
+ credentials: data.credentials,
185
+ });
186
+ modalArg.destroy();
187
+ },
188
+ },
189
+ ],
190
+ });
191
+ }
192
+
193
+ private async showEditDialog(provider: interfaces.data.IDnsProviderPublic) {
194
+ const { DeesModal } = await import('@design.estate/dees-catalog');
195
+ const formEl = document.createElement('dns-provider-form') as DnsProviderForm;
196
+ formEl.providerName = provider.name;
197
+ formEl.selectedType = provider.type;
198
+ formEl.lockType = true;
199
+ formEl.credentialsHint =
200
+ 'Leave credential fields blank to keep the current values. Fill them to rotate.';
201
+ DeesModal.createAndShow({
202
+ heading: `Edit Provider: ${provider.name}`,
203
+ content: html`${formEl}`,
204
+ menuOptions: [
205
+ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
206
+ {
207
+ name: 'Save',
208
+ action: async (modalArg: any) => {
209
+ const data = await formEl.collectData();
210
+ if (!data) return;
211
+ await appstate.domainsStatePart.dispatchAction(appstate.updateDnsProviderAction, {
212
+ id: provider.id,
213
+ name: data.name || provider.name,
214
+ // Only send credentials if the user actually entered something —
215
+ // otherwise we keep the current secret untouched.
216
+ credentials: data.credentialsTouched ? data.credentials : undefined,
217
+ });
218
+ modalArg.destroy();
219
+ },
220
+ },
221
+ ],
222
+ });
223
+ }
224
+
225
+ private async testProvider(provider: interfaces.data.IDnsProviderPublic) {
226
+ const { DeesToast } = await import('@design.estate/dees-catalog');
227
+ await appstate.domainsStatePart.dispatchAction(appstate.testDnsProviderAction, {
228
+ id: provider.id,
229
+ });
230
+ const updated = appstate.domainsStatePart
231
+ .getState()!
232
+ .providers.find((p) => p.id === provider.id);
233
+ if (updated?.status === 'ok') {
234
+ DeesToast.show({
235
+ message: `${provider.name}: connection OK`,
236
+ type: 'success',
237
+ duration: 3000,
238
+ });
239
+ } else {
240
+ DeesToast.show({
241
+ message: `${provider.name}: ${updated?.lastError || 'connection failed'}`,
242
+ type: 'error',
243
+ duration: 4000,
244
+ });
245
+ }
246
+ }
247
+
248
+ private async deleteProvider(provider: interfaces.data.IDnsProviderPublic) {
249
+ const linkedDomains = this.domainsState.domains.filter((d) => d.providerId === provider.id);
250
+ const { DeesModal } = await import('@design.estate/dees-catalog');
251
+
252
+ const doDelete = async (force: boolean) => {
253
+ await appstate.domainsStatePart.dispatchAction(appstate.deleteDnsProviderAction, {
254
+ id: provider.id,
255
+ force,
256
+ });
257
+ };
258
+
259
+ if (linkedDomains.length > 0) {
260
+ DeesModal.createAndShow({
261
+ heading: `Provider in use`,
262
+ content: html`
263
+ <p>
264
+ Provider <strong>${provider.name}</strong> is referenced by ${linkedDomains.length}
265
+ domain(s). Deleting will also remove the imported domain(s) and their cached
266
+ records (the records at ${provider.type} are NOT touched).
267
+ </p>
268
+ `,
269
+ menuOptions: [
270
+ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
271
+ {
272
+ name: 'Force Delete',
273
+ action: async (modalArg: any) => {
274
+ await doDelete(true);
275
+ modalArg.destroy();
276
+ },
277
+ },
278
+ ],
279
+ });
280
+ } else {
281
+ await doDelete(false);
282
+ }
283
+ }
284
+ }
@@ -111,7 +111,7 @@ export class OpsViewEmailSecurity extends DeesElement {
111
111
  ];
112
112
 
113
113
  return html`
114
- <dees-heading level="hr">Email Security</dees-heading>
114
+ <dees-heading level="3">Email Security</dees-heading>
115
115
 
116
116
  <dees-statsgrid
117
117
  .tiles=${tiles}
@@ -60,7 +60,7 @@ export class OpsViewEmails extends DeesElement {
60
60
 
61
61
  public render() {
62
62
  return html`
63
- <dees-heading level="hr">Email Log</dees-heading>
63
+ <dees-heading level="3">Email Log</dees-heading>
64
64
  <div class="viewContainer">
65
65
  ${this.currentView === 'detail' && this.selectedEmail
66
66
  ? html`
@@ -5,5 +5,5 @@ export * from './email/index.js';
5
5
  export * from './ops-view-logs.js';
6
6
  export * from './access/index.js';
7
7
  export * from './security/index.js';
8
- export * from './ops-view-certificates.js';
8
+ export * from './domains/index.js';
9
9
  export * from './shared/index.js';
@@ -285,7 +285,7 @@ export class OpsViewNetworkActivity extends DeesElement {
285
285
 
286
286
  public render() {
287
287
  return html`
288
- <dees-heading level="hr">Network Activity</dees-heading>
288
+ <dees-heading level="3">Network Activity</dees-heading>
289
289
 
290
290
  <div class="networkContainer">
291
291
  <!-- Stats Grid -->
@@ -64,7 +64,7 @@ export class OpsViewNetworkTargets extends DeesElement {
64
64
  ];
65
65
 
66
66
  return html`
67
- <dees-heading level="hr">Network Targets</dees-heading>
67
+ <dees-heading level="3">Network Targets</dees-heading>
68
68
  <div class="targetsContainer">
69
69
  <dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
70
70
  <dees-table
@@ -174,7 +174,7 @@ export class OpsViewRemoteIngress extends DeesElement {
174
174
  ];
175
175
 
176
176
  return html`
177
- <dees-heading level="hr">Remote Ingress</dees-heading>
177
+ <dees-heading level="3">Remote Ingress</dees-heading>
178
178
 
179
179
  ${this.riState.newEdgeId ? html`
180
180
  <div class="secretDialog">
@@ -200,7 +200,7 @@ export class OpsViewRoutes extends DeesElement {
200
200
  });
201
201
 
202
202
  return html`
203
- <dees-heading level="hr">Route Management</dees-heading>
203
+ <dees-heading level="3">Route Management</dees-heading>
204
204
 
205
205
  <div class="routesContainer">
206
206
  <dees-statsgrid
@@ -64,7 +64,7 @@ export class OpsViewSourceProfiles extends DeesElement {
64
64
  ];
65
65
 
66
66
  return html`
67
- <dees-heading level="hr">Source Profiles</dees-heading>
67
+ <dees-heading level="3">Source Profiles</dees-heading>
68
68
  <div class="profilesContainer">
69
69
  <dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
70
70
  <dees-table