@serve.zone/dcrouter 13.4.2 → 13.7.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 (148) hide show
  1. package/dist_serve/bundle.js +1779 -1375
  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 +4 -0
  26. package/dist_ts/opsserver/classes.opsserver.js +9 -1
  27. package/dist_ts/opsserver/handlers/admin.handler.d.ts +9 -0
  28. package/dist_ts/opsserver/handlers/admin.handler.js +12 -1
  29. package/dist_ts/opsserver/handlers/config.handler.js +11 -2
  30. package/dist_ts/opsserver/handlers/dns-provider.handler.d.ts +16 -0
  31. package/dist_ts/opsserver/handlers/dns-provider.handler.js +119 -0
  32. package/dist_ts/opsserver/handlers/dns-record.handler.d.ts +13 -0
  33. package/dist_ts/opsserver/handlers/dns-record.handler.js +98 -0
  34. package/dist_ts/opsserver/handlers/domain.handler.d.ts +13 -0
  35. package/dist_ts/opsserver/handlers/domain.handler.js +124 -0
  36. package/dist_ts/opsserver/handlers/index.d.ts +4 -0
  37. package/dist_ts/opsserver/handlers/index.js +5 -1
  38. package/dist_ts/opsserver/handlers/users.handler.d.ts +12 -0
  39. package/dist_ts/opsserver/handlers/users.handler.js +24 -0
  40. package/dist_ts_interfaces/data/dns-provider.d.ts +112 -0
  41. package/dist_ts_interfaces/data/dns-provider.js +27 -0
  42. package/dist_ts_interfaces/data/dns-record.d.ts +40 -0
  43. package/dist_ts_interfaces/data/dns-record.js +2 -0
  44. package/dist_ts_interfaces/data/domain.d.ts +34 -0
  45. package/dist_ts_interfaces/data/domain.js +2 -0
  46. package/dist_ts_interfaces/data/index.d.ts +3 -0
  47. package/dist_ts_interfaces/data/index.js +4 -1
  48. package/dist_ts_interfaces/data/route-management.d.ts +1 -1
  49. package/dist_ts_interfaces/requests/dns-providers.d.ts +117 -0
  50. package/dist_ts_interfaces/requests/dns-providers.js +2 -0
  51. package/dist_ts_interfaces/requests/dns-records.d.ts +89 -0
  52. package/dist_ts_interfaces/requests/dns-records.js +2 -0
  53. package/dist_ts_interfaces/requests/domains.d.ts +118 -0
  54. package/dist_ts_interfaces/requests/domains.js +2 -0
  55. package/dist_ts_interfaces/requests/index.d.ts +4 -0
  56. package/dist_ts_interfaces/requests/index.js +5 -1
  57. package/dist_ts_interfaces/requests/users.d.ts +19 -0
  58. package/dist_ts_interfaces/requests/users.js +3 -0
  59. package/dist_ts_web/00_commitinfo_data.js +1 -1
  60. package/dist_ts_web/appstate.d.ts +85 -0
  61. package/dist_ts_web/appstate.js +339 -6
  62. package/dist_ts_web/elements/access/index.d.ts +1 -0
  63. package/dist_ts_web/elements/access/index.js +2 -1
  64. package/dist_ts_web/elements/access/ops-view-apitokens.js +1 -1
  65. package/dist_ts_web/elements/access/ops-view-users.d.ts +11 -0
  66. package/dist_ts_web/elements/access/ops-view-users.js +190 -0
  67. package/dist_ts_web/elements/domains/dns-provider-form.d.ts +58 -0
  68. package/dist_ts_web/elements/domains/dns-provider-form.js +268 -0
  69. package/dist_ts_web/elements/domains/index.d.ts +5 -0
  70. package/dist_ts_web/elements/domains/index.js +6 -0
  71. package/dist_ts_web/elements/{ops-view-certificates.d.ts → domains/ops-view-certificates.d.ts} +1 -1
  72. package/dist_ts_web/elements/{ops-view-certificates.js → domains/ops-view-certificates.js} +5 -5
  73. package/dist_ts_web/elements/domains/ops-view-dns.d.ts +17 -0
  74. package/dist_ts_web/elements/domains/ops-view-dns.js +304 -0
  75. package/dist_ts_web/elements/domains/ops-view-domains.d.ts +18 -0
  76. package/dist_ts_web/elements/domains/ops-view-domains.js +361 -0
  77. package/dist_ts_web/elements/domains/ops-view-providers.d.ts +21 -0
  78. package/dist_ts_web/elements/domains/ops-view-providers.js +316 -0
  79. package/dist_ts_web/elements/email/ops-view-email-security.js +1 -1
  80. package/dist_ts_web/elements/email/ops-view-emails.js +1 -1
  81. package/dist_ts_web/elements/index.d.ts +1 -1
  82. package/dist_ts_web/elements/index.js +2 -2
  83. package/dist_ts_web/elements/network/ops-view-network-activity.js +6 -2
  84. package/dist_ts_web/elements/network/ops-view-networktargets.js +1 -1
  85. package/dist_ts_web/elements/network/ops-view-remoteingress.js +1 -1
  86. package/dist_ts_web/elements/network/ops-view-routes.js +1 -1
  87. package/dist_ts_web/elements/network/ops-view-sourceprofiles.js +1 -1
  88. package/dist_ts_web/elements/network/ops-view-targetprofiles.js +1 -1
  89. package/dist_ts_web/elements/network/ops-view-vpn.js +1 -1
  90. package/dist_ts_web/elements/ops-dashboard.js +16 -5
  91. package/dist_ts_web/elements/ops-view-logs.js +1 -1
  92. package/dist_ts_web/elements/overview/ops-view-config.js +3 -3
  93. package/dist_ts_web/elements/overview/ops-view-overview.js +1 -1
  94. package/dist_ts_web/elements/security/ops-view-security-authentication.js +1 -1
  95. package/dist_ts_web/elements/security/ops-view-security-blocked.js +1 -1
  96. package/dist_ts_web/elements/security/ops-view-security-overview.js +1 -1
  97. package/dist_ts_web/router.d.ts +1 -1
  98. package/dist_ts_web/router.js +5 -3
  99. package/package.json +2 -2
  100. package/ts/00_commitinfo_data.ts +1 -1
  101. package/ts/classes.dcrouter.ts +46 -17
  102. package/ts/db/documents/classes.dns-provider.doc.ts +63 -0
  103. package/ts/db/documents/classes.dns-record.doc.ts +62 -0
  104. package/ts/db/documents/classes.domain.doc.ts +66 -0
  105. package/ts/db/documents/index.ts +5 -0
  106. package/ts/dns/index.ts +2 -0
  107. package/ts/dns/manager.dns.ts +869 -0
  108. package/ts/dns/providers/cloudflare.provider.ts +131 -0
  109. package/ts/dns/providers/factory.ts +48 -0
  110. package/ts/dns/providers/index.ts +3 -0
  111. package/ts/dns/providers/interfaces.ts +67 -0
  112. package/ts/opsserver/classes.opsserver.ts +8 -0
  113. package/ts/opsserver/handlers/admin.handler.ts +12 -0
  114. package/ts/opsserver/handlers/config.handler.ts +10 -1
  115. package/ts/opsserver/handlers/dns-provider.handler.ts +159 -0
  116. package/ts/opsserver/handlers/dns-record.handler.ts +127 -0
  117. package/ts/opsserver/handlers/domain.handler.ts +161 -0
  118. package/ts/opsserver/handlers/index.ts +5 -1
  119. package/ts/opsserver/handlers/users.handler.ts +30 -0
  120. package/ts_web/00_commitinfo_data.ts +1 -1
  121. package/ts_web/appstate.ts +460 -5
  122. package/ts_web/elements/access/index.ts +1 -0
  123. package/ts_web/elements/access/ops-view-apitokens.ts +1 -1
  124. package/ts_web/elements/access/ops-view-users.ts +140 -0
  125. package/ts_web/elements/domains/dns-provider-form.ts +216 -0
  126. package/ts_web/elements/domains/index.ts +5 -0
  127. package/ts_web/elements/{ops-view-certificates.ts → domains/ops-view-certificates.ts} +4 -4
  128. package/ts_web/elements/domains/ops-view-dns.ts +273 -0
  129. package/ts_web/elements/domains/ops-view-domains.ts +335 -0
  130. package/ts_web/elements/domains/ops-view-providers.ts +284 -0
  131. package/ts_web/elements/email/ops-view-email-security.ts +1 -1
  132. package/ts_web/elements/email/ops-view-emails.ts +1 -1
  133. package/ts_web/elements/index.ts +1 -1
  134. package/ts_web/elements/network/ops-view-network-activity.ts +5 -1
  135. package/ts_web/elements/network/ops-view-networktargets.ts +1 -1
  136. package/ts_web/elements/network/ops-view-remoteingress.ts +1 -1
  137. package/ts_web/elements/network/ops-view-routes.ts +1 -1
  138. package/ts_web/elements/network/ops-view-sourceprofiles.ts +1 -1
  139. package/ts_web/elements/network/ops-view-targetprofiles.ts +1 -1
  140. package/ts_web/elements/network/ops-view-vpn.ts +1 -1
  141. package/ts_web/elements/ops-dashboard.ts +16 -4
  142. package/ts_web/elements/ops-view-logs.ts +1 -1
  143. package/ts_web/elements/overview/ops-view-config.ts +2 -2
  144. package/ts_web/elements/overview/ops-view-overview.ts +1 -1
  145. package/ts_web/elements/security/ops-view-security-authentication.ts +1 -1
  146. package/ts_web/elements/security/ops-view-security-blocked.ts +1 -1
  147. package/ts_web/elements/security/ops-view-security-overview.ts +1 -1
  148. package/ts_web/router.ts +4 -2
@@ -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="2">Email Operations</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 -->
@@ -347,6 +347,7 @@ export class OpsViewNetworkActivity extends DeesElement {
347
347
  heading1="Recent Network Activity"
348
348
  heading2="Recent network requests"
349
349
  searchable
350
+ .showColumnFilters=${true}
350
351
  .pagination=${true}
351
352
  .paginationSize=${50}
352
353
  dataName="request"
@@ -606,6 +607,8 @@ export class OpsViewNetworkActivity extends DeesElement {
606
607
  }}
607
608
  heading1="Top Connected IPs"
608
609
  heading2="IPs with most active connections and bandwidth"
610
+ searchable
611
+ .showColumnFilters=${true}
609
612
  .pagination=${false}
610
613
  dataName="ip"
611
614
  ></dees-table>
@@ -656,6 +659,7 @@ export class OpsViewNetworkActivity extends DeesElement {
656
659
  heading1="Backend Protocols"
657
660
  heading2="Auto-detected backend protocols and connection pool health"
658
661
  searchable
662
+ .showColumnFilters=${true}
659
663
  .pagination=${false}
660
664
  dataName="backend"
661
665
  ></dees-table>
@@ -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