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>
632 lines
28 KiB
HTML
632 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="pt-BR">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
<title>Ligbox — Área Gerente de Domínio (Sandbox)</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&display=swap" rel="stylesheet"/>
|
|
<style>
|
|
:root {
|
|
--bg: #f5f1eb;
|
|
--surface: #fffdf9;
|
|
--surface-2: #f8f4ee;
|
|
--border: #ddd4c8;
|
|
--ink: #2a2520;
|
|
--muted: #6b6560;
|
|
--accent: #5c2e2e;
|
|
--accent-soft: #f3e8e8;
|
|
--ok: #1a6648;
|
|
--ok-bg: #dff0e8;
|
|
--warn: #a05a18;
|
|
--warn-bg: #faecd8;
|
|
--mail: #1a6b5c;
|
|
--mail-bg: #e8f5f1;
|
|
--files: #2a5298;
|
|
--files-bg: #eaf0fa;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
font-family: 'DM Sans', system-ui, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--ink);
|
|
min-height: 100vh;
|
|
}
|
|
.demo-banner {
|
|
background: #a05a18;
|
|
color: #fff;
|
|
text-align: center;
|
|
padding: 0.55rem 1rem;
|
|
font-size: 0.82rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.03em;
|
|
border-bottom: 3px solid #7a4412;
|
|
}
|
|
.demo-banner strong { font-size: 0.95rem; }
|
|
.sandbox-bar {
|
|
background: var(--accent);
|
|
color: #fff;
|
|
text-align: center;
|
|
padding: 0.45rem 1rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
.sandbox-bar strong { font-weight: 700; }
|
|
.app { max-width: 1200px; margin: 0 auto; padding: 1.25rem 1rem 2rem; }
|
|
.topbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
margin-bottom: 1.25rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
.brand { font-size: 1.1rem; font-weight: 700; letter-spacing: -0.02em; }
|
|
.brand span { color: var(--muted); font-weight: 500; font-size: 0.85rem; margin-left: 0.5rem; }
|
|
.domain-pill {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
padding: 0.35rem 0.75rem;
|
|
border-radius: 999px;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
}
|
|
.cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: 0.75rem;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 1rem;
|
|
}
|
|
.card-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin-bottom: 0.35rem; }
|
|
.card-value { font-size: 1.15rem; font-weight: 700; }
|
|
.card-sub { font-size: 0.75rem; color: var(--muted); margin-top: 0.25rem; }
|
|
.badge-ok { display: inline-block; background: var(--ok-bg); color: var(--ok); font-size: 0.72rem; font-weight: 600; padding: 0.15rem 0.5rem; border-radius: 4px; }
|
|
.layout {
|
|
display: grid;
|
|
grid-template-columns: 220px 1fr;
|
|
gap: 1rem;
|
|
align-items: start;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.layout { grid-template-columns: 1fr; }
|
|
.nav { display: flex; flex-wrap: wrap; gap: 0.35rem; }
|
|
.nav button { flex: 1 1 auto; border-radius: 8px !important; border-right: 1px solid var(--border) !important; }
|
|
}
|
|
.nav {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
}
|
|
.nav button {
|
|
display: block;
|
|
width: 100%;
|
|
text-align: left;
|
|
border: none;
|
|
border-bottom: 1px solid var(--border);
|
|
background: transparent;
|
|
padding: 0.65rem 1rem;
|
|
font: inherit;
|
|
font-size: 0.82rem;
|
|
cursor: pointer;
|
|
color: var(--muted);
|
|
}
|
|
.nav button:last-child { border-bottom: none; }
|
|
.nav button.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
.nav button:hover:not(.active) { background: var(--surface-2); }
|
|
.panel {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 1.25rem;
|
|
min-height: 420px;
|
|
}
|
|
.panel h2 { margin: 0 0 0.35rem; font-size: 1.15rem; }
|
|
.panel p.desc { margin: 0 0 1rem; font-size: 0.82rem; color: var(--muted); }
|
|
table { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
|
|
th, td { text-align: left; padding: 0.55rem 0.5rem; border-bottom: 1px solid var(--border); }
|
|
th { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
|
|
tr:last-child td { border-bottom: none; }
|
|
.btn {
|
|
border: 1px solid var(--border);
|
|
background: var(--surface-2);
|
|
padding: 0.45rem 0.85rem;
|
|
border-radius: 6px;
|
|
font: inherit;
|
|
font-size: 0.8rem;
|
|
cursor: pointer;
|
|
}
|
|
.btn:hover { background: var(--border); }
|
|
.btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
.btn-primary:hover { opacity: 0.92; }
|
|
.btn-sm { padding: 0.25rem 0.5rem; font-size: 0.72rem; }
|
|
.btn-danger { color: #9b2c2c; border-color: #e8c4c4; background: #fdf5f5; }
|
|
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-wrap: wrap; gap: 0.5rem; }
|
|
.progress-wrap { margin: 0.75rem 0; }
|
|
.progress-label { display: flex; justify-content: space-between; font-size: 0.75rem; margin-bottom: 0.25rem; }
|
|
.progress-bar { height: 8px; background: var(--surface-2); border-radius: 4px; overflow: hidden; }
|
|
.progress-fill { height: 100%; background: var(--mail); border-radius: 4px; transition: width 0.3s; }
|
|
.dmarc-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; }
|
|
.dmarc-item { background: var(--surface-2); border-radius: 8px; padding: 1rem; text-align: center; }
|
|
.dmarc-item .icon { font-size: 1.5rem; margin-bottom: 0.35rem; }
|
|
.dns-row { display: flex; justify-content: space-between; align-items: center; padding: 0.65rem 0; border-bottom: 1px solid var(--border); font-size: 0.82rem; }
|
|
.modal-backdrop {
|
|
position: fixed; inset: 0; background: rgba(42,37,32,0.45);
|
|
display: none; align-items: center; justify-content: center; z-index: 100; padding: 1rem;
|
|
}
|
|
.modal-backdrop.open { display: flex; }
|
|
.modal {
|
|
background: var(--surface); border-radius: 12px; max-width: 440px; width: 100%;
|
|
border: 1px solid var(--border); padding: 1.25rem;
|
|
}
|
|
.modal h3 { margin: 0 0 1rem; font-size: 1rem; }
|
|
.field { margin-bottom: 0.85rem; }
|
|
.field label { display: block; font-size: 0.72rem; font-weight: 600; color: var(--muted); margin-bottom: 0.3rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
.field input, .field select {
|
|
width: 100%; padding: 0.5rem 0.65rem; border: 1px solid var(--border);
|
|
border-radius: 6px; font: inherit; font-size: 0.85rem; background: #fff;
|
|
}
|
|
.field-row { display: flex; gap: 0.5rem; align-items: center; }
|
|
.field-row input { flex: 1; }
|
|
.field-suffix { font-size: 0.82rem; color: var(--muted); white-space: nowrap; }
|
|
.modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1rem; }
|
|
.toast {
|
|
position: fixed; bottom: 1.25rem; right: 1.25rem; max-width: 340px;
|
|
background: var(--ink); color: #fff; padding: 0.75rem 1rem; border-radius: 8px;
|
|
font-size: 0.8rem; z-index: 200; opacity: 0; transform: translateY(8px);
|
|
transition: opacity 0.25s, transform 0.25s; pointer-events: none;
|
|
}
|
|
.toast.show { opacity: 1; transform: translateY(0); }
|
|
.hidden { display: none !important; }
|
|
.protected-tag { font-size:0.65rem;background:var(--warn-bg);color:var(--warn);padding:0.1rem 0.35rem;border-radius:3px;margin-left:0.25rem; }
|
|
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-top: 1rem; }
|
|
.stat-box { background: var(--surface-2); border-radius: 8px; padding: 0.85rem; }
|
|
.stat-box.mail { border-left: 3px solid var(--mail); }
|
|
.stat-box.files { border-left: 3px solid var(--files); }
|
|
.upgrade-box {
|
|
background: var(--accent-soft); border: 1px dashed #d4a8a8;
|
|
border-radius: 8px; padding: 1rem; margin-top: 1rem; font-size: 0.82rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="demo-banner" id="demo-site-banner">
|
|
<strong>DEMO SITE</strong> — Ambiente de demonstração Ligbox · não usar para produção real
|
|
<div id="demo-creds" style="font-size:0.72rem;font-weight:500;margin-top:0.35rem;opacity:0.95">
|
|
Desk API: <code>admin</code> / <code>Demo805353</code> ·
|
|
Gerente email: <code>admin@cenario-demo.ops.ligbox.com.br</code> / <code>Demo805353</code> ·
|
|
Webmail: <a href="https://mail.cenario-demo.ops.ligbox.com.br/" style="color:#fff" target="_blank">mail.cenario-demo.ops.ligbox.com.br</a>
|
|
</div>
|
|
</div>
|
|
<div class="sandbox-bar" id="mode-bar">
|
|
<strong id="mode-label">SANDBOX MOCK</strong> — <span id="mode-desc">dados fictícios · zero API</span>
|
|
<span style="margin-left:1rem">
|
|
<button type="button" class="btn btn-sm" id="btn-mode-mock" style="margin-right:0.35rem">Mock</button>
|
|
<button type="button" class="btn btn-sm" id="btn-mode-live">Live create-only</button>
|
|
</span>
|
|
</div>
|
|
<div class="sandbox-bar hidden" id="live-config" style="background:#1a6b5c;font-size:0.72rem">
|
|
API Desk: <input id="api-base" value="https://desk.ligbox.com.br" style="width:240px;padding:2px 6px;border-radius:4px;border:none"/>
|
|
Token: <input id="api-token" type="password" placeholder="Bearer JWT Desk" style="width:280px;padding:2px 6px;border-radius:4px;border:none"/>
|
|
<button type="button" class="btn btn-sm" id="btn-new-scenario" style="margin-left:0.5rem">+ Cenário real</button>
|
|
</div>
|
|
|
|
<div class="app">
|
|
<header class="topbar">
|
|
<div class="brand">Ligbox <span>Área Gerente de Domínio</span></div>
|
|
<div class="domain-pill" id="domain-label">empresa.com.br</div>
|
|
</header>
|
|
|
|
<div class="cards" id="summary-cards"></div>
|
|
|
|
<div class="layout">
|
|
<nav class="nav" id="nav"></nav>
|
|
<main class="panel" id="panel"></main>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-backdrop" id="modal-account">
|
|
<div class="modal">
|
|
<h3>Nova conta de email</h3>
|
|
<div class="field">
|
|
<label>Email</label>
|
|
<div class="field-row">
|
|
<input type="text" id="new-local" placeholder="vendas"/>
|
|
<span class="field-suffix" id="new-domain-suffix">@empresa.com.br</span>
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label>Nome</label>
|
|
<input type="text" id="new-name" placeholder="Vendas"/>
|
|
</div>
|
|
<div class="field">
|
|
<label>Quota mail (GB)</label>
|
|
<select id="new-mail-gb"></select>
|
|
</div>
|
|
<div class="field">
|
|
<label>Quota Nextcloud (GB)</label>
|
|
<select id="new-files-gb"></select>
|
|
</div>
|
|
<div class="field">
|
|
<label><input type="checkbox" id="new-nc" checked/> Criar Nextcloud Files</label>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn" id="modal-cancel">Cancelar</button>
|
|
<button type="button" class="btn btn-primary" id="modal-save">Criar conta (simulado)</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script>
|
|
/* ── MOCK (default) ou LIVE create-only via Desk API ── */
|
|
const MODE = { kind: 'mock', scenarioId: 'c31581f3-a6fd-456f-907c-eb03b6728aed', apiBase: '', token: '' };
|
|
|
|
const STATE = {
|
|
domain: 'empresa.com.br',
|
|
plan: { name: 'Business', price: 549, maxSeats: 25, mailGbDefault: 30, filesGbDefault: 200 },
|
|
billing: { status: 'Em dia', nextRenewal: '2026-07-21' },
|
|
dmarc: { spf: true, dkim: true, dmarc: true, score: 98 },
|
|
dns: { mx: true, aMail: true, subdomain: 'intranet.empresa.com.br' },
|
|
accounts: [
|
|
{ email: 'admin@empresa.com.br', name: 'Administrador', mailGb: 30, filesGb: 200, nc: true, active: true, role: 'gerente' },
|
|
{ email: 'vendas@empresa.com.br', name: 'Vendas', mailGb: 30, filesGb: 200, nc: true, active: true },
|
|
{ email: 'financeiro@empresa.com.br', name: 'Financeiro', mailGb: 30, filesGb: 150, nc: true, active: true },
|
|
{ email: 'suporte@empresa.com.br', name: 'Suporte', mailGb: 20, filesGb: 100, nc: true, active: true },
|
|
],
|
|
section: 'overview',
|
|
};
|
|
|
|
const SECTIONS = [
|
|
{ id: 'overview', label: 'Visão geral' },
|
|
{ id: 'accounts', label: 'Contas de email' },
|
|
{ id: 'files', label: 'Nextcloud / Files' },
|
|
{ id: 'dmarc', label: 'Certificação mail' },
|
|
{ id: 'dns', label: 'Domínio & DNS' },
|
|
{ id: 'billing', label: 'Plano & faturação' },
|
|
];
|
|
|
|
function seatsUsed() { return STATE.accounts.filter(a => a.active).length; }
|
|
function toast(msg) {
|
|
const el = document.getElementById('toast');
|
|
el.textContent = msg;
|
|
el.classList.add('show');
|
|
setTimeout(() => el.classList.remove('show'), 4200);
|
|
}
|
|
|
|
async function apiLive(path, opts = {}) {
|
|
const base = (document.getElementById('api-base').value || '').replace(/\/$/, '');
|
|
const token = document.getElementById('api-token').value.trim();
|
|
if (!base || !token) throw new Error('Configure API Desk + token JWT');
|
|
const r = await fetch(base + path, {
|
|
...opts,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: 'Bearer ' + token,
|
|
...(opts.headers || {}),
|
|
},
|
|
});
|
|
const text = await r.text();
|
|
let data;
|
|
try { data = JSON.parse(text); } catch { data = { detail: text }; }
|
|
if (!r.ok) throw new Error(data.detail || data.error || `HTTP ${r.status}`);
|
|
return data;
|
|
}
|
|
|
|
function applyLiveState(payload) {
|
|
STATE.domain = payload.domain;
|
|
STATE.plan = {
|
|
name: payload.plan.name,
|
|
price: payload.plan.price || 549,
|
|
maxSeats: payload.plan.maxSeats || 25,
|
|
mailGbDefault: payload.plan.mailGbDefault || 30,
|
|
filesGbDefault: payload.plan.filesGbDefault || 200,
|
|
};
|
|
STATE.billing = payload.billing || { status: 'Sandbox', nextRenewal: '—' };
|
|
STATE.dmarc = payload.dmarc || STATE.dmarc;
|
|
STATE.dns = payload.dns || STATE.dns;
|
|
STATE.accounts = (payload.accounts || []).map(a => ({
|
|
email: a.email,
|
|
name: a.name || a.email.split('@')[0],
|
|
mailGb: a.mail_gb || 30,
|
|
filesGb: a.files_gb || 200,
|
|
nc: a.nc !== false,
|
|
active: a.active !== false,
|
|
role: a.email.startsWith('admin@') ? 'gerente' : undefined,
|
|
protected: a.protected,
|
|
sandboxCreated: a.sandbox_created,
|
|
}));
|
|
document.getElementById('domain-label').textContent = STATE.domain;
|
|
}
|
|
|
|
async function loadLiveScenario() {
|
|
if (!MODE.scenarioId) return;
|
|
const data = await apiLive(`/api/v1/domain-console/sandbox/scenarios/${MODE.scenarioId}/state`);
|
|
applyLiveState(data);
|
|
render();
|
|
toast('Estado live carregado — delete bloqueado');
|
|
}
|
|
|
|
async function createLiveScenario() {
|
|
const label = prompt('Nome do cenário (ex: Demo Cliente X):', 'Demo Roger');
|
|
if (!label) return;
|
|
const cfg = await apiLive('/api/v1/domain-console/sandbox/config');
|
|
const res = await apiLive('/api/v1/domain-console/sandbox/scenarios', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
label,
|
|
domain: 'cenario-demo.ops.ligbox.com.br',
|
|
admin_password: 'Demo805353',
|
|
}),
|
|
});
|
|
MODE.scenarioId = res.scenario.id;
|
|
alert(
|
|
`Cenário REAL criado em produção (create-only):\n\n` +
|
|
`Domínio: ${res.scenario.domain}\n` +
|
|
`Admin: ${res.credentials.admin_email}\n` +
|
|
`Senha: ${res.credentials.password}\n\n` +
|
|
`Webmail: ${res.credentials.webmail}\n\n` +
|
|
`Nada existente foi apagado. Delete continua bloqueado.`
|
|
);
|
|
applyLiveState(await apiLive(`/api/v1/domain-console/sandbox/scenarios/${MODE.scenarioId}/state`));
|
|
render();
|
|
}
|
|
|
|
function setMode(kind) {
|
|
MODE.kind = kind;
|
|
document.getElementById('live-config').classList.toggle('hidden', kind !== 'live');
|
|
document.getElementById('mode-label').textContent = kind === 'live' ? 'SANDBOX LIVE' : 'SANDBOX MOCK';
|
|
document.getElementById('mode-desc').textContent = kind === 'live'
|
|
? 'create-only · VM112 real · delete bloqueado'
|
|
: 'dados fictícios · zero API';
|
|
if (kind === 'mock') { MODE.scenarioId = null; render(); }
|
|
}
|
|
|
|
document.getElementById('btn-mode-mock').addEventListener('click', () => setMode('mock'));
|
|
document.getElementById('btn-mode-live').addEventListener('click', () => setMode('live'));
|
|
document.getElementById('btn-new-scenario').addEventListener('click', () => {
|
|
createLiveScenario().catch(e => toast('Erro: ' + e.message));
|
|
});
|
|
|
|
function renderSummary() {
|
|
const used = seatsUsed();
|
|
const max = STATE.plan.maxSeats;
|
|
document.getElementById('summary-cards').innerHTML = `
|
|
<div class="card"><div class="card-label">Plano</div><div class="card-value">${STATE.plan.name}</div><div class="card-sub">R$ ${STATE.plan.price}/mês</div></div>
|
|
<div class="card"><div class="card-label">Contas</div><div class="card-value">${used} / ${max}</div><div class="card-sub">${max - used} disponíveis</div></div>
|
|
<div class="card"><div class="card-label">DMARC</div><div class="card-value"><span class="badge-ok">Certificado</span></div><div class="card-sub">Score ${STATE.dmarc.score}%</div></div>
|
|
<div class="card"><div class="card-label">Faturação</div><div class="card-value">${STATE.billing.status}</div><div class="card-sub">Renova ${STATE.billing.nextRenewal}</div></div>
|
|
`;
|
|
}
|
|
|
|
function renderNav() {
|
|
document.getElementById('nav').innerHTML = SECTIONS.map(s =>
|
|
`<button type="button" data-section="${s.id}" class="${STATE.section === s.id ? 'active' : ''}">${s.label}</button>`
|
|
).join('');
|
|
document.querySelectorAll('#nav button').forEach(btn => {
|
|
btn.addEventListener('click', () => { STATE.section = btn.dataset.section; render(); });
|
|
});
|
|
}
|
|
|
|
function renderOverview() {
|
|
const used = seatsUsed();
|
|
const pct = Math.round((used / STATE.plan.maxSeats) * 100);
|
|
const recent = STATE.accounts.slice(-3).reverse();
|
|
return `
|
|
<h2>Visão geral</h2>
|
|
<p class="desc">Resumo do domínio ${STATE.domain} — sandbox, sem ligação a servidores reais.</p>
|
|
<div class="progress-wrap">
|
|
<div class="progress-label"><span>Contas utilizadas</span><span>${used} / ${STATE.plan.maxSeats}</span></div>
|
|
<div class="progress-bar"><div class="progress-fill" style="width:${pct}%"></div></div>
|
|
</div>
|
|
<div class="stat-grid">
|
|
<div class="stat-box mail"><strong>Mail (Carbonio)</strong><br/>${STATE.accounts.reduce((s,a)=>s+a.mailGb,0)} GB atribuídos</div>
|
|
<div class="stat-box files"><strong>Files (Nextcloud)</strong><br/>${STATE.accounts.reduce((s,a)=>s+(a.nc?a.filesGb:0),0)} GB atribuídos</div>
|
|
</div>
|
|
<p style="margin-top:1.25rem;font-size:0.82rem;color:var(--muted)">Contas recentes:</p>
|
|
<ul style="font-size:0.82rem;margin:0.25rem 0 0 1rem;padding:0">${recent.map(a=>`<li>${a.email} — ${a.name}</li>`).join('')}</ul>
|
|
`;
|
|
}
|
|
|
|
function renderAccounts() {
|
|
const rows = STATE.accounts.map((a, i) => `
|
|
<tr>
|
|
<td>${a.email}${a.role === 'gerente' ? ' <span class="badge-ok" style="font-size:0.65rem">gerente</span>' : ''}</td>
|
|
<td>${a.name}</td>
|
|
<td>${a.mailGb} GB</td>
|
|
<td>${a.nc ? a.filesGb + ' GB' : '—'}</td>
|
|
<td>${a.active ? '<span class="badge-ok">Activo</span>' : 'Suspenso'}${a.protected ? '<span class="protected-tag">protegido</span>' : ''}</td>
|
|
<td>
|
|
${MODE.kind === 'mock' && a.role !== 'gerente' ? `<button type="button" class="btn btn-sm btn-danger" data-del="${i}">Remover</button>` : '—'}
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
return `
|
|
<div class="toolbar">
|
|
<div><h2 style="margin:0">Contas de email</h2></div>
|
|
<button type="button" class="btn btn-primary" id="btn-new-account">+ Nova conta</button>
|
|
</div>
|
|
<p class="desc">Criar e gerir contas — alterações só na memória deste browser.</p>
|
|
<table><thead><tr><th>Email</th><th>Nome</th><th>Mail</th><th>Nextcloud</th><th>Estado</th><th></th></tr></thead><tbody>${rows}</tbody></table>
|
|
`;
|
|
}
|
|
|
|
function renderFiles() {
|
|
const total = STATE.accounts.filter(a=>a.nc).reduce((s,a)=>s+a.filesGb,0);
|
|
return `
|
|
<h2>Nextcloud / Files</h2>
|
|
<p class="desc">Quota Files por conta — URL produção: files.${STATE.domain}</p>
|
|
<div class="stat-box files" style="margin-bottom:1rem"><strong>${total} GB</strong> atribuídos no domínio (simulado)</div>
|
|
<table><thead><tr><th>Conta</th><th>Quota Files</th><th>Nextcloud</th></tr></thead><tbody>
|
|
${STATE.accounts.filter(a=>a.nc).map(a=>`<tr><td>${a.email}</td><td>${a.filesGb} GB</td><td><button type="button" class="btn btn-sm" data-open-files="${a.email}">Abrir Files (sim.)</button></td></tr>`).join('')}
|
|
</tbody></table>
|
|
<p style="font-size:0.75rem;color:var(--muted);margin-top:1rem">Em produção: SSO token → files.${STATE.domain}. Aqui: toast apenas.</p>
|
|
`;
|
|
}
|
|
|
|
function renderDmarc() {
|
|
const ok = (v) => v ? '✅' : '⚠️';
|
|
return `
|
|
<h2>Certificação mail (EasyDMARC)</h2>
|
|
<p class="desc">Ligbox Certified Mail — dados mock. Produção: API Desk proxy.</p>
|
|
<div class="dmarc-grid">
|
|
<div class="dmarc-item"><div class="icon">${ok(STATE.dmarc.spf)}</div><strong>SPF</strong><br/><span style="font-size:0.75rem;color:var(--muted)">Alinhado</span></div>
|
|
<div class="dmarc-item"><div class="icon">${ok(STATE.dmarc.dkim)}</div><strong>DKIM</strong><br/><span style="font-size:0.75rem;color:var(--muted)">Selector ligbox</span></div>
|
|
<div class="dmarc-item"><div class="icon">${ok(STATE.dmarc.dmarc)}</div><strong>DMARC</strong><br/><span style="font-size:0.75rem;color:var(--muted)">p=quarantine</span></div>
|
|
</div>
|
|
<div class="upgrade-box" style="margin-top:1.25rem">Score global: <strong>${STATE.dmarc.score}%</strong> — entregabilidade excelente (simulado)</div>
|
|
`;
|
|
}
|
|
|
|
function renderDns() {
|
|
const ok = (v) => v ? '<span class="badge-ok">OK</span>' : '<span style="color:var(--warn)">Verificar</span>';
|
|
return `
|
|
<h2>Domínio & DNS</h2>
|
|
<p class="desc">Status registos — sandbox. Produção: wizard checks Spec 010.</p>
|
|
<div class="dns-row"><span>MX mail.${STATE.domain}</span>${ok(STATE.dns.mx)}</div>
|
|
<div class="dns-row"><span>A mail.${STATE.domain}</span>${ok(STATE.dns.aMail)}</div>
|
|
<div class="dns-row"><span>Subdomínio incluído</span><code style="font-size:0.8rem">${STATE.dns.subdomain}</code></div>
|
|
<button type="button" class="btn" style="margin-top:1rem" id="btn-dns-check">Verificação DNS avançada (sim.)</button>
|
|
`;
|
|
}
|
|
|
|
function renderBilling() {
|
|
const used = seatsUsed();
|
|
return `
|
|
<h2>Plano & faturação</h2>
|
|
<p class="desc">Resumo FOSSBilling — embed futuro. Nenhuma chamada a financeiro.ligbox.com.br.</p>
|
|
<table><tbody>
|
|
<tr><td><strong>Plano actual</strong></td><td>${STATE.plan.name} — R$ ${STATE.plan.price}/mês</td></tr>
|
|
<tr><td>Contas incluídas</td><td>${used} / ${STATE.plan.maxSeats}</td></tr>
|
|
<tr><td>Mail por conta</td><td>até ${STATE.plan.mailGbDefault} GB</td></tr>
|
|
<tr><td>Nextcloud por conta</td><td>até ${STATE.plan.filesGbDefault} GB</td></tr>
|
|
<tr><td>Próxima renovação</td><td>${STATE.billing.nextRenewal}</td></tr>
|
|
</tbody></table>
|
|
<div class="upgrade-box">
|
|
<strong>Upgrade simulado</strong> — Enterprise (50 contas, 50 GB mail) — R$ 999/mês<br/>
|
|
<button type="button" class="btn btn-primary" style="margin-top:0.75rem" id="btn-upgrade">Simular upgrade</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderPanel() {
|
|
const fns = { overview: renderOverview, accounts: renderAccounts, files: renderFiles, dmarc: renderDmarc, dns: renderDns, billing: renderBilling };
|
|
document.getElementById('panel').innerHTML = fns[STATE.section]();
|
|
bindPanelEvents();
|
|
}
|
|
|
|
function bindPanelEvents() {
|
|
document.getElementById('btn-new-account')?.addEventListener('click', openModal);
|
|
document.querySelectorAll('[data-del]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const i = +btn.dataset.del;
|
|
const email = STATE.accounts[i].email;
|
|
STATE.accounts.splice(i, 1);
|
|
toast(`Removido ${email} — simulado, produção intacta`);
|
|
render();
|
|
});
|
|
});
|
|
document.querySelectorAll('[data-open-files]').forEach(btn => {
|
|
btn.addEventListener('click', () => toast(`Abriria files.${STATE.domain} com SSO para ${btn.dataset.openFiles} (simulado)`));
|
|
});
|
|
document.getElementById('btn-dns-check')?.addEventListener('click', () => toast('Modal DNS avançado — EasyDMARC/MXToolbox (simulado)'));
|
|
document.getElementById('btn-upgrade')?.addEventListener('click', () => {
|
|
STATE.plan = { name: 'Enterprise', price: 999, maxSeats: 50, mailGbDefault: 50, filesGbDefault: 500 };
|
|
toast('Upgrade para Enterprise simulado — FOSS não alterado');
|
|
render();
|
|
});
|
|
}
|
|
|
|
function openModal() {
|
|
if (MODE.kind === 'live' && !MODE.scenarioId) {
|
|
toast('Crie primeiro um cenário real (+ Cenário real)');
|
|
return;
|
|
}
|
|
if (seatsUsed() >= STATE.plan.maxSeats) {
|
|
toast(`Limite ${STATE.plan.maxSeats} contas — simule upgrade em Plano & faturação`);
|
|
STATE.section = 'billing';
|
|
render();
|
|
return;
|
|
}
|
|
document.getElementById('new-domain-suffix').textContent = '@' + STATE.domain;
|
|
const mailSel = document.getElementById('new-mail-gb');
|
|
const filesSel = document.getElementById('new-files-gb');
|
|
mailSel.innerHTML = [20,30,40,50].map(g=>`<option value="${g}" ${g===STATE.plan.mailGbDefault?'selected':''}>${g} GB</option>`).join('');
|
|
filesSel.innerHTML = [100,200,300,400,500].map(g=>`<option value="${g}" ${g===STATE.plan.filesGbDefault?'selected':''}>${g} GB</option>`).join('');
|
|
document.getElementById('new-local').value = '';
|
|
document.getElementById('new-name').value = '';
|
|
document.getElementById('new-nc').checked = true;
|
|
document.getElementById('modal-account').classList.add('open');
|
|
}
|
|
|
|
function closeModal() { document.getElementById('modal-account').classList.remove('open'); }
|
|
|
|
document.getElementById('modal-cancel').addEventListener('click', closeModal);
|
|
document.getElementById('modal-save').addEventListener('click', async () => {
|
|
const local = document.getElementById('new-local').value.trim().toLowerCase();
|
|
const name = document.getElementById('new-name').value.trim() || local;
|
|
if (!local) { toast('Indique o nome da caixa (ex: vendas)'); return; }
|
|
const email = local + '@' + STATE.domain;
|
|
|
|
if (MODE.kind === 'live' && MODE.scenarioId) {
|
|
try {
|
|
await apiLive(`/api/v1/domain-console/sandbox/scenarios/${MODE.scenarioId}/accounts`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
local_part: local,
|
|
display_name: name,
|
|
}),
|
|
});
|
|
closeModal();
|
|
await loadLiveScenario();
|
|
toast(`Criada ${email} em produção (create-only)`);
|
|
} catch (e) {
|
|
toast('Erro live: ' + e.message);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (STATE.accounts.some(a => a.email === email)) { toast('Conta já existe (sandbox)'); return; }
|
|
STATE.accounts.push({
|
|
email, name,
|
|
mailGb: +document.getElementById('new-mail-gb').value,
|
|
filesGb: +document.getElementById('new-files-gb').value,
|
|
nc: document.getElementById('new-nc').checked,
|
|
active: true,
|
|
});
|
|
closeModal();
|
|
toast(`Criada ${email} + Nextcloud — simulado, zero APIs`);
|
|
render();
|
|
});
|
|
|
|
document.getElementById('modal-account').addEventListener('click', e => {
|
|
if (e.target.id === 'modal-account') closeModal();
|
|
});
|
|
|
|
function render() {
|
|
renderSummary();
|
|
renderNav();
|
|
renderPanel();
|
|
}
|
|
|
|
render();
|
|
(function () {
|
|
const p = new URLSearchParams(location.search);
|
|
if (p.get('live') === '1' || p.get('token')) {
|
|
if (p.get('token')) document.getElementById('api-token').value = p.get('token');
|
|
setMode('live');
|
|
loadLiveScenario().catch(() => {});
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|