ligbox-ops-platform/deploy/vm112-wizard/frontend-dns037.patch.py
Ligbox Spec Hub 038fb8f7ce feat(dns): Spec 037 DNS Viewer — Desk, Console e patches Wizard V4
Entrega read-only de apontamentos DNS (Cloudflare, OpenPanel BIND, público)
no Desk e Console /admin/dominio, com spec, scripts de rollback e patches
VM112 para painel lateral no passo DNS do onboarding (deploy wizard pendente).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 16:36:25 +00:00

315 lines
13 KiB
Python

#!/usr/bin/env python3
"""Patch App.jsx Spec 037 — multi-conta CF + BYO."""
from pathlib import Path
APP = Path("/opt/ligbox-wizard/frontend/src/App.jsx")
text = APP.read_text(encoding="utf-8")
if "chooseByoDns" in text and "dnsResolve" in text:
print("App.jsx already patched")
raise SystemExit(0)
text = text.replace(
" choosePortalDns: false,\n chooseExternalDns: false,",
" choosePortalDns: false,\n chooseByoDns: false,\n chooseExternalDns: false,",
)
text = text.replace(
" const [showAdvancedDns, setShowAdvancedDns] = useState(false)",
" const [showAdvancedDns, setShowAdvancedDns] = useState(false)\n const [dnsResolve, setDnsResolve] = useState(null)\n const [byoToken, setByoToken] = useState('')",
)
OLD_CHOOSE_PORTAL = """ async function choosePortalDns() {
setDnsChoice('portal')
setError(null)
startBusy('dns_zone')
try {
const guide = await api('/onboarding/dns/cloudflare/provision-zone', {
method: 'POST',
body: JSON.stringify({ domain }),
})
setPortalGuide(guide)
const cf = guide.status || (await refreshCfStatus())
setCfStatus(cf)
markActionDone('choosePortalDns')
if (guide.verification) {
setVerification(guide.verification)
setResult({ type: 'dns', data: { verification: guide.verification } })
markActionDone('verifyZone')
markActionDone('applyPortalDns')
} else if (cf?.zone_in_account && cf?.can_apply_mail_records) {
markActionDone('verifyZone')
const data = await api('/onboarding/dns/cloudflare/apply', {
method: 'POST',
body: JSON.stringify({
domain,
mail_aliases: sanitizeMailAliases(mailAliasInputs, domain),
}),
})
setVerification(data.verification)
setResult({ type: 'dns', data })
markActionDone('applyPortalDns')
}
} catch (e) {
setError(e.message)
setDnsChoice(null)
setActionsDone((prev) => ({ ...prev, choosePortalDns: false }))
} finally {
stopBusy()
}
}"""
NEW_CHOOSE_PORTAL = """ async function choosePortalDns() {
setDnsChoice('portal')
setError(null)
startBusy('dns_zone')
try {
const resolved = dnsResolve || (await api(`/onboarding/dns/resolve/${domain}`))
setDnsResolve(resolved)
if (!resolved.matched) {
throw new Error(
resolved.message ||
'Domínio não está nas contas Ligbox (ligit, itecnologys, ibytera).',
)
}
const cf = await refreshCfStatus()
setCfStatus(cf)
markActionDone('choosePortalDns')
markActionDone('verifyZone')
const data = await api('/onboarding/dns/cloudflare/apply', {
method: 'POST',
body: JSON.stringify({
domain,
zone_id: resolved.zone_id,
mail_aliases: sanitizeMailAliases(mailAliasInputs, domain),
}),
})
setPortalGuide({
message: resolved.message,
nameservers: cf?.nameservers || [],
status: cf,
ligbox_account_id: resolved.account_id,
})
setVerification(data.verification)
setResult({ type: 'dns', data })
markActionDone('applyPortalDns')
} catch (e) {
setError(e.message)
setDnsChoice(null)
setActionsDone((prev) => ({ ...prev, choosePortalDns: false }))
} finally {
stopBusy()
}
}
async function chooseByoDns() {
setDnsChoice('byo')
setError(null)
markActionDone('chooseByoDns')
setShowAdvancedDns(true)
}
async function connectByoAndApply() {
const token = byoToken.trim()
if (!token) {
setError('Cole o API Token da sua conta Cloudflare (DNS Edit na zona).')
return
}
setError(null)
startBusy('dns_byo')
try {
const conn = await api('/onboarding/dns/cloudflare/connect-custom', {
method: 'POST',
body: JSON.stringify({ domain, api_token: token }),
})
setDnsResolve({ ...conn, matched: true, dns_mode: 'byo_cloudflare' })
markActionDone('chooseByoDns')
const data = await api('/onboarding/dns/cloudflare/apply', {
method: 'POST',
body: JSON.stringify({
domain,
zone_id: conn.zone_id,
mail_aliases: sanitizeMailAliases(mailAliasInputs, domain),
}),
})
setVerification(data.verification)
setResult({ type: 'dns', data })
markActionDone('applyPortalDns')
} catch (e) {
setError(e.message)
setActionsDone((prev) => ({ ...prev, chooseByoDns: false, applyPortalDns: false }))
} finally {
stopBusy()
}
}"""
text = text.replace(OLD_CHOOSE_PORTAL, NEW_CHOOSE_PORTAL)
text = text.replace(
""" const DNS_ACTION_KEYS = {
choosePortalDns: false,
chooseExternalDns: false,""",
""" const DNS_ACTION_KEYS = {
choosePortalDns: false,
chooseByoDns: false,
chooseExternalDns: false,""",
)
text = text.replace(
" if (dnsChoice === 'portal') markActionDone('choosePortalDns')\n if (dnsChoice === 'external') markActionDone('chooseExternalDns')",
" if (dnsChoice === 'portal') markActionDone('choosePortalDns')\n if (dnsChoice === 'byo') markActionDone('chooseByoDns')\n if (dnsChoice === 'external') markActionDone('chooseExternalDns')",
)
text = text.replace(
" const data = await api(`/onboarding/dns/instructions/${dom}`)\n setInstructions(data)\n setCfStatus(data.cloudflare)",
" const data = await api(`/onboarding/dns/instructions/${dom}`)\n setInstructions(data)\n setCfStatus(data.cloudflare)\n setDnsResolve(data.resolve || null)",
)
text = text.replace(
" if (step !== 1 || !instructions || showAdvancedDns || dnsChoice === 'external' || asmMode) return",
" if (step !== 1 || !instructions || showAdvancedDns || dnsChoice === 'external' || dnsChoice === 'byo' || asmMode) return",
)
text = text.replace(
" if (portalDnsApplied || loading || autoDnsAttempted.current) return\n autoDnsAttempted.current = true",
" if (portalDnsApplied || loading || autoDnsAttempted.current) return\n const resolve = instructions?.resolve || dnsResolve\n if (resolve && !resolve.matched) return\n autoDnsAttempted.current = true",
)
text = text.replace(
" dns_mode: dnsChoice === 'portal' ? 'Cloudflare Ligbox' : 'Provedor externo',",
" dns_mode:\n dnsChoice === 'portal'\n ? dnsResolve?.dns_mode || cfStatus?.dns_mode || 'ligbox_cf'\n : dnsChoice === 'byo'\n ? 'byo_cloudflare'\n : 'external_registrar',",
)
# Advanced mode choice grid — add BYO button and update portal label
OLD_GRID = """ <ActionDoneButton
block
done={actionsDone.choosePortalDns}
label="Trazer DNS para o portal"
hint="Zona DNS Ligbox + apontamentos de email."
disabled={loading || actionsDone.chooseExternalDns}
busy={loading && !actionsDone.choosePortalDns}
onClick={choosePortalDns}
attentionId="choosePortalDns"
/>
<ActionDoneButton
block
secondary
done={actionsDone.chooseExternalDns}
label="Manter no provedor atual"
hint="Apontamentos manuais no provedor atual."
disabled={loading || actionsDone.choosePortalDns}
busy={loading && !actionsDone.chooseExternalDns}
onClick={chooseExternalDns}
attentionId="chooseExternalDns"
/>"""
NEW_GRID = """ <ActionDoneButton
block
done={actionsDone.choosePortalDns}
label={
dnsResolve?.matched
? `Cloudflare Ligbox (${dnsResolve.account_id})`
: 'Cloudflare Ligbox (indisponível)'
}
hint={
dnsResolve?.matched
? dnsResolve.message
: 'Domínio não está nas contas ligit / itecnologys / ibytera.'
}
disabled={
loading ||
actionsDone.chooseExternalDns ||
actionsDone.chooseByoDns ||
!dnsResolve?.matched
}
busy={loading && !actionsDone.choosePortalDns}
onClick={choosePortalDns}
attentionId="choosePortalDns"
/>
<ActionDoneButton
block
secondary
done={actionsDone.chooseByoDns}
label="Minha conta Cloudflare"
hint="API Token com DNS Edit na zona do domínio."
disabled={loading || actionsDone.choosePortalDns || actionsDone.chooseExternalDns}
busy={loading && !actionsDone.chooseByoDns}
onClick={chooseByoDns}
attentionId="chooseByoDns"
/>
<ActionDoneButton
block
secondary
done={actionsDone.chooseExternalDns}
label="Registrador / DNS externo"
hint="Apontamentos manuais (domínio a registar ou NS noutro sítio)."
disabled={loading || actionsDone.choosePortalDns || actionsDone.chooseByoDns}
busy={loading && !actionsDone.chooseExternalDns}
onClick={chooseExternalDns}
attentionId="chooseExternalDns"
/>"""
text = text.replace(OLD_GRID, NEW_GRID)
BYO_PANEL = """
{dnsChoice === 'byo' && showAdvancedDns && (
<div className="message" style={{ background: '#f4f0ff', color: '#3d2d6b' }}>
<strong>Sua conta Cloudflare (BYO)</strong>
<p className="sub" style={{ margin: '0.35rem 0 0' }}>
Crie um API Token com permissão <strong>Zone DNS Edit</strong> apenas na zona{' '}
<code>{domain}</code>.
</p>
<label className="sub" style={{ display: 'block', marginTop: '0.75rem' }}>
API Token Cloudflare
<input
type="password"
className="input"
style={{ marginTop: '0.35rem', width: '100%' }}
value={byoToken}
onChange={(e) => setByoToken(e.target.value)}
autoComplete="off"
/>
</label>
<div className="actions" style={{ marginTop: '0.75rem' }}>
<ActionDoneButton
done={portalDnsApplied}
label="Validar token e aplicar apontamentos"
disabled={loading || portalDnsApplied || !byoToken.trim()}
busy={loading && !portalDnsApplied}
onClick={connectByoAndApply}
attentionId="applyPortalDns"
/>
</div>
</div>
)}
"""
if "dnsChoice === 'byo'" not in text:
text = text.replace(
" {dnsChoice === 'external' && showAdvancedDns && (",
BYO_PANEL + " {dnsChoice === 'external' && showAdvancedDns && (",
)
# Simple mode message when not matched
SIMPLE_MSG = """
{!dnsResolve?.matched && !portalDnsApplied && (
<WizardStatusPanel variant="warn" icon={ShieldCheck} title="Escolha como apontar o DNS">
<p>
Este domínio não está nas contas Cloudflare Ligbox. Use{' '}
<strong>Minha conta Cloudflare</strong> ou{' '}
<strong>Registrador / DNS externo</strong> (botão técnico abaixo).
</p>
</WizardStatusPanel>
)}
"""
if "não está nas contas Cloudflare Ligbox" not in text:
text = text.replace(
" {portalDnsApplied && (",
SIMPLE_MSG + " {portalDnsApplied && (",
)
APP.write_text(text, encoding="utf-8")
print("App.jsx patched OK")