fix(dns): o botao de reconciliar nao criava registo nenhum
O painel listava os registos em falta e oferecia um botao para os criar, mas esse botao chegava a um endpoint que so escrevia uma linha de auditoria e respondia "REQUESTED". Nunca tocou no DNS. Quem tinha o token da Cloudflare era o servico de protocolos, e a unica coisa capaz de o usar corria num temporizador -- para um dominio so, o de MAIL_DOMAIN. Por isso um dominio adicionado pela consola, como o cienciaembarcada.com.br, nunca era reconciliado: os registos nao apareciam e o unico sintoma visivel era a consola dizer que faltavam, para sempre. - POST /api/v1/admin/dns/reconcile publica os registos de um dominio, agora, e devolve o que foi criado, corrigido e o que ficou em conflito - o temporizador passa a percorrer todos os dominios ativos - conflitos sao reportados, nao resolvidos: um registo do mesmo tipo e nome com outro valor pode ser correio da zona ou um servico de que ninguem se lembra, e nao e deste servidor decidir sobrepo-lo - o erro do fornecedor fica no log, nao na resposta: pode nomear a conta ou a zona Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8227fe2b97
commit
2b369ab373
11 files changed
+497
-17
No files matched your search
@@ -91,7 +91,10 @@
|
||||
"unlockAccount": "Unlock",
|
||||
"verifyIntro": "Resolves every expected record against public resolvers and reports each one.",
|
||||
"watcherHealthy": "Certificate watcher",
|
||||
"openAdminShort": "Admin"
|
||||
"openAdminShort": "Admin",
|
||||
"reconcileSummary": "{created} created, {updated} corrected, {unchanged} already right.",
|
||||
"reconcileConflicts": "Left untouched — another value is already published:",
|
||||
"reconcileErrors": "The provider refused these:"
|
||||
},
|
||||
"auth": {
|
||||
"adminLoginTitle": "Admin console",
|
||||
|
||||
@@ -91,7 +91,10 @@
|
||||
"unlockAccount": "Desbloquear",
|
||||
"verifyIntro": "Resuelve cada registro esperado en resolutores públicos e informa uno por uno.",
|
||||
"watcherHealthy": "Monitor de certificado",
|
||||
"openAdminShort": "Admin"
|
||||
"openAdminShort": "Admin",
|
||||
"reconcileSummary": "{created} creados, {updated} corregidos, {unchanged} ya correctos.",
|
||||
"reconcileConflicts": "Sin tocar — ya hay otro valor publicado:",
|
||||
"reconcileErrors": "El proveedor rechazó estos:"
|
||||
},
|
||||
"auth": {
|
||||
"adminLoginTitle": "Consola de administración",
|
||||
|
||||
@@ -91,7 +91,10 @@
|
||||
"unlockAccount": "Desbloquear",
|
||||
"verifyIntro": "Resolve cada registro esperado em servidores DNS públicos e relata um a um.",
|
||||
"watcherHealthy": "Monitor de certificado",
|
||||
"openAdminShort": "Admin"
|
||||
"openAdminShort": "Admin",
|
||||
"reconcileSummary": "{created} criados, {updated} corrigidos, {unchanged} já corretos.",
|
||||
"reconcileConflicts": "Não alterados — já existe outro valor publicado:",
|
||||
"reconcileErrors": "O provedor recusou estes:"
|
||||
},
|
||||
"auth": {
|
||||
"adminLoginTitle": "Console de administração",
|
||||
|
||||
@@ -32,6 +32,15 @@ interface DnsRecordCheck {
|
||||
proxied: boolean;
|
||||
}
|
||||
|
||||
interface ReconcileResult {
|
||||
domain: string;
|
||||
created: number;
|
||||
updated: number;
|
||||
unchanged: number;
|
||||
conflicts?: string[];
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
interface DnsVerification {
|
||||
domain: string;
|
||||
status: string;
|
||||
@@ -70,6 +79,9 @@ export default function AdminDomainsPage() {
|
||||
|
||||
const [reconciling, setReconciling] = useState(false);
|
||||
const [verification, setVerification] = useState<DnsVerification | null>(null);
|
||||
// What the last reconciliation actually did, so the panel can say it rather
|
||||
// than leaving the operator to infer it from the verification that follows.
|
||||
const [reconcileResult, setReconcileResult] = useState<ReconcileResult | null>(null);
|
||||
const [onboardDomain, setOnboardDomain] = useState("");
|
||||
const [onboarding, setOnboarding] = useState(false);
|
||||
|
||||
@@ -112,13 +124,28 @@ export default function AdminDomainsPage() {
|
||||
setNotice(null);
|
||||
setError(null);
|
||||
setVerification(null);
|
||||
setReconcileResult(null);
|
||||
try {
|
||||
// Publish first, then look. The button used to verify and then call an
|
||||
// endpoint that only wrote an audit row, so it reported what was missing
|
||||
// and never created any of it - which is what made the panel feel like
|
||||
// it did nothing.
|
||||
const applied = await fetch(
|
||||
`/api/v1/admin/dns/reconcile?domain=${encodeURIComponent(selected.name)}`,
|
||||
{ method: "POST" },
|
||||
);
|
||||
const result = await applied.json().catch(() => ({}));
|
||||
if (!applied.ok) throw new Error(result.message);
|
||||
setReconcileResult(result);
|
||||
|
||||
// Then verify, so what is shown is the state after publishing rather
|
||||
// than the state that prompted it.
|
||||
const res = await fetch(`/api/v1/admin/dns/verify?domain=${encodeURIComponent(selected.name)}`);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.message);
|
||||
setVerification(data);
|
||||
// Reconciliation still runs, so the stored status and the audit trail
|
||||
// stay in step with what was just observed.
|
||||
|
||||
// The audit trail still records who asked for it.
|
||||
await fetch("/api/v1/admin/domains/reconcile", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -295,6 +322,41 @@ export default function AdminDomainsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* What the reconciliation did. Without this the panel showed only
|
||||
the verification that followed, so an operator could not tell
|
||||
whether anything had been published or the records had simply
|
||||
been there all along. */}
|
||||
{reconcileResult && (
|
||||
<div className="flex flex-col gap-2 rounded-xl bg-dark-card p-3.5 shadow-edge">
|
||||
<span className="text-[13px] text-slate-200">
|
||||
{t("admin.reconcileSummary", {
|
||||
created: reconcileResult.created,
|
||||
updated: reconcileResult.updated,
|
||||
unchanged: reconcileResult.unchanged,
|
||||
})}
|
||||
</span>
|
||||
{(reconcileResult.conflicts ?? []).length > 0 && (
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* Left untouched on purpose: a record of the right type
|
||||
and name already holding another value may be something
|
||||
the zone still needs. */}
|
||||
<span className="text-[12px] text-amber-300">{t("admin.reconcileConflicts")}</span>
|
||||
{(reconcileResult.conflicts ?? []).map((c) => (
|
||||
<span key={c} className="font-mono text-[11.5px] text-slate-400">{c}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(reconcileResult.errors ?? []).length > 0 && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[12px] text-rose-300">{t("admin.reconcileErrors")}</span>
|
||||
{(reconcileResult.errors ?? []).map((e) => (
|
||||
<span key={e} className="font-mono text-[11.5px] text-slate-400">{e}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{verification && (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
|
||||
Reference in new issue
Block a user