ligbox-ops-platform/projects/ops-desk/frontend/assets/domain-orchestration.js
Ligbox Spec Hub 821675ab4a Reorganize monorepo into projects/wizard, ops-desk, finance
Specs stay at repo root (cross-VM). Move deploy and code into logical
projects with README per domain, updated manifest.yaml, and symlinks at
legacy paths for VM122 backward compatibility.
2026-06-19 18:55:03 +00:00

270 lines
12 KiB
JavaScript

/**
* Domínios VM112 — Account Home card + modal (Spec 017)
*/
const DeskVm112Domains = (() => {
const API_BASE = '/api';
let _domains = [];
let _query = '';
function canManage() {
return typeof canRunAudit === 'function' && canRunAudit();
}
function isEnabled() {
return window.DeskModules?.isEnabled('vm112-domains') !== false;
}
function esc(s) {
return String(s ?? '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function filtered() {
const q = _query.trim().toLowerCase();
if (!q) return _domains;
return _domains.filter((d) => {
const blob = [
d.domain,
d.portal_admin_email,
(d.accounts_preview || []).join(' '),
(d.portal_users || []).map((u) => u.login_id).join(' '),
].join(' ').toLowerCase();
return blob.includes(q);
});
}
function statusBadges(d) {
const parts = [];
parts.push(d.carbonio_exists
? '<span class="badge badge-ok">Carbonio</span>'
: '<span class="badge badge-warn">sem CD</span>');
parts.push(d.site_folder_exists
? '<span class="badge badge-ok">site</span>'
: '<span class="badge badge-muted">sem pasta</span>');
parts.push(`<span class="badge">${d.account_count != null ? d.account_count : (d.carbonio_exists ? 'CD' : '0')} contas</span>`);
return parts.join(' ');
}
function cardHtml() {
if (!canManage() || !isEnabled()) return '';
const rows = filtered()
.map((d) => `
<button type="button" class="cf-domain-row vm112-domain-row" data-vm112-domain="${esc(d.domain)}">
<span class="cf-domain-status ${d.carbonio_exists ? 'ok' : 'warn'}"></span>
<div class="cf-domain-main">
<strong>${esc(d.domain)}</strong>
<span>${esc(d.portal_admin_email || '—')} · ${esc(d.mail_host || '')}</span>
</div>
<span class="cf-domain-metric vm112-domain-badges">${statusBadges(d)}</span>
</button>`)
.join('');
return `
<div class="cf-panel vm112-domains-panel" id="vm112-domains-panel">
<div class="cf-panel-head">
<h3>Domínios orquestrados (VM112)</h3>
<div class="cf-panel-actions">
<input type="search" id="vm112-domains-search" class="cf-select vm112-domains-search"
placeholder="Pesquisar domínio, e-mail, login…" value="${esc(_query)}"/>
<button type="button" class="cf-icon-btn" id="vm112-domains-refresh" title="Actualizar">↻</button>
</div>
</div>
<div class="cf-panel-body" id="vm112-domains-list">
${rows || '<p class="cf-empty">Nenhum domínio encontrado na VM112.</p>'}
</div>
<p class="ticket-meta vm112-domains-foot">${filtered().length} / ${_domains.length} domínio(s) · Admin only</p>
</div>`;
}
async function loadDomains() {
const res = await fetchWithTimeout(`${API_BASE}/v1/vm112/domains`, {
headers: authHeaders({ 'Content-Type': 'application/json' }),
}, 120000);
if (res.status === 401) { logout(); throw new Error('sessão expirada'); }
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || `${res.status} /v1/vm112/domains`);
}
const data = await res.json();
_domains = data.domains || [];
return _domains;
}
function bindCard(root) {
if (!root) return;
root.querySelector('#vm112-domains-search')?.addEventListener('input', (e) => {
_query = e.target.value;
const list = root.querySelector('#vm112-domains-list');
const panel = root.querySelector('#vm112-domains-panel');
if (list && panel) {
const foot = panel.querySelector('.vm112-domains-foot');
const html = filtered().map((d) => `
<button type="button" class="cf-domain-row vm112-domain-row" data-vm112-domain="${esc(d.domain)}">
<span class="cf-domain-status ${d.carbonio_exists ? 'ok' : 'warn'}"></span>
<div class="cf-domain-main">
<strong>${esc(d.domain)}</strong>
<span>${esc(d.portal_admin_email || '—')} · ${esc(d.mail_host || '')}</span>
</div>
<span class="cf-domain-metric vm112-domain-badges">${statusBadges(d)}</span>
</button>`).join('');
list.innerHTML = html || '<p class="cf-empty">Nenhum resultado.</p>';
if (foot) foot.textContent = `${filtered().length} / ${_domains.length} domínio(s) · Admin only`;
list.querySelectorAll('[data-vm112-domain]').forEach((btn) => {
btn.addEventListener('click', () => openModal(btn.dataset.vm112Domain));
});
}
});
root.querySelector('#vm112-domains-refresh')?.addEventListener('click', async () => {
const list = root.querySelector('#vm112-domains-list');
if (list) list.innerHTML = '<p class="cf-empty">A carregar VM112…</p>';
try {
await loadDomains();
await injectCard(root.closest('.cf-home') || root);
} catch (e) {
if (list) list.innerHTML = `<p class="cf-empty">Erro: ${esc(e.message)}</p>`;
}
});
root.querySelectorAll('[data-vm112-domain]').forEach((btn) => {
btn.addEventListener('click', () => openModal(btn.dataset.vm112Domain));
});
}
async function injectCard(cfHome) {
if (!cfHome || !canManage() || !isEnabled()) return;
const existing = cfHome.querySelector('#vm112-domains-panel');
if (existing) existing.remove();
const grid = cfHome.querySelector('.cf-grid-2');
const loading = document.createElement('div');
loading.id = 'vm112-domains-panel';
loading.className = 'cf-panel vm112-domains-panel';
loading.innerHTML = '<div class="cf-panel-head"><h3>Domínios orquestrados (VM112)</h3></div><div class="cf-panel-body"><p class="cf-empty">A carregar lista VM112 (pode demorar ~1 min)…</p></div>';
if (grid) grid.before(loading);
try {
if (!_domains.length) await loadDomains();
} catch (e) {
loading.innerHTML = `<div class="cf-panel-head"><h3>Domínios orquestrados (VM112)</h3></div><div class="cf-panel-body"><p class="cf-empty">Erro: ${esc(e.message)}</p></div>`;
return;
}
loading.remove();
const wrap = document.createElement('div');
wrap.innerHTML = cardHtml();
const card = wrap.firstElementChild;
const grid = cfHome.querySelector('.cf-grid-2');
if (grid) grid.before(card);
else cfHome.appendChild(card);
bindCard(card);
}
function closeModal() {
const modal = document.getElementById('vm112-domain-modal');
if (!modal) return;
modal.classList.add('hidden');
modal.setAttribute('aria-hidden', 'true');
}
function openModal(domain) {
const modal = document.getElementById('vm112-domain-modal');
const body = document.getElementById('vm112-domain-modal-body');
const title = document.getElementById('vm112-domain-modal-title');
const sub = document.getElementById('vm112-domain-modal-sub');
if (!modal || !body) return;
modal.classList.remove('hidden');
modal.setAttribute('aria-hidden', 'false');
title.textContent = domain;
sub.textContent = 'A carregar detalhe VM112…';
body.innerHTML = '<p class="loading">A carregar…</p>';
loadModal(domain);
modal.querySelector('[data-close-vm112-modal]')?.addEventListener('click', closeModal, { once: true });
modal.querySelector('.modal-backdrop')?.addEventListener('click', closeModal, { once: true });
}
async function loadModal(domain) {
const body = document.getElementById('vm112-domain-modal-body');
const sub = document.getElementById('vm112-domain-modal-sub');
try {
const d = await fetchWithTimeout(`${API_BASE}/v1/vm112/domains/${encodeURIComponent(domain)}`, {
headers: authHeaders({ 'Content-Type': 'application/json' }),
}, 120000).then(async (res) => {
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || `${res.status}`);
}
return res.json();
});
sub.textContent = `${d.account_count || 0} conta(s) · ${d.mail_host || ''}`;
const steps = (d.infra_status?.steps || [])
.map((s) => `<li class="${s.ok ? 'ok' : 'fail'}"><strong>${esc(s.label)}</strong> — ${esc(s.message)}</li>`)
.join('');
const accounts = (d.accounts || d.accounts_preview || [])
.map((a) => `<li>${esc(a)}</li>`).join('') || '<li class="muted">Nenhuma</li>';
const cf = d.cloudflare_zone;
const cfTxt = cf?.name
? `Zona ${cf.name} (${cf.status || '—'})`
: (cf?.error ? `Erro CF: ${cf.error}` : 'Sem zona na conta Ibytera');
body.innerHTML = `
<div class="modal-section">
<h4>Resumo</h4>
<p><strong>Admin portal:</strong> ${esc(d.portal_admin_email || '—')}</p>
<p><strong>Carbonio:</strong> ${d.carbonio_exists ? 'ativo' : 'ausente'} · <strong>Pasta site:</strong> ${d.site_folder_exists ? 'sim' : 'não'}</p>
<p><strong>Cloudflare:</strong> ${esc(cfTxt)}</p>
</div>
<div class="modal-section">
<h4>Infraestrutura</h4>
<ul class="vm112-infra-steps">${steps || '<li>—</li>'}</ul>
</div>
<div class="modal-section">
<h4>Contas Carbonio</h4>
<ul>${accounts}</ul>
</div>
<div class="modal-section vm112-purge-zone">
<h4>Zona perigosa — Purge completo</h4>
<p class="vm112-purge-warn">Remove domínio Carbonio, contas, pasta ligbox-sites, zona Cloudflare, Traefik/SNI e registos Desk. <strong>Irreversível.</strong> Uso principal: limpar testes.</p>
<label>Confirmar domínio (digite exactamente)</label>
<input type="text" id="vm112-purge-confirm" class="vm112-purge-input" placeholder="${esc(domain)}" autocomplete="off"/>
<label>Senha Root (Desk)</label>
<input type="password" id="vm112-purge-root-pwd" class="vm112-purge-input" autocomplete="current-password"/>
<button type="button" class="btn btn-danger" id="vm112-purge-btn">Apagar domínio e todos os dados</button>
<p id="vm112-purge-msg" class="ticket-meta"></p>
</div>`;
body.querySelector('#vm112-purge-btn')?.addEventListener('click', () => runPurge(domain));
} catch (e) {
body.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
}
}
async function runPurge(domain) {
const msg = document.getElementById('vm112-purge-msg');
const confirmDomain = document.getElementById('vm112-purge-confirm')?.value?.trim() || '';
const rootPassword = document.getElementById('vm112-purge-root-pwd')?.value || '';
if (!confirmDomain || !rootPassword) {
if (msg) msg.textContent = 'Preencha confirmação do domínio e senha Root.';
return;
}
if (!window.confirm(`PURGE definitivo de ${domain}? Esta ação não pode ser desfeita.`)) return;
const btn = document.getElementById('vm112-purge-btn');
if (btn) { btn.disabled = true; btn.textContent = 'A apagar…'; }
if (msg) msg.textContent = 'A executar purge VM112 + Desk…';
try {
const res = await api(`/v1/vm112/domains/${encodeURIComponent(domain)}/purge`, {
method: 'POST',
body: JSON.stringify({ confirm_domain: confirmDomain, root_password: rootPassword }),
});
if (msg) msg.textContent = `Concluído. Desk: ${JSON.stringify(res.desk || {})}`;
_domains = _domains.filter((d) => d.domain !== domain);
setTimeout(() => {
closeModal();
const panel = document.getElementById('vm112-domains-panel');
if (panel) document.getElementById('vm112-domains-refresh')?.click();
}, 1500);
} catch (e) {
if (msg) msg.textContent = e.message || 'Purge falhou';
if (btn) { btn.disabled = false; btn.textContent = 'Apagar domínio e todos os dados'; }
}
}
return { injectCard, loadDomains, openModal, canManage, isEnabled };
})();
window.DeskVm112Domains = DeskVm112Domains;