ligbox-ops-platform/projects/ops-desk/frontend/assets/dns-viewer.js
Ligbox Spec Hub c1881f58e6 chore: sync Console SSO, DNS viewer, specs e infra docs pendentes
Inclui console handoff Desk↔Console (Spec 019), melhorias DNS Viewer (037),
OpenPanel/Nextcloud/VM116 deploy notes, contracts stack e sidebar actualizado.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 20:10:17 +00:00

218 lines
8.5 KiB
JavaScript

/**
* Spec 037-DNS-VIEWER — painel DNS unificado (Desk)
* Depende de: auth.js (api, esc)
*/
(function () {
function dnsPurposeLabel(purpose) {
return {
mx: 'MX',
spf: 'SPF',
dkim: 'DKIM',
dmarc: 'DMARC',
'mail-host': 'Mail host',
autodiscover: 'Autodiscover',
'mail-alias': 'Alias',
other: 'Outro',
}[purpose] || purpose || '—';
}
function sourceLabel(source) {
return {
cf_ligbox: 'CF Ligbox',
cf_byo: 'CF cliente',
public_resolver: 'Público',
planned_ligbox: 'Planeado',
openpanel_bind: 'OpenPanel',
}[source] || source || '—';
}
function checkIcon(c) {
if (!c) return '—';
if (c.ok) return '✅';
if (c.warn) return '⚠';
return '❌';
}
async function fetchDnsViewer(domain, emailService) {
const q = new URLSearchParams();
if (emailService != null) q.set('email_service', emailService ? 'true' : 'false');
const qs = q.toString();
return api(`/v1/dns/viewer/${encodeURIComponent(domain)}${qs ? `?${qs}` : ''}`);
}
async function fetchDomainDns(domain, emailService) {
try {
const viewer = await fetchDnsViewer(domain, emailService);
if (viewer && viewer.domain) return { ...viewer, _viewer: true };
} catch (e) {
if (e?.status === 404) {
/* DNS_VIEWER_ENABLED=0 — legado */
} else if (e?.status !== 403) {
console.warn('DnsViewer fallback:', e?.message || e);
}
}
try {
const legacy = await api(
`/v1/dns/cloudflare/records?domain=${encodeURIComponent(domain)}&email_service=${emailService ? 'true' : 'false'}`
);
return legacy;
} catch (e) {
return {
domain,
records: [],
email_records: [],
summary: { total: 0, email_related: 0 },
error: e.message || 'Falha ao carregar DNS',
};
}
}
function publicChecksRow(checks) {
if (!checks || !Object.keys(checks).length) return '';
const parts = ['mx', 'spf', 'dkim', 'dmarc'].map(
(k) => `${k.toUpperCase()} ${checkIcon(checks[k])}`
);
return `<p class="cf-dns-meta dns-viewer-checks">Verificação pública: ${parts.join(' · ')}</p>`;
}
function nsRow(data) {
const ns = data.nameservers || {};
const current = ns.current_public || [];
if (!current.length) return '';
const match = ns.match_ligbox;
const warn = data.display_mode === 'planned' || (data.dns_mode?.startsWith('ligbox_cf') && !match);
return `<p class="cf-dns-meta ${warn ? 'dns-viewer-ns-warn' : ''}">NS actuais: <code>${current.map((n) => esc(n)).join('</code> · <code>')}</code>${warn ? ' · ⚠ delegação Ligbox pendente' : match ? ' · ✅ Cloudflare' : ''}</p>`;
}
function recordRows(records, compact) {
return (records || []).map((r) => {
const src = r.source ? `<span class="dns-source-badge">${esc(sourceLabel(r.source))}</span>` : '';
const state = r.status === 'planned' ? ' <span class="badge review">planeado</span>' : '';
if (compact) {
return `
<tr class="${r.email_related ? 'dns-email-row' : ''}">
<td><span class="dns-purpose-badge purpose-${esc(r.purpose || 'other')}">${esc(dnsPurposeLabel(r.purpose))}</span></td>
<td><code>${esc(r.name)}</code></td>
<td><strong>${esc(r.type)}</strong></td>
<td class="dns-content">${esc(r.content)}</td>
</tr>`;
}
return `
<tr class="${r.email_related ? 'dns-email-row' : ''}">
<td><span class="dns-purpose-badge purpose-${esc(r.purpose || 'other')}">${esc(dnsPurposeLabel(r.purpose))}</span></td>
<td><code>${esc(r.name)}</code></td>
<td><strong>${esc(r.type)}</strong>${r.priority != null ? ` <span class="ticket-meta">prio ${r.priority}</span>` : ''}</td>
<td class="dns-content">${esc(r.content)}</td>
<td class="ticket-meta">${src}${state}${r.proxied != null ? (r.proxied ? ' · proxy' : ' · DNS only') : ''}</td>
</tr>`;
}).join('');
}
function editLinksHtml(links, domain) {
const parts = [];
if (typeof canReadDnsViewer === 'function' && canReadDnsViewer() && domain) {
parts.push(
`<button type="button" class="btn btn-ghost btn-sm js-console-dns" data-domain="${esc(domain)}">Abrir no Console ↗</button>`,
);
}
if (links?.length) {
parts.push(
...links.map(
(l) =>
`<a class="btn btn-ghost btn-sm" href="${esc(l.url)}" target="_blank" rel="noopener noreferrer">${esc(l.label)} ↗</a>`,
),
);
}
if (!parts.length) return '';
return `<div class="dns-viewer-actions">${parts.join(' ')}</div>`;
}
function renderDnsPanel(data, { compact = false, title = 'DNS do domínio' } = {}) {
if (!data) {
return compact
? '<p class="cf-dns-empty">Dados DNS indisponíveis.</p>'
: `<div class="modal-section"><h4>${esc(title)}</h4><p class="loading">Dados DNS indisponíveis.</p></div>`;
}
const isViewer = data._viewer || data.dns_mode;
if (!isViewer) {
return compact ? legacyInline(data) : legacyCard(data, title);
}
const summary = data.summary || {};
const zone = data.zone || {};
const rows = recordRows(data.records, compact);
const planned = (data.planned_records || []).length
? `<h5 class="dns-viewer-sub">Planeados (após NS)</h5><div class="cf-dns-table-wrap"><table class="data-table dns-records-table ${compact ? 'dns-records-table-compact' : ''}"><thead><tr><th>Função</th><th>Nome</th><th>Tipo</th><th>Conteúdo</th>${compact ? '' : '<th>Origem</th>'}</tr></thead><tbody>${recordRows(data.planned_records, compact) || '<tr><td colspan="5">—</td></tr>'}</tbody></table></div>`
: '';
const badgeCls = data.display_mode === 'planned' ? 'review' : data.dns_mode?.startsWith('ligbox_cf') ? 'onboard' : 'open';
const inner = `
<div class="dns-viewer-head">
<span class="badge ${badgeCls}">${esc(data.mode_label || data.dns_mode || 'DNS')}</span>
${data.email_service ? '<span class="badge onboard">E-mail</span>' : ''}
</div>
${data.mode_message ? `<p class="cf-dns-meta dns-viewer-msg">${esc(data.mode_message)}</p>` : ''}
${nsRow(data)}
<div class="cf-dns-inline-summary">
<div class="cf-metric-stat"><strong>${summary.total || 0}</strong><span>registos</span></div>
<div class="cf-metric-stat"><strong>${summary.email_related || 0}</strong><span>e-mail</span></div>
${summary.planned ? `<div class="cf-metric-stat"><strong>${summary.planned}</strong><span>planeados</span></div>` : ''}
</div>
${zone.name ? `<p class="cf-dns-meta">Zona CF <code>${esc(zone.name)}</code></p>` : ''}
${publicChecksRow(data.public_checks)}
<div class="cf-dns-table-wrap">
<table class="data-table dns-records-table ${compact ? 'dns-records-table-compact' : ''}">
<thead><tr><th>Função</th><th>Nome</th><th>Tipo</th><th>Conteúdo</th>${compact ? '' : '<th>Origem</th>'}</tr></thead>
<tbody>${rows || '<tr><td colspan="5">Sem registos para este domínio.</td></tr>'}</tbody>
</table>
</div>
${planned}
${editLinksHtml(data.edit_links, data.domain)}
${data.errors?.length ? `<p class="cf-dns-error">${esc(data.errors.join(' · '))}</p>` : ''}`;
if (compact) return inner;
return `
<div class="modal-section dns-records-section dns-viewer-section">
<div class="card-head-row">
<h4>${esc(title)}</h4>
</div>
${inner}
</div>`;
}
function legacyInline(dns) {
if (dns.error && !dns.records?.length) {
return `<p class="cf-dns-error">${esc(dns.error)}</p>`;
}
const rows = recordRows(dns.records, true);
const summary = dns.summary || {};
return `
<div class="cf-dns-inline-summary">
<div class="cf-metric-stat"><strong>${summary.total || 0}</strong><span>registos na zona</span></div>
<div class="cf-metric-stat"><strong>${summary.email_related || 0}</strong><span>para e-mail</span></div>
</div>
<div class="cf-dns-table-wrap">
<table class="data-table dns-records-table dns-records-table-compact">
<thead><tr><th>Função</th><th>Nome</th><th>Tipo</th><th>Conteúdo</th></tr></thead>
<tbody>${rows || '<tr><td colspan="4">Sem registos.</td></tr>'}</tbody>
</table>
</div>`;
}
function legacyCard(dns, title) {
return `
<div class="modal-section dns-records-section">
<h4>${esc(title)} (Cloudflare)</h4>
${legacyInline(dns)}
</div>`;
}
window.DnsViewer = {
fetch: fetchDnsViewer,
fetchDomainDns,
renderPanel: renderDnsPanel,
dnsPurposeLabel,
};
})();