Files
inbuxa-admin/src/features/dns/useRecordChecks.ts
T
jcoffey-dev 5641560a91 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.
2026-09-19 01:41:00 -07:00

80 lines
2.5 KiB
TypeScript

/*
* 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,
};
}