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.
270 lines
12 KiB
JavaScript
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, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
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;
|