Guided wizards, opt-in every time, and automatic DNS as the first

A job that has a wizard now asks "Guide me / I'll do it myself" each time
it starts; nothing is remembered. The shared wizard shell gives every guide
a stepper, a side panel on what each step does and how to undo it, and the
way forward or back.

Automatic DNS, from a domain's DNS section:
- finds where the domain's DNS is hosted from its SOA and NS records, and
  offers that host when the server can drive it;
- for the major hosts, steps to create the narrowest credential, and the
  field named as the steps name it;
- records grouped by what they do, TLSA off unless the zone is signed;
- saves the provider and switches the domain over, removing the provider
  again if the switch fails;
- watches the publishing task and public DNS, ticking each record green,
  and boils a host's refusal down to its distinct messages;
- for hosts it can't drive, or domains not in DNS yet, every record laid
  out for copying, with the same live checks.
This commit is contained in:
2026-09-19 01:41:00 -07:00
parent 2e02532279
commit 5641560a91
18 changed files with 2435 additions and 1 deletions
+13 -1
View File
@@ -57,6 +57,7 @@ import { SECRET_MASK } from '@/lib/jmapUtils';
import { toast } from '@/hooks/use-toast';
import { logFormChange } from '@/lib/debug';
import { FieldWidget } from '@/components/forms/FieldWidget';
import { DnsConnectCard } from '@/features/dns/DnsConnectCard';
import { isSieveScriptField } from '@/lib/sievepad';
import type { Field, Fields, Form, FormField, Schema } from '@/types/schema';
@@ -777,6 +778,17 @@ export function DynamicForm({ viewName, objectId }: DynamicFormProps) {
)}
<CardContent className={section.title ? '' : 'pt-6'}>
<div className="space-y-6">
{resolved.obj.objectName === 'x:Domain' &&
objectId &&
!readOnly &&
section.fields.some((sf) => sf.formField.name === 'dnsManagement') && (
<DnsConnectCard
domainId={objectId}
automatic={
(originalData.dnsManagement as { '@type'?: string } | undefined)?.['@type'] === 'Automatic'
}
/>
)}
{section.fields.map((sf) => {
const { formField, field, visible, enterpriseDisabled } = sf;
if (!visible) return null;
@@ -833,7 +845,7 @@ export function DynamicForm({ viewName, objectId }: DynamicFormProps) {
<div className="opacity-60">{widget}</div>
</TooltipTrigger>
<TooltipContent>
<p>{t('enterprise.featureDisabled', 'This feature isn\'t available on this server.')}</p>
<p>{t('enterprise.featureDisabled', "This feature isn't available on this server.")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>