feat(dns): add built-in dcrouter DNS provider support and rename manual domains to dcrouter-hosted/local

This commit is contained in:
2026-04-08 14:54:49 +00:00
parent 5689e93665
commit 93cc5c7b06
21 changed files with 245 additions and 91 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '13.8.0',
version: '13.9.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -1793,7 +1793,7 @@ export async function fetchProviderDomains(
return await request.fire({ identity: context.identity, providerId });
}
export const createManualDomainAction = domainsStatePart.createAction<{
export const createDcrouterDomainAction = domainsStatePart.createAction<{
name: string;
description?: string;
}>(async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {

View File

@@ -44,12 +44,15 @@ export class DnsProviderForm extends DeesElement {
accessor providerName: string = '';
/**
* Currently selected provider type. Initialized to the first descriptor;
* caller can override before mounting (e.g. for edit dialogs).
* Currently selected provider type. Initialized to the first user-creatable
* descriptor; caller can override before mounting (e.g. for edit dialogs).
* The built-in 'dcrouter' pseudo-provider is excluded from the picker —
* operators cannot create another one.
*/
@state()
accessor selectedType: interfaces.data.TDnsProviderType =
interfaces.data.dnsProviderTypeDescriptors[0]?.type ?? 'cloudflare';
interfaces.data.dnsProviderTypeDescriptors.find((d) => d.type !== 'dcrouter')?.type ??
'cloudflare';
/** When true, hide the type picker — used in edit dialogs. */
@property({ type: Boolean })
@@ -102,7 +105,12 @@ export class DnsProviderForm extends DeesElement {
];
public render(): TemplateResult {
const descriptors = interfaces.data.dnsProviderTypeDescriptors;
// Exclude the built-in 'dcrouter' pseudo-provider from the type picker —
// operators cannot create another one, it's surfaced at read time by the
// backend handler instead.
const descriptors = interfaces.data.dnsProviderTypeDescriptors.filter(
(d) => d.type !== 'dcrouter',
);
const descriptor = interfaces.data.getDnsProviderTypeDescriptor(this.selectedType);
return html`

View File

@@ -80,7 +80,7 @@ export class OpsViewDns extends DeesElement {
font-weight: 500;
}
.sourceBadge.manual {
.sourceBadge.local {
background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
}
@@ -184,7 +184,7 @@ export class OpsViewDns extends DeesElement {
private domainHint(domainId: string): string {
const domain = this.domainsState.domains.find((d) => d.id === domainId);
if (!domain) return '';
if (domain.source === 'manual') {
if (domain.source === 'dcrouter') {
return 'Records are served by dcrouter (authoritative).';
}
return 'Records are stored at the provider — changes here are pushed via the provider API.';

View File

@@ -55,7 +55,7 @@ export class OpsViewDomains extends DeesElement {
font-weight: 500;
}
.sourceBadge.manual {
.sourceBadge.dcrouter {
background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
}
@@ -76,7 +76,7 @@ export class OpsViewDomains extends DeesElement {
<div class="domainsContainer">
<dees-table
.heading1=${'Domains'}
.heading2=${'Domains under management — manual (authoritative) or imported from a provider'}
.heading2=${'Domains under management — served by dcrouter (authoritative) or imported from a provider'}
.data=${domains}
.showColumnFilters=${true}
.displayFunction=${(d: interfaces.data.IDomain) => ({
@@ -90,11 +90,11 @@ export class OpsViewDomains extends DeesElement {
})}
.dataActions=${[
{
name: 'Add Manual Domain',
name: 'Add DcRouter Domain',
iconName: 'lucide:plus',
type: ['header' as const],
actionFunc: async () => {
await this.showCreateManualDialog();
await this.showCreateDcrouterDialog();
},
},
{
@@ -168,17 +168,17 @@ export class OpsViewDomains extends DeesElement {
d: interfaces.data.IDomain,
providersById: Map<string, interfaces.data.IDnsProviderPublic>,
): TemplateResult {
if (d.source === 'manual') {
return html`<span class="sourceBadge manual">Manual</span>`;
if (d.source === 'dcrouter') {
return html`<span class="sourceBadge dcrouter">DcRouter</span>`;
}
const provider = d.providerId ? providersById.get(d.providerId) : undefined;
return html`<span class="sourceBadge provider">${provider?.name || 'Provider'}</span>`;
}
private async showCreateManualDialog() {
private async showCreateDcrouterDialog() {
const { DeesModal } = await import('@design.estate/dees-catalog');
DeesModal.createAndShow({
heading: 'Add Manual Domain',
heading: 'Add DcRouter Domain',
content: html`
<dees-form>
<dees-input-text .key=${'name'} .label=${'FQDN (e.g. example.com)'} .required=${true}></dees-input-text>
@@ -199,7 +199,7 @@ export class OpsViewDomains extends DeesElement {
?.querySelector('dees-form');
if (!form) return;
const data = await form.collectFormData();
await appstate.domainsStatePart.dispatchAction(appstate.createManualDomainAction, {
await appstate.domainsStatePart.dispatchAction(appstate.createDcrouterDomainAction, {
name: String(data.name),
description: data.description ? String(data.description) : undefined,
});

View File

@@ -71,6 +71,11 @@ export class OpsViewProviders extends DeesElement {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
color: ${cssManager.bdTheme('#4b5563', '#9ca3af')};
}
.statusBadge.builtin {
background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
}
`,
];
@@ -82,15 +87,21 @@ export class OpsViewProviders extends DeesElement {
<div class="providersContainer">
<dees-table
.heading1=${'Providers'}
.heading2=${'External DNS provider accounts'}
.heading2=${'Built-in dcrouter + external DNS provider accounts'}
.data=${providers}
.showColumnFilters=${true}
.displayFunction=${(p: interfaces.data.IDnsProviderPublic) => ({
Name: p.name,
Type: this.providerTypeLabel(p.type),
Status: this.renderStatusBadge(p.status),
'Last Tested': p.lastTestedAt ? new Date(p.lastTestedAt).toLocaleString() : 'never',
Error: p.lastError || '-',
Status: p.builtIn
? html`<span class="statusBadge builtin">built-in</span>`
: this.renderStatusBadge(p.status),
'Last Tested': p.builtIn
? '—'
: p.lastTestedAt
? new Date(p.lastTestedAt).toLocaleString()
: 'never',
Error: p.builtIn ? '—' : p.lastError || '-',
})}
.dataActions=${[
{
@@ -116,6 +127,7 @@ export class OpsViewProviders extends DeesElement {
name: 'Test Connection',
iconName: 'lucide:plug',
type: ['inRow', 'contextmenu'] as any,
actionRelevancyCheckFunc: (p: interfaces.data.IDnsProviderPublic) => !p.builtIn,
actionFunc: async (actionData: any) => {
const provider = actionData.item as interfaces.data.IDnsProviderPublic;
await this.testProvider(provider);
@@ -125,6 +137,7 @@ export class OpsViewProviders extends DeesElement {
name: 'Edit',
iconName: 'lucide:pencil',
type: ['inRow', 'contextmenu'] as any,
actionRelevancyCheckFunc: (p: interfaces.data.IDnsProviderPublic) => !p.builtIn,
actionFunc: async (actionData: any) => {
const provider = actionData.item as interfaces.data.IDnsProviderPublic;
await this.showEditDialog(provider);
@@ -134,6 +147,7 @@ export class OpsViewProviders extends DeesElement {
name: 'Delete',
iconName: 'lucide:trash2',
type: ['inRow', 'contextmenu'] as any,
actionRelevancyCheckFunc: (p: interfaces.data.IDnsProviderPublic) => !p.builtIn,
actionFunc: async (actionData: any) => {
const provider = actionData.item as interfaces.data.IDnsProviderPublic;
await this.deleteProvider(provider);