changes
This commit is contained in:
@@ -18,6 +18,28 @@ function bytes(n) {
|
||||
return `${n.toFixed(n >= 10 || i === 0 ? 0 : 1)} ${u[i]}`;
|
||||
}
|
||||
|
||||
function percent(n) {
|
||||
if (n === null || n === undefined || Number.isNaN(Number(n))) return null;
|
||||
const value = Number(n);
|
||||
if (value < 1 && value > 0) return value.toFixed(1);
|
||||
return value.toFixed(0);
|
||||
}
|
||||
|
||||
function usageInfo(m) {
|
||||
const used = Number(m.used_bytes || 0);
|
||||
const limit = m.quota_bytes === null || m.quota_bytes === undefined ? null : Number(m.quota_bytes);
|
||||
const calculated = limit && limit > 0 ? (used / limit) * 100 : null;
|
||||
const rawPct = m.quota_percent === null || m.quota_percent === undefined ? calculated : Number(m.quota_percent);
|
||||
const pct = rawPct === null || Number.isNaN(rawPct) ? null : Math.max(0, Math.min(100, rawPct));
|
||||
|
||||
return {
|
||||
used,
|
||||
limit,
|
||||
pct,
|
||||
label: limit ? `${bytes(used)} / ${bytes(limit)} (${percent(pct)}%)` : `${bytes(used)} / unlimited`,
|
||||
};
|
||||
}
|
||||
|
||||
function esc(s) { return String(s ?? '').replace(/[&<>'"]/g, c => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c])); }
|
||||
|
||||
async function init() {
|
||||
@@ -29,11 +51,15 @@ async function init() {
|
||||
async function loadDomains(resync = false) {
|
||||
state.domains = await api(`/api/domains${resync ? '?resync=true' : ''}`);
|
||||
if (!state.selectedDomain && state.domains.length) state.selectedDomain = state.domains[0].domain;
|
||||
if (state.selectedDomain) await loadMailboxes();
|
||||
if (state.selectedDomain) await loadMailboxes(true);
|
||||
}
|
||||
|
||||
async function loadMailboxes() {
|
||||
state.mailboxes = await api(`/api/mailboxes?domain=${encodeURIComponent(state.selectedDomain)}`);
|
||||
async function loadMailboxes(refreshQuota = false) {
|
||||
if (!state.selectedDomain) {
|
||||
state.mailboxes = [];
|
||||
return;
|
||||
}
|
||||
state.mailboxes = await api(`/api/mailboxes?domain=${encodeURIComponent(state.selectedDomain)}${refreshQuota ? '&refreshQuota=true' : ''}`);
|
||||
}
|
||||
|
||||
async function loadAudit() {
|
||||
@@ -41,6 +67,22 @@ async function loadAudit() {
|
||||
renderAuditModal();
|
||||
}
|
||||
|
||||
function renderUsage(m) {
|
||||
const usage = usageInfo(m);
|
||||
const barWidth = usage.pct === null ? 0 : usage.pct;
|
||||
return `
|
||||
<div class="usage-cell">
|
||||
<div class="usage-label">Disk Usage: ${esc(usage.label)}</div>
|
||||
<div class="usage-bar" aria-label="Disk Usage ${esc(usage.label)}">
|
||||
<div class="usage-bar-fill" style="width:${barWidth}%"></div>
|
||||
</div>
|
||||
<div class="muted">
|
||||
${m.usage_scanned_at ? `quota checked ${new Date(m.usage_scanned_at).toLocaleString()}` : 'quota not checked yet'}
|
||||
${m.message_count !== null && m.message_count !== undefined ? ` · ${Number(m.message_count)} messages` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!state.user) return renderLogin();
|
||||
app.innerHTML = `
|
||||
@@ -70,8 +112,8 @@ function render() {
|
||||
</section>
|
||||
<section class="card">
|
||||
<div style="display:flex; justify-content:space-between; gap:12px; align-items:center; margin-bottom:14px">
|
||||
<div><h2 style="margin:0">${esc(state.selectedDomain || 'Mailboxes')}</h2><div class="muted">Create/delete mailboxes, reset passwords, edit rules.</div></div>
|
||||
<div class="actions"><button id="newMailboxBtn">New mailbox</button><button class="secondary" id="usageBtn">Scan usage</button></div>
|
||||
<div><h2 style="margin:0">${esc(state.selectedDomain || 'Mailboxes')}</h2><div class="muted">Create/delete mailboxes, reset passwords, edit rules. Quotas are refreshed when you open a domain.</div></div>
|
||||
<div class="actions"><button id="newMailboxBtn">New mailbox</button></div>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead><tr><th>Email</th><th>Status</th><th>Usage</th><th>Updated</th><th>Actions</th></tr></thead>
|
||||
@@ -80,7 +122,7 @@ function render() {
|
||||
<tr>
|
||||
<td><strong>${esc(m.email_address)}</strong><div class="muted">${esc(m.node_name)}</div></td>
|
||||
<td><span class="pill">${esc(m.status)}</span></td>
|
||||
<td>${bytes(m.used_bytes)}<div class="muted">${m.usage_scanned_at ? new Date(m.usage_scanned_at).toLocaleString() : 'not scanned'}</div></td>
|
||||
<td>${renderUsage(m)}</td>
|
||||
<td class="muted">${new Date(m.updated_at).toLocaleString()}</td>
|
||||
<td><div class="actions">
|
||||
<button class="secondary" data-rules="${esc(m.email_address)}">Rules</button>
|
||||
@@ -99,8 +141,7 @@ function render() {
|
||||
document.getElementById('resyncBtn').onclick = guard(async () => { await api('/api/domains/resync', { method:'POST' }); await loadDomains(false); state.message = 'DMS sync completed.'; render(); });
|
||||
document.getElementById('auditBtn').onclick = guard(loadAudit);
|
||||
document.getElementById('newMailboxBtn').onclick = renderCreateMailboxModal;
|
||||
document.getElementById('usageBtn').onclick = guard(async () => { await api('/api/mailboxes/usage/rescan', { method:'POST', body: JSON.stringify({ domain: state.selectedDomain }) }); await loadMailboxes(); render(); });
|
||||
document.querySelectorAll('[data-domain]').forEach(el => el.onclick = guard(async () => { state.selectedDomain = el.dataset.domain; await loadMailboxes(); render(); }));
|
||||
document.querySelectorAll('[data-domain]').forEach(el => el.onclick = guard(async () => { state.selectedDomain = el.dataset.domain; await loadMailboxes(true); render(); }));
|
||||
document.querySelectorAll('[data-delete]').forEach(el => el.onclick = () => renderDeleteModal(el.dataset.delete));
|
||||
document.querySelectorAll('[data-password]').forEach(el => el.onclick = () => renderPasswordModal(el.dataset.password));
|
||||
document.querySelectorAll('[data-rules]').forEach(el => el.onclick = () => renderRulesModal(el.dataset.rules));
|
||||
@@ -127,13 +168,17 @@ function modal(html) {
|
||||
}
|
||||
|
||||
function renderCreateMailboxModal() {
|
||||
const d = modal(`<h2>New mailbox</h2><form id="createForm" class="form-grid"><label>Email<input name="email" type="email" value="@${esc(state.selectedDomain || '')}" required></label><label>Password<input name="password" type="password" minlength="8" required></label><div><button>Create</button></div></form>`);
|
||||
const domain = state.selectedDomain || '';
|
||||
const d = modal(`<h2>New mailbox</h2><form id="createForm" class="form-grid"><label>Email<input name="email" type="email" value="@${esc(domain)}" placeholder="@${esc(domain)}" required></label><label>Password<input name="password" type="password" value="" minlength="8" autocomplete="new-password" required></label><div><button>Create</button></div></form>`);
|
||||
const input = d.querySelector('input[name="email"]');
|
||||
input.focus();
|
||||
input.setSelectionRange(0, 0);
|
||||
d.querySelector('#createForm').onsubmit = guard(async e => { e.preventDefault(); const f = new FormData(e.target); await api('/api/mailboxes', { method:'POST', body: JSON.stringify({ email:f.get('email'), password:f.get('password') }) }); d.remove(); await loadDomains(false); render(); });
|
||||
}
|
||||
|
||||
function renderDeleteModal(email) {
|
||||
const d = modal(`<h2>Delete mailbox</h2><p>Delete <strong>${esc(email)}</strong> from DMS?</p><button class="danger" id="confirmDelete">Delete</button>`);
|
||||
d.querySelector('#confirmDelete').onclick = guard(async () => { await api(`/api/mailboxes/${encodeURIComponent(email)}`, { method:'DELETE' }); d.remove(); await loadMailboxes(); render(); });
|
||||
d.querySelector('#confirmDelete').onclick = guard(async () => { await api(`/api/mailboxes/${encodeURIComponent(email)}`, { method:'DELETE' }); d.remove(); await loadMailboxes(true); render(); });
|
||||
}
|
||||
|
||||
function renderPasswordModal(email) {
|
||||
@@ -159,4 +204,4 @@ function renderAuditModal() {
|
||||
|
||||
function guard(fn) { return async function(...args) { try { state.error=''; state.message=''; await fn.apply(this,args); } catch(e) { state.error = e.message; render(); } }; }
|
||||
|
||||
init();
|
||||
init();
|
||||
Reference in New Issue
Block a user