@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,216 @@
1
+ import {
2
+ DeesElement,
3
+ html,
4
+ customElement,
5
+ type TemplateResult,
6
+ css,
7
+ state,
8
+ property,
9
+ cssManager,
10
+ } from '@design.estate/dees-element';
11
+ import * as interfaces from '../../../dist_ts_interfaces/index.js';
12
+
13
+ declare global {
14
+ interface HTMLElementTagNameMap {
15
+ 'dns-provider-form': DnsProviderForm;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Reactive credential form for a DNS provider. Renders the type picker
21
+ * and the credential fields for the currently-selected type.
22
+ *
23
+ * Provider-agnostic — driven entirely by `dnsProviderTypeDescriptors` from
24
+ * `ts_interfaces/data/dns-provider.ts`. Adding a new provider type means
25
+ * appending one entry to the descriptors array; this form picks it up
26
+ * automatically.
27
+ *
28
+ * Usage:
29
+ *
30
+ * const formEl = document.createElement('dns-provider-form');
31
+ * formEl.providerName = 'My provider';
32
+ * // ... pass element into a DeesModal as content ...
33
+ * // on submit:
34
+ * const data = formEl.collectData();
35
+ * // → { name, type, credentials }
36
+ *
37
+ * In edit mode, set `lockType = true` so the user cannot change provider
38
+ * type after creation (credentials shapes don't transfer between types).
39
+ */
40
+ @customElement('dns-provider-form')
41
+ export class DnsProviderForm extends DeesElement {
42
+ /** Pre-populated provider name. */
43
+ @property({ type: String })
44
+ accessor providerName: string = '';
45
+
46
+ /**
47
+ * Currently selected provider type. Initialized to the first descriptor;
48
+ * caller can override before mounting (e.g. for edit dialogs).
49
+ */
50
+ @state()
51
+ accessor selectedType: interfaces.data.TDnsProviderType =
52
+ interfaces.data.dnsProviderTypeDescriptors[0]?.type ?? 'cloudflare';
53
+
54
+ /** When true, hide the type picker — used in edit dialogs. */
55
+ @property({ type: Boolean })
56
+ accessor lockType: boolean = false;
57
+
58
+ /**
59
+ * Help text shown above credentials. Useful for edit dialogs to indicate
60
+ * that fields can be left blank to keep current values.
61
+ */
62
+ @property({ type: String })
63
+ accessor credentialsHint: string = '';
64
+
65
+ /** Internal map of credential field values, keyed by the descriptor's `key`. */
66
+ @state()
67
+ accessor credentialValues: Record<string, string> = {};
68
+
69
+ public static styles = [
70
+ cssManager.defaultStyles,
71
+ css`
72
+ :host {
73
+ display: block;
74
+ }
75
+
76
+ .field {
77
+ margin-bottom: 12px;
78
+ }
79
+
80
+ .helpText {
81
+ font-size: 12px;
82
+ opacity: 0.7;
83
+ margin-top: -6px;
84
+ margin-bottom: 8px;
85
+ }
86
+
87
+ .typeDescription {
88
+ font-size: 12px;
89
+ opacity: 0.8;
90
+ margin: 4px 0 16px;
91
+ padding: 8px 12px;
92
+ background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
93
+ border-radius: 6px;
94
+ }
95
+
96
+ .credentialsHint {
97
+ font-size: 12px;
98
+ opacity: 0.7;
99
+ margin-bottom: 12px;
100
+ }
101
+ `,
102
+ ];
103
+
104
+ public render(): TemplateResult {
105
+ const descriptors = interfaces.data.dnsProviderTypeDescriptors;
106
+ const descriptor = interfaces.data.getDnsProviderTypeDescriptor(this.selectedType);
107
+
108
+ return html`
109
+ <dees-form>
110
+ <div class="field">
111
+ <dees-input-text
112
+ .key=${'name'}
113
+ .label=${'Provider name'}
114
+ .value=${this.providerName}
115
+ .required=${true}
116
+ ></dees-input-text>
117
+ </div>
118
+
119
+ ${this.lockType
120
+ ? html`
121
+ <div class="field">
122
+ <dees-input-text
123
+ .key=${'__type_display'}
124
+ .label=${'Type'}
125
+ .value=${descriptor?.displayName ?? this.selectedType}
126
+ .disabled=${true}
127
+ ></dees-input-text>
128
+ </div>
129
+ `
130
+ : html`
131
+ <div class="field">
132
+ <dees-input-dropdown
133
+ .key=${'__type'}
134
+ .label=${'Provider type'}
135
+ .options=${descriptors.map((d) => ({ option: d.displayName, key: d.type }))}
136
+ .selectedOption=${descriptor
137
+ ? { option: descriptor.displayName, key: descriptor.type }
138
+ : undefined}
139
+ @selectedOption=${(e: CustomEvent) => {
140
+ const newType = (e.detail as any)?.key as
141
+ | interfaces.data.TDnsProviderType
142
+ | undefined;
143
+ if (newType && newType !== this.selectedType) {
144
+ this.selectedType = newType;
145
+ this.credentialValues = {};
146
+ }
147
+ }}
148
+ ></dees-input-dropdown>
149
+ </div>
150
+ `}
151
+ ${descriptor
152
+ ? html`
153
+ <div class="typeDescription">${descriptor.description}</div>
154
+ ${this.credentialsHint
155
+ ? html`<div class="credentialsHint">${this.credentialsHint}</div>`
156
+ : ''}
157
+ ${descriptor.credentialFields.map(
158
+ (f) => html`
159
+ <div class="field">
160
+ <dees-input-text
161
+ .key=${f.key}
162
+ .label=${f.label}
163
+ .required=${f.required && !this.lockType}
164
+ ></dees-input-text>
165
+ ${f.helpText ? html`<div class="helpText">${f.helpText}</div>` : ''}
166
+ </div>
167
+ `,
168
+ )}
169
+ `
170
+ : html`<p>No provider types registered.</p>`}
171
+ </dees-form>
172
+ `;
173
+ }
174
+
175
+ /**
176
+ * Read the form values and assemble the create/update payload.
177
+ * Returns the typed credentials object built from the descriptor's keys.
178
+ */
179
+ public async collectData(): Promise<{
180
+ name: string;
181
+ type: interfaces.data.TDnsProviderType;
182
+ credentials: interfaces.data.TDnsProviderCredentials;
183
+ credentialsTouched: boolean;
184
+ } | null> {
185
+ const form = this.shadowRoot?.querySelector('dees-form') as any;
186
+ if (!form) return null;
187
+ const data = await form.collectFormData();
188
+ const descriptor = interfaces.data.getDnsProviderTypeDescriptor(this.selectedType);
189
+ if (!descriptor) return null;
190
+
191
+ // Build the credentials object from the descriptor's field keys.
192
+ const credsBody: Record<string, string> = {};
193
+ let credentialsTouched = false;
194
+ for (const f of descriptor.credentialFields) {
195
+ const value = data[f.key];
196
+ if (value !== undefined && value !== null && String(value).length > 0) {
197
+ credsBody[f.key] = String(value);
198
+ credentialsTouched = true;
199
+ }
200
+ }
201
+
202
+ // The discriminator goes on the credentials object so the backend
203
+ // factory and the discriminated union both stay happy.
204
+ const credentials = {
205
+ type: this.selectedType,
206
+ ...credsBody,
207
+ } as unknown as interfaces.data.TDnsProviderCredentials;
208
+
209
+ return {
210
+ name: String(data.name ?? ''),
211
+ type: this.selectedType,
212
+ credentials,
213
+ credentialsTouched,
214
+ };
215
+ }
216
+ }
@@ -0,0 +1,5 @@
1
+ export * from './dns-provider-form.js';
2
+ export * from './ops-view-providers.js';
3
+ export * from './ops-view-domains.js';
4
+ export * from './ops-view-dns.js';
5
+ export * from './ops-view-certificates.js';
@@ -7,9 +7,9 @@ import {
7
7
  state,
8
8
  cssManager,
9
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';
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
13
  import { type IStatsTile } from '@design.estate/dees-catalog';
14
14
 
15
15
  declare global {
@@ -159,7 +159,7 @@ export class OpsViewCertificates extends DeesElement {
159
159
  const { summary } = this.certState;
160
160
 
161
161
  return html`
162
- <dees-heading level="hr">Certificates</dees-heading>
162
+ <dees-heading level="3">Certificates</dees-heading>
163
163
 
164
164
  <div class="certificatesContainer">
165
165
  ${this.renderStatsTiles(summary)}
@@ -0,0 +1,273 @@
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
+
14
+ declare global {
15
+ interface HTMLElementTagNameMap {
16
+ 'ops-view-dns': OpsViewDns;
17
+ }
18
+ }
19
+
20
+ const RECORD_TYPES: interfaces.data.TDnsRecordType[] = [
21
+ 'A',
22
+ 'AAAA',
23
+ 'CNAME',
24
+ 'MX',
25
+ 'TXT',
26
+ 'NS',
27
+ 'CAA',
28
+ ];
29
+
30
+ @customElement('ops-view-dns')
31
+ export class OpsViewDns extends DeesElement {
32
+ @state()
33
+ accessor domainsState: appstate.IDomainsState = appstate.domainsStatePart.getState()!;
34
+
35
+ constructor() {
36
+ super();
37
+ const sub = appstate.domainsStatePart.select().subscribe((newState) => {
38
+ this.domainsState = newState;
39
+ });
40
+ this.rxSubscriptions.push(sub);
41
+ }
42
+
43
+ async connectedCallback() {
44
+ await super.connectedCallback();
45
+ await appstate.domainsStatePart.dispatchAction(appstate.fetchDomainsAndProvidersAction, null);
46
+ // If a domain is already selected (e.g. via "View Records" navigation), refresh its records
47
+ const selected = this.domainsState.selectedDomainId;
48
+ if (selected) {
49
+ await appstate.domainsStatePart.dispatchAction(appstate.fetchDnsRecordsForDomainAction, {
50
+ domainId: selected,
51
+ });
52
+ }
53
+ }
54
+
55
+ public static styles = [
56
+ cssManager.defaultStyles,
57
+ viewHostCss,
58
+ css`
59
+ .dnsContainer {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 24px;
63
+ }
64
+
65
+ .domainPicker {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 12px;
69
+ padding: 12px 16px;
70
+ background: ${cssManager.bdTheme('#f9fafb', '#111827')};
71
+ border-radius: 8px;
72
+ }
73
+
74
+ .sourceBadge {
75
+ display: inline-flex;
76
+ align-items: center;
77
+ padding: 2px 8px;
78
+ border-radius: 4px;
79
+ font-size: 11px;
80
+ font-weight: 500;
81
+ }
82
+
83
+ .sourceBadge.manual {
84
+ background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
85
+ color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
86
+ }
87
+
88
+ .sourceBadge.synced {
89
+ background: ${cssManager.bdTheme('#fef3c7', '#451a03')};
90
+ color: ${cssManager.bdTheme('#92400e', '#fde047')};
91
+ }
92
+ `,
93
+ ];
94
+
95
+ public render(): TemplateResult {
96
+ const domains = this.domainsState.domains;
97
+ const selectedId = this.domainsState.selectedDomainId;
98
+ const records = this.domainsState.records;
99
+
100
+ return html`
101
+ <dees-heading level="3">DNS Records</dees-heading>
102
+ <div class="dnsContainer">
103
+ <div class="domainPicker">
104
+ <span>Domain:</span>
105
+ <dees-input-dropdown
106
+ .options=${domains.map((d) => ({ option: d.name, key: d.id }))}
107
+ .selectedOption=${selectedId
108
+ ? { option: domains.find((d) => d.id === selectedId)?.name || '', key: selectedId }
109
+ : undefined}
110
+ @selectedOption=${async (e: CustomEvent) => {
111
+ const id = (e.detail as any)?.key;
112
+ if (!id) return;
113
+ await appstate.domainsStatePart.dispatchAction(
114
+ appstate.fetchDnsRecordsForDomainAction,
115
+ { domainId: id },
116
+ );
117
+ }}
118
+ ></dees-input-dropdown>
119
+ </div>
120
+
121
+ ${selectedId
122
+ ? html`
123
+ <dees-table
124
+ .heading1=${'DNS Records'}
125
+ .heading2=${this.domainHint(selectedId)}
126
+ .data=${records}
127
+ .showColumnFilters=${true}
128
+ .displayFunction=${(r: interfaces.data.IDnsRecord) => ({
129
+ Name: r.name,
130
+ Type: r.type,
131
+ Value: r.value,
132
+ TTL: r.ttl,
133
+ Source: html`<span class="sourceBadge ${r.source}">${r.source}</span>`,
134
+ })}
135
+ .dataActions=${[
136
+ {
137
+ name: 'Add Record',
138
+ iconName: 'lucide:plus',
139
+ type: ['header' as const],
140
+ actionFunc: async () => {
141
+ await this.showCreateRecordDialog(selectedId);
142
+ },
143
+ },
144
+ {
145
+ name: 'Refresh',
146
+ iconName: 'lucide:rotateCw',
147
+ type: ['header' as const],
148
+ actionFunc: async () => {
149
+ await appstate.domainsStatePart.dispatchAction(
150
+ appstate.fetchDnsRecordsForDomainAction,
151
+ { domainId: selectedId },
152
+ );
153
+ },
154
+ },
155
+ {
156
+ name: 'Edit',
157
+ iconName: 'lucide:pencil',
158
+ type: ['inRow', 'contextmenu'] as any,
159
+ actionFunc: async (actionData: any) => {
160
+ const rec = actionData.item as interfaces.data.IDnsRecord;
161
+ await this.showEditRecordDialog(rec);
162
+ },
163
+ },
164
+ {
165
+ name: 'Delete',
166
+ iconName: 'lucide:trash2',
167
+ type: ['inRow', 'contextmenu'] as any,
168
+ actionFunc: async (actionData: any) => {
169
+ const rec = actionData.item as interfaces.data.IDnsRecord;
170
+ await appstate.domainsStatePart.dispatchAction(
171
+ appstate.deleteDnsRecordAction,
172
+ { id: rec.id, domainId: rec.domainId },
173
+ );
174
+ },
175
+ },
176
+ ]}
177
+ ></dees-table>
178
+ `
179
+ : html`<p style="opacity: 0.7;">Pick a domain above to view its records.</p>`}
180
+ </div>
181
+ `;
182
+ }
183
+
184
+ private domainHint(domainId: string): string {
185
+ const domain = this.domainsState.domains.find((d) => d.id === domainId);
186
+ if (!domain) return '';
187
+ if (domain.source === 'manual') {
188
+ return 'Records are served by dcrouter (authoritative).';
189
+ }
190
+ return 'Records are stored at the provider — changes here are pushed via the provider API.';
191
+ }
192
+
193
+ private async showCreateRecordDialog(domainId: string) {
194
+ const { DeesModal } = await import('@design.estate/dees-catalog');
195
+ DeesModal.createAndShow({
196
+ heading: 'Add DNS Record',
197
+ content: html`
198
+ <dees-form>
199
+ <dees-input-text .key=${'name'} .label=${'Name (FQDN)'} .required=${true}></dees-input-text>
200
+ <dees-input-dropdown
201
+ .key=${'type'}
202
+ .label=${'Type'}
203
+ .options=${RECORD_TYPES.map((t) => ({ option: t, key: t }))}
204
+ .required=${true}
205
+ ></dees-input-dropdown>
206
+ <dees-input-text
207
+ .key=${'value'}
208
+ .label=${'Value (for MX use "10 mail.example.com")'}
209
+ .required=${true}
210
+ ></dees-input-text>
211
+ <dees-input-text .key=${'ttl'} .label=${'TTL (seconds)'} .value=${'300'}></dees-input-text>
212
+ </dees-form>
213
+ `,
214
+ menuOptions: [
215
+ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
216
+ {
217
+ name: 'Create',
218
+ action: async (modalArg: any) => {
219
+ const form = modalArg.shadowRoot
220
+ ?.querySelector('.content')
221
+ ?.querySelector('dees-form');
222
+ if (!form) return;
223
+ const data = await form.collectFormData();
224
+ const type = (data.type?.key ?? data.type) as interfaces.data.TDnsRecordType;
225
+ await appstate.domainsStatePart.dispatchAction(appstate.createDnsRecordAction, {
226
+ domainId,
227
+ name: String(data.name),
228
+ type,
229
+ value: String(data.value),
230
+ ttl: parseInt(String(data.ttl || '300'), 10),
231
+ });
232
+ modalArg.destroy();
233
+ },
234
+ },
235
+ ],
236
+ });
237
+ }
238
+
239
+ private async showEditRecordDialog(rec: interfaces.data.IDnsRecord) {
240
+ const { DeesModal } = await import('@design.estate/dees-catalog');
241
+ DeesModal.createAndShow({
242
+ heading: `Edit ${rec.type} ${rec.name}`,
243
+ content: html`
244
+ <dees-form>
245
+ <dees-input-text .key=${'name'} .label=${'Name (FQDN)'} .value=${rec.name}></dees-input-text>
246
+ <dees-input-text .key=${'value'} .label=${'Value'} .value=${rec.value}></dees-input-text>
247
+ <dees-input-text .key=${'ttl'} .label=${'TTL (seconds)'} .value=${String(rec.ttl)}></dees-input-text>
248
+ </dees-form>
249
+ `,
250
+ menuOptions: [
251
+ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
252
+ {
253
+ name: 'Save',
254
+ action: async (modalArg: any) => {
255
+ const form = modalArg.shadowRoot
256
+ ?.querySelector('.content')
257
+ ?.querySelector('dees-form');
258
+ if (!form) return;
259
+ const data = await form.collectFormData();
260
+ await appstate.domainsStatePart.dispatchAction(appstate.updateDnsRecordAction, {
261
+ id: rec.id,
262
+ domainId: rec.domainId,
263
+ name: String(data.name),
264
+ value: String(data.value),
265
+ ttl: parseInt(String(data.ttl || '300'), 10),
266
+ });
267
+ modalArg.destroy();
268
+ },
269
+ },
270
+ ],
271
+ });
272
+ }
273
+ }