ligbox-ops-platform/deploy/vm112-wizard/frontend-dns-viewer-v4.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

123 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""Spec 037 V4 — painel DNS read-only no passo DNS do wizard (App.jsx). Idempotente."""
from pathlib import Path
APP = Path("/opt/ligbox-wizard/frontend/src/App.jsx")
text = APP.read_text(encoding="utf-8")
if "dnsViewerData" in text:
print("App.jsx: dns viewer V4 already patched")
raise SystemExit(0)
text = text.replace(
" const [showAdvancedDns, setShowAdvancedDns] = useState(false)",
" const [showAdvancedDns, setShowAdvancedDns] = useState(false)\n"
" const [dnsViewerData, setDnsViewerData] = useState(null)\n"
" const [dnsViewerLoading, setDnsViewerLoading] = useState(false)",
)
LOAD_FN = """
async function refreshDnsViewer(dom) {
const d = (dom || domain || '').trim().toLowerCase()
if (!d || d.length < 3) return
setDnsViewerLoading(true)
try {
const data = await api(`/onboarding/dns/viewer/${d}`)
setDnsViewerData(data)
} catch {
setDnsViewerData(null)
} finally {
setDnsViewerLoading(false)
}
}
"""
if "refreshDnsViewer" not in text:
text = text.replace(" async function choosePortalDns() {", LOAD_FN + " async function choosePortalDns() {")
# Refresh when instructions load
text = text.replace(
" setDnsResolve(data.resolve || null)",
" setDnsResolve(data.resolve || null)\n refreshDnsViewer(dom)",
)
text = text.replace(
" setDnsChoice('portal')\n setError(null)\n startBusy('dns_zone')",
" setDnsChoice('portal')\n setError(null)\n refreshDnsViewer(domain)\n startBusy('dns_zone')",
)
text = text.replace(
" setDnsChoice('external')\n markActionDone('chooseExternalDns')",
" setDnsChoice('external')\n refreshDnsViewer(domain)\n markActionDone('chooseExternalDns')",
)
PANEL = """
{(dnsViewerData || dnsViewerLoading) && step === 1 && (
<aside className="dns-viewer-wizard-panel" style={{
marginTop: '1rem',
padding: '0.85rem 1rem',
borderRadius: '10px',
border: '1px solid #dbe4f4',
background: '#f8fbff',
}}>
<strong>Apontamentos DNS (visualização)</strong>
{dnsViewerLoading && <p className="sub">Carregando…</p>}
{!dnsViewerLoading && dnsViewerData && (
<>
<p className="sub" style={{ margin: '0.35rem 0' }}>
{dnsViewerData.mode_message || dnsViewerData.mode_label}
</p>
<p className="sub">
Modo: <code>{dnsViewerData.mode_label || dnsViewerData.dns_mode}</code>
{' · '}
{dnsViewerData.summary?.total || 0} registo(s)
</p>
<table className="data-table" style={{ fontSize: '0.82rem', marginTop: '0.5rem' }}>
<thead>
<tr><th>Tipo</th><th>Nome</th><th>Conteúdo</th></tr>
</thead>
<tbody>
{(dnsViewerData.records || []).slice(0, 12).map((r, i) => (
<tr key={i}>
<td>{r.type}</td>
<td><code>{r.name}</code></td>
<td style={{ wordBreak: 'break-all' }}>{r.content}</td>
</tr>
))}
{!(dnsViewerData.records || []).length && (
<tr><td colSpan={3}>Sem registos — escolha o caminho DNS acima.</td></tr>
)}
</tbody>
</table>
{(dnsViewerData.planned_records || []).length > 0 && (
<>
<p className="sub" style={{ marginTop: '0.65rem' }}><strong>Serão aplicados (Ligbox):</strong></p>
<table className="data-table" style={{ fontSize: '0.82rem' }}>
<tbody>
{(dnsViewerData.planned_records || []).slice(0, 8).map((r, i) => (
<tr key={`p-${i}`}>
<td>{r.type}</td>
<td><code>{r.name}</code></td>
<td>{r.content}</td>
</tr>
))}
</tbody>
</table>
</>
)}
</>
)}
</aside>
)}
"""
if "dns-viewer-wizard-panel" not in text:
text = text.replace(
" {dnsChoice === 'byo' && showAdvancedDns && (",
PANEL + " {dnsChoice === 'byo' && showAdvancedDns && (",
)
APP.write_text(text, encoding="utf-8")
print("App.jsx: DNS viewer V4 panel OK")