feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes
Features added: - Admin > Domains: add domains to Mailcow servers, auto-generate DKIM, display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs) with one-click copy per record - Admin > Transports: manage sender-dependent relay hosts (add/delete) - Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other server logs in a dark scrollable panel - My Account: per-domain Quarantine panel — view score, sender, subject, date; permanently delete quarantined messages - My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API - My Account: Aliases & Forwarders (alias creation doubles as forwarder to any external address) UX fixes: - Quota 0 now displays ∞ (unlimited) in both admin and account views - Admin mailbox action buttons replaced with Dashicon icon buttons (lock, chart-bar, trash) with title tooltips i18n: - load_plugin_textdomain registered on init hook - All user-facing PHP strings wrapped in __() / esc_html__() - Translated strings array passed to account JS via wp_localize_script - woocow-es_ES.po/.mo — Spanish translation - woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms) - English remains the fallback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -336,6 +336,93 @@
|
||||
.woocow-muted { color: #aaa; font-size: 13px; }
|
||||
.woocow-error { color: #c0392b; }
|
||||
|
||||
/* ── Icon buttons (admin) ────────────────────────────────────── */
|
||||
.button.woocow-icon-btn {
|
||||
width: 30px !important;
|
||||
min-width: 0 !important;
|
||||
padding: 0 !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.button.woocow-icon-btn .dashicons {
|
||||
font-size: 15px;
|
||||
width: 15px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
/* ── DNS records table ───────────────────────────────────────── */
|
||||
.woocow-dns-table { margin-top: 8px; }
|
||||
.woocow-dns-host code { font-size: 11px; word-break: break-all; }
|
||||
.woocow-dns-val { max-width: 340px; }
|
||||
.woocow-dns-value {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
background: #f6f8fa;
|
||||
padding: 4px 6px;
|
||||
border-radius: 3px;
|
||||
max-height: 60px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ── Log viewer ──────────────────────────────────────────────── */
|
||||
.woocow-log-toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.woocow-log-pre {
|
||||
background: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ── Quarantine (account) ────────────────────────────────────── */
|
||||
.woocow-quarantine-wrap {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background: #fffbf0;
|
||||
}
|
||||
.woocow-quarantine-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.woocow-quarantine-table th,
|
||||
.woocow-quarantine-table td {
|
||||
padding: 7px 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.woocow-quarantine-table th { font-weight: 700; background: #f5f5f5; }
|
||||
.woocow-quarantine-table tr:hover td { background: #fafafa; }
|
||||
.woocow-score-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 7px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Spam filter panel ───────────────────────────────────────── */
|
||||
.woocow-spam-panel { font-size: 13px; }
|
||||
.woocow-spam-panel input[type=range] { vertical-align: middle; }
|
||||
.wc-spam-val { font-weight: 700; min-width: 30px; display: inline-block; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.woocow-domain-header,
|
||||
.woocow-mbox-main,
|
||||
@@ -343,4 +430,6 @@
|
||||
.woocow-quota-bar-outer { width: 80px; }
|
||||
.woocow-alias-fields { flex-direction: column; }
|
||||
.woocow-input { max-width: 100%; }
|
||||
.woocow-quarantine-table { font-size: 11px; }
|
||||
.woocow-quarantine-table th, .woocow-quarantine-table td { padding: 5px 6px; }
|
||||
}
|
||||
|
||||
@@ -57,9 +57,10 @@
|
||||
|
||||
let html = '';
|
||||
boxes.forEach(m => {
|
||||
const pct = parseFloat(m.percent_in_use || 0);
|
||||
const unlimited = (m.quota === 0 || m.quota === '0');
|
||||
const pct = unlimited ? 0 : parseFloat(m.percent_in_use || 0);
|
||||
const used = formatMB(m.quota_used);
|
||||
const max = formatMB(m.quota);
|
||||
const max = unlimited ? '∞' : formatMB(m.quota);
|
||||
const col = pct > 85 ? '#e74c3c' : pct > 60 ? '#f39c12' : '#27ae60';
|
||||
|
||||
html += `<div class="woocow-mailbox-row" data-email="${esc(m.username)}">
|
||||
@@ -79,14 +80,19 @@
|
||||
<div class="woocow-mbox-actions">
|
||||
<button class="woocow-btn woocow-btn-sm wc-change-pw"
|
||||
data-server="${esc(sid)}" data-domain="${esc(domain)}" data-email="${esc(m.username)}">
|
||||
Change Password
|
||||
🔑 Change Password
|
||||
</button>
|
||||
<button class="woocow-btn woocow-btn-sm woocow-btn-outline wc-toggle-aliases"
|
||||
data-server="${esc(sid)}" data-domain="${esc(domain)}">
|
||||
Aliases
|
||||
✉ Aliases & Forwarders
|
||||
</button>
|
||||
<button class="woocow-btn woocow-btn-sm woocow-btn-outline wc-spam-score-btn"
|
||||
data-server="${esc(sid)}" data-domain="${esc(domain)}"
|
||||
data-email="${esc(m.username)}" data-score="${esc(m.spam_score || 5)}">
|
||||
🛡 Spam Filter
|
||||
</button>
|
||||
<a href="${esc(webmail)}" target="_blank" rel="noopener" class="woocow-btn woocow-btn-sm woocow-btn-ghost">
|
||||
Webmail ↗
|
||||
↗ Webmail
|
||||
</a>
|
||||
</div>
|
||||
<div class="woocow-aliases-wrap" style="display:none">
|
||||
@@ -278,4 +284,106 @@
|
||||
});
|
||||
});
|
||||
|
||||
// ── Quarantine ────────────────────────────────────────────────────────────
|
||||
|
||||
$(document).on('click', '.woocow-load-quarantine', function () {
|
||||
const $panel = $(this).closest('.woocow-domain-panel');
|
||||
const $wrap = $panel.find('.woocow-quarantine-wrap');
|
||||
const $list = $panel.find('.woocow-quarantine-list');
|
||||
const sid = $panel.data('server-id');
|
||||
const domain = $panel.data('domain');
|
||||
|
||||
if ($wrap.is(':visible')) { $wrap.slideUp(); return; }
|
||||
|
||||
$list.html('<p class="woocow-loading">Loading quarantine…</p>');
|
||||
$wrap.slideDown();
|
||||
|
||||
ajax('woocow_acct_quarantine', { server_id: sid, domain }).done(res => {
|
||||
if (!res.success) { $list.html(`<p class="woocow-error">${esc(res.data)}</p>`); return; }
|
||||
const msgs = res.data;
|
||||
if (!msgs.length) { $list.html('<p class="woocow-muted">No quarantined messages for this domain.</p>'); return; }
|
||||
|
||||
let html = `<table class="woocow-quarantine-table">
|
||||
<thead><tr><th>From</th><th>To</th><th>Subject</th><th>Score</th><th>Date</th><th></th></tr></thead><tbody>`;
|
||||
msgs.forEach(m => {
|
||||
const date = new Date(m.created * 1000).toLocaleString();
|
||||
const score = parseFloat(m.score).toFixed(1);
|
||||
const virus = m.virus_flag == 1 ? ' 🦠' : '';
|
||||
html += `<tr>
|
||||
<td>${esc(m.sender)}</td>
|
||||
<td>${esc(m.rcpt)}</td>
|
||||
<td>${esc(m.subject)}${virus}</td>
|
||||
<td><span class="woocow-score-badge" style="background:${score > 10 ? '#e74c3c' : score > 5 ? '#f39c12' : '#95a5a6'}">${score}</span></td>
|
||||
<td style="white-space:nowrap;font-size:12px">${esc(date)}</td>
|
||||
<td><button class="woocow-btn woocow-btn-danger woocow-btn-xs wc-q-del"
|
||||
data-id="${esc(m.id)}" data-server="${esc(sid)}" data-domain="${esc(domain)}">Delete</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
html += '<p class="woocow-muted" style="margin-top:8px">To release a message to your inbox, use the link in your quarantine notification email or via Webmail.</p>';
|
||||
$list.html(html);
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.wc-q-del', function () {
|
||||
if (!confirm('Permanently delete this quarantined message?')) return;
|
||||
const $row = $(this).closest('tr');
|
||||
ajax('woocow_acct_quarantine_delete', {
|
||||
server_id: $(this).data('server'),
|
||||
domain: $(this).data('domain'),
|
||||
qid: $(this).data('id'),
|
||||
}).done(res => {
|
||||
if (res.success) $row.fadeOut(300, function () { $(this).remove(); });
|
||||
else alert('Delete failed: ' + res.data);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Spam Score ────────────────────────────────────────────────────────────
|
||||
|
||||
$(document).on('click', '.wc-spam-score-btn', function () {
|
||||
const $row = $(this).closest('.woocow-mailbox-row');
|
||||
const sid = $(this).data('server');
|
||||
const domain = $(this).data('domain');
|
||||
const email = $(this).data('email');
|
||||
const score = $(this).data('score');
|
||||
|
||||
const $existing = $row.find('.woocow-spam-panel');
|
||||
if ($existing.length) { $existing.slideToggle(); return; }
|
||||
|
||||
$row.append(`
|
||||
<div class="woocow-spam-panel" style="margin-top:12px;padding-top:12px;border-top:1px dashed #e0e0e0">
|
||||
<strong>Spam Filter Threshold</strong>
|
||||
<p class="woocow-muted">Lower = stricter. Default is 5. Emails above this score go to spam/quarantine.</p>
|
||||
<div class="woocow-flex-inline" style="gap:10px;margin-top:8px">
|
||||
<input type="range" class="wc-spam-slider" min="1" max="15" step="0.5" value="${esc(score)}"
|
||||
style="width:200px">
|
||||
<span class="wc-spam-val">${parseFloat(score).toFixed(1)}</span>
|
||||
<button class="woocow-btn woocow-btn-primary woocow-btn-sm wc-spam-save"
|
||||
data-server="${esc(sid)}" data-domain="${esc(domain)}" data-email="${esc(email)}">Save</button>
|
||||
<span class="wc-spam-notice"></span>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$row.find('.wc-spam-slider').on('input', function () {
|
||||
$row.find('.wc-spam-val').text(parseFloat($(this).val()).toFixed(1));
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.wc-spam-save', function () {
|
||||
const $panel = $(this).closest('.woocow-spam-panel');
|
||||
const $note = $panel.find('.wc-spam-notice');
|
||||
const score = $panel.find('.wc-spam-slider').val();
|
||||
$note.text('Saving…');
|
||||
ajax('woocow_acct_spam_score', {
|
||||
server_id: $(this).data('server'),
|
||||
domain: $(this).data('domain'),
|
||||
email: $(this).data('email'),
|
||||
spam_score: score,
|
||||
}).done(res => {
|
||||
if (res.success) $note.html('<span style="color:green">✓ Saved</span>');
|
||||
else $note.html(`<span class="woocow-error">${esc(res.data)}</span>`);
|
||||
});
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
|
||||
@@ -324,12 +324,18 @@
|
||||
<td>${max}</td>
|
||||
<td>${m.active == 1 ? '✓' : '–'}</td>
|
||||
<td class="woocow-actions">
|
||||
<button class="button button-small wc-mb-reset-pw"
|
||||
data-email="${esc(m.username)}">Reset PW</button>
|
||||
<button class="button button-small wc-mb-set-quota"
|
||||
data-email="${esc(m.username)}" data-quota="${quotaMB}">Set Quota</button>
|
||||
<button class="button button-small wc-mb-del"
|
||||
data-email="${esc(m.username)}" style="color:#a00">Delete</button>
|
||||
<button class="button button-small woocow-icon-btn wc-mb-reset-pw"
|
||||
title="Reset Password" data-email="${esc(m.username)}">
|
||||
<span class="dashicons dashicons-lock"></span>
|
||||
</button>
|
||||
<button class="button button-small woocow-icon-btn wc-mb-set-quota"
|
||||
title="Set Quota" data-email="${esc(m.username)}" data-quota="${quotaMB}">
|
||||
<span class="dashicons dashicons-chart-bar"></span>
|
||||
</button>
|
||||
<button class="button button-small woocow-icon-btn wc-mb-del"
|
||||
title="Delete" data-email="${esc(m.username)}" style="color:#a00">
|
||||
<span class="dashicons dashicons-trash"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
@@ -459,9 +465,236 @@
|
||||
}
|
||||
|
||||
function formatMB(bytes) {
|
||||
if (bytes === 0 || bytes === '0') return '∞';
|
||||
if (!bytes) return '0 MB';
|
||||
const mb = bytes / 1024 / 1024;
|
||||
return mb >= 1024 ? (mb / 1024).toFixed(1) + ' GB' : mb.toFixed(0) + ' MB';
|
||||
}
|
||||
|
||||
// ── Domains Page ──────────────────────────────────────────────────────────
|
||||
|
||||
if ($('#wc-dom-server').length) {
|
||||
let domServerId = null;
|
||||
let domServerUrl = null;
|
||||
|
||||
$('#wc-dom-server').on('change', function () {
|
||||
domServerId = $(this).val();
|
||||
domServerUrl = $(this).find('option:selected').data('url');
|
||||
$('#wc-dom-load').prop('disabled', !domServerId);
|
||||
$('#wc-dom-add-btn, #wc-dom-table-wrap').hide();
|
||||
});
|
||||
|
||||
const loadDomains = () => {
|
||||
ajax('woocow_server_domains', { server_id: domServerId }).done(res => {
|
||||
if (!res.success) {
|
||||
notice($('#wc-dom-notices'), 'error', res.data);
|
||||
return;
|
||||
}
|
||||
const domains = res.data;
|
||||
if (!domains.length) {
|
||||
$('#wc-dom-table-wrap').html('<p>No domains on this server yet.</p>').show();
|
||||
} else {
|
||||
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
|
||||
<thead><tr><th>Domain</th><th>Active</th><th>Actions</th></tr></thead><tbody>`;
|
||||
domains.forEach(d => {
|
||||
html += `<tr>
|
||||
<td><strong>${esc(d.domain)}</strong></td>
|
||||
<td>${d.active == 1 ? '<span class="woocow-badge woocow-badge-green">Active</span>' : '<span class="woocow-badge woocow-badge-grey">Inactive</span>'}</td>
|
||||
<td>
|
||||
<button class="button button-small wc-dom-dns" data-domain="${esc(d.domain)}">
|
||||
<span class="dashicons dashicons-admin-site"></span> DNS Records
|
||||
</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
$('#wc-dom-table-wrap').html(html).show();
|
||||
}
|
||||
$('#wc-dom-add-btn').show();
|
||||
});
|
||||
};
|
||||
|
||||
$('#wc-dom-load').on('click', loadDomains);
|
||||
|
||||
$('#wc-dom-add-btn').on('click', () => {
|
||||
$('#wc-dom-name, #wc-dom-desc').val('');
|
||||
$('#wc-dom-form-notice').text('');
|
||||
$('#wc-dom-form').slideDown();
|
||||
});
|
||||
$('#wc-dom-cancel').on('click', () => $('#wc-dom-form').slideUp());
|
||||
|
||||
$('#wc-dom-save').on('click', () => {
|
||||
const $note = $('#wc-dom-form-notice').text('Adding domain…');
|
||||
const data = {
|
||||
server_id: domServerId,
|
||||
domain: $('#wc-dom-name').val().trim(),
|
||||
description: $('#wc-dom-desc').val().trim(),
|
||||
mailboxes: $('#wc-dom-mailboxes').val(),
|
||||
aliases: $('#wc-dom-aliases').val(),
|
||||
quota: $('#wc-dom-quota').val(),
|
||||
defquota: $('#wc-dom-defquota').val(),
|
||||
dkim_size: $('#wc-dom-dkim-size').val(),
|
||||
};
|
||||
if (!data.domain) { $note.html('<span style="color:red">Domain name required.</span>'); return; }
|
||||
|
||||
ajax('woocow_admin_domain_add', data).done(res => {
|
||||
if (res.success) {
|
||||
$note.html('<span style="color:green">✓ Domain added with DKIM generated!</span>');
|
||||
$('#wc-dom-form').slideUp();
|
||||
loadDomains();
|
||||
} else {
|
||||
$note.html(`<span style="color:red">${esc(res.data)}</span>`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// DNS Records panel
|
||||
$(document).on('click', '.wc-dom-dns', function () {
|
||||
const domain = $(this).data('domain');
|
||||
$('#wc-dom-dns-domain').text(domain);
|
||||
$('#wc-dom-dns-content').html('<p>Loading…</p>');
|
||||
$('#wc-dom-dns-panel').show();
|
||||
$('html,body').animate({ scrollTop: $('#wc-dom-dns-panel').offset().top - 40 }, 300);
|
||||
|
||||
ajax('woocow_admin_domain_dns', { server_id: domServerId, domain }).done(res => {
|
||||
if (!res.success) {
|
||||
$('#wc-dom-dns-content').html(`<p style="color:red">${esc(res.data)}</p>`);
|
||||
return;
|
||||
}
|
||||
const d = res.data;
|
||||
let html = `<table class="wp-list-table widefat fixed striped woocow-table woocow-dns-table">
|
||||
<thead><tr><th>Type</th><th>Host / Name</th><th>Value</th><th>Priority</th><th>TTL</th><th>Note</th><th></th></tr></thead><tbody>`;
|
||||
d.records.forEach(r => {
|
||||
html += `<tr>
|
||||
<td><code>${esc(r.type)}</code></td>
|
||||
<td class="woocow-dns-host"><code>${esc(r.host)}</code></td>
|
||||
<td class="woocow-dns-val"><code class="woocow-dns-value">${esc(r.value)}</code></td>
|
||||
<td>${esc(r.prio)}</td>
|
||||
<td>${esc(r.ttl)}</td>
|
||||
<td><em>${esc(r.note || '')}</em></td>
|
||||
<td><button class="button button-small wc-copy-dns" data-val="${esc(r.value)}">Copy</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
if (!d.dkim_txt) {
|
||||
html += `<p class="description" style="color:orange">⚠ DKIM key not yet generated for this domain. Add the domain first, then view DNS records again.</p>`;
|
||||
}
|
||||
$('#wc-dom-dns-content').html(html);
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.wc-copy-dns', function () {
|
||||
const val = $(this).data('val');
|
||||
navigator.clipboard.writeText(val).then(() => {
|
||||
$(this).text('Copied!');
|
||||
setTimeout(() => $(this).text('Copy'), 1500);
|
||||
});
|
||||
});
|
||||
|
||||
$('#wc-dom-dns-close').on('click', () => $('#wc-dom-dns-panel').hide());
|
||||
}
|
||||
|
||||
// ── Transports Page ───────────────────────────────────────────────────────
|
||||
|
||||
if ($('#wc-tr-server').length) {
|
||||
let trServerId = null;
|
||||
|
||||
$('#wc-tr-server').on('change', function () {
|
||||
trServerId = $(this).val();
|
||||
$('#wc-tr-load').prop('disabled', !trServerId);
|
||||
});
|
||||
|
||||
const loadTransports = () => {
|
||||
ajax('woocow_admin_relayhosts_list', { server_id: trServerId }).done(res => {
|
||||
if (!res.success) { notice($('#wc-tr-notices'), 'error', res.data); return; }
|
||||
const rows = res.data;
|
||||
if (!Array.isArray(rows) || !rows.length) {
|
||||
$('#wc-tr-table-wrap').html('<p>No transports configured on this server.</p>');
|
||||
} else {
|
||||
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
|
||||
<thead><tr><th>Hostname:Port</th><th>Username</th><th>Used by Domains</th><th>Active</th><th>Actions</th></tr></thead><tbody>`;
|
||||
rows.forEach(r => {
|
||||
html += `<tr>
|
||||
<td><code>${esc(r.hostname)}</code></td>
|
||||
<td>${esc(r.username)}</td>
|
||||
<td>${esc(r.used_by_domains || '—')}</td>
|
||||
<td>${r.active == 1 ? '✓' : '–'}</td>
|
||||
<td><button class="button button-small woocow-icon-btn wc-tr-del" title="Delete" data-id="${r.id}" style="color:#a00">
|
||||
<span class="dashicons dashicons-trash"></span>
|
||||
</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
$('#wc-tr-table-wrap').html(html);
|
||||
}
|
||||
$('#wc-tr-add-btn').show();
|
||||
});
|
||||
};
|
||||
|
||||
$('#wc-tr-load').on('click', loadTransports);
|
||||
$('#wc-tr-add-btn').on('click', () => { $('#wc-tr-hostname,#wc-tr-user,#wc-tr-pass').val(''); $('#wc-tr-form').slideDown(); });
|
||||
$('#wc-tr-cancel').on('click', () => $('#wc-tr-form').slideUp());
|
||||
|
||||
$('#wc-tr-save').on('click', () => {
|
||||
const $note = $('#wc-tr-form-notice').text('Saving…');
|
||||
ajax('woocow_admin_relayhost_save', {
|
||||
server_id: trServerId,
|
||||
hostname: $('#wc-tr-hostname').val().trim(),
|
||||
username: $('#wc-tr-user').val().trim(),
|
||||
password: $('#wc-tr-pass').val(),
|
||||
active: $('#wc-tr-active').is(':checked') ? 1 : 0,
|
||||
}).done(res => {
|
||||
if (res.success) { $note.html('<span style="color:green">✓ Transport added.</span>'); $('#wc-tr-form').slideUp(); loadTransports(); }
|
||||
else $note.html(`<span style="color:red">${esc(res.data)}</span>`);
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.wc-tr-del', function () {
|
||||
if (!confirm('Delete this transport?')) return;
|
||||
ajax('woocow_admin_relayhost_delete', { server_id: trServerId, id: $(this).data('id') }).done(res => {
|
||||
if (res.success) loadTransports();
|
||||
else notice($('#wc-tr-notices'), 'error', res.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Logs Page ─────────────────────────────────────────────────────────────
|
||||
|
||||
if ($('#wc-log-server').length) {
|
||||
$('#wc-log-server').on('change', function () {
|
||||
$('#wc-log-load').prop('disabled', !$(this).val());
|
||||
});
|
||||
|
||||
$('#wc-log-load').on('click', () => {
|
||||
const sid = $('#wc-log-server').val();
|
||||
const type = $('#wc-log-type').val();
|
||||
$('#wc-log-wrap').html('<p>Loading…</p>');
|
||||
|
||||
ajax('woocow_admin_logs', { server_id: sid, log_type: type }).done(res => {
|
||||
if (!res.success) {
|
||||
$('#wc-log-wrap').html(`<p style="color:red">${esc(res.data)}</p>`);
|
||||
return;
|
||||
}
|
||||
const entries = Array.isArray(res.data) ? res.data : Object.values(res.data);
|
||||
if (!entries.length) {
|
||||
$('#wc-log-wrap').html('<p>No log entries.</p>');
|
||||
return;
|
||||
}
|
||||
// Render as a scrollable pre block
|
||||
const text = entries.map(e => {
|
||||
if (typeof e === 'string') return e;
|
||||
if (e.time && e.message) return `[${e.time}] ${e.message}`;
|
||||
return JSON.stringify(e);
|
||||
}).join('\n');
|
||||
$('#wc-log-wrap').html(`
|
||||
<div class="woocow-log-toolbar">
|
||||
<strong>${esc(type.charAt(0).toUpperCase() + type.slice(1))} log</strong>
|
||||
<span style="color:#666;font-size:12px">${entries.length} entries</span>
|
||||
</div>
|
||||
<pre class="woocow-log-pre">${esc(text)}</pre>
|
||||
`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
})(jQuery);
|
||||
|
||||
Reference in New Issue
Block a user