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:
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { getAccountId, jmapQueryAndGet } from '@/services/jmap/client';
|
||||
import { checkRecords, type LiveState } from './liveCheck';
|
||||
import type { ZoneRecord } from './zone';
|
||||
|
||||
export interface DnsTask {
|
||||
id: string;
|
||||
'@type': string;
|
||||
domainId?: string;
|
||||
status?: { '@type': string; failureReason?: string };
|
||||
}
|
||||
|
||||
const POLL_MS = 8000;
|
||||
const GIVE_UP_MS = 10 * 60_000;
|
||||
|
||||
/**
|
||||
* Keep checking a set of records in public DNS, every few seconds for ten
|
||||
* minutes, and (for automatic DNS) the server's publishing task for the
|
||||
* domain. `task` is undefined until first read, null when none is queued.
|
||||
*/
|
||||
export function useRecordChecks(records: ZoneRecord[], domainId: string, watchTask: boolean) {
|
||||
const [states, setStates] = useState<Map<ZoneRecord, LiveState>>(new Map());
|
||||
const [task, setTask] = useState<DnsTask | null | undefined>(undefined);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [lastChecked, setLastChecked] = useState<Date | null>(null);
|
||||
const started = useRef<number | null>(null);
|
||||
|
||||
const check = useCallback(async () => {
|
||||
setChecking(true);
|
||||
if (watchTask) {
|
||||
try {
|
||||
const [, getRes] = await jmapQueryAndGet('x:Task', getAccountId('x:Task'), {}, ['@type', 'domainId', 'status']);
|
||||
const list = ((getRes?.[1] as { list?: DnsTask[] })?.list ?? []).filter(
|
||||
(x) => x['@type'] === 'DnsManagement' && x.domainId === domainId,
|
||||
);
|
||||
setTask(list[0] ?? null);
|
||||
} catch {
|
||||
setTask(null);
|
||||
}
|
||||
}
|
||||
setStates(await checkRecords(records));
|
||||
setLastChecked(new Date());
|
||||
setChecking(false);
|
||||
}, [domainId, records, watchTask]);
|
||||
|
||||
useEffect(() => {
|
||||
// Checking DNS and the task list syncs with outside systems, so state
|
||||
// lands from their callbacks, never synchronously in the effect.
|
||||
const first = setTimeout(() => {
|
||||
started.current = Date.now();
|
||||
void check();
|
||||
}, 0);
|
||||
const timer = setInterval(() => {
|
||||
if (started.current !== null && Date.now() - started.current > GIVE_UP_MS) return;
|
||||
void check();
|
||||
}, POLL_MS);
|
||||
return () => {
|
||||
clearTimeout(first);
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [check]);
|
||||
|
||||
const liveCount = records.filter((r) => states.get(r) === 'live').length;
|
||||
return {
|
||||
states,
|
||||
task,
|
||||
checking,
|
||||
lastChecked,
|
||||
check,
|
||||
liveCount,
|
||||
allLive: records.length > 0 && liveCount === records.length,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user