Files
WooCow/assets/js/woocow-admin.js
Malin 1c5b58f238 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>
2026-02-27 08:38:52 +01:00

701 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* WooCow Admin JavaScript
*/
(function ($) {
'use strict';
const ajax = (action, data) =>
$.post(woocow.ajax_url, { action, nonce: woocow.nonce, ...data });
const notice = ($el, type, msg, autohide = true) => {
$el.html(`<div class="notice notice-${type} is-dismissible"><p>${msg}</p></div>`);
if (autohide) setTimeout(() => $el.find('.notice').fadeOut(), 4000);
};
// ── Servers Page ──────────────────────────────────────────────────────────
if ($('#wc-servers-table-wrap').length) {
let editId = 0;
const loadServers = () => {
ajax('woocow_servers_list').done(res => {
if (!res.success) return;
const rows = res.data;
if (!rows.length) {
$('#wc-servers-table-wrap').html('<p>No servers yet. Add one above.</p>');
return;
}
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr>
<th>Name</th><th>URL</th><th>Status</th><th>Added</th><th>Actions</th>
</tr></thead><tbody>`;
rows.forEach(s => {
const badge = s.active == 1
? '<span class="woocow-badge woocow-badge-green">Active</span>'
: '<span class="woocow-badge woocow-badge-grey">Inactive</span>';
html += `<tr data-id="${s.id}" data-name="${esc(s.name)}" data-url="${esc(s.url)}">
<td><strong>${esc(s.name)}</strong></td>
<td><a href="${esc(s.url)}" target="_blank" rel="noopener">${esc(s.url)}</a></td>
<td>${badge}</td>
<td>${s.created_at.split(' ')[0]}</td>
<td class="woocow-actions">
<button class="button button-small wc-srv-edit" data-id="${s.id}">Edit</button>
<button class="button button-small wc-srv-test" data-id="${s.id}">Test</button>
<button class="button button-small wc-srv-del" data-id="${s.id}" style="color:#a00">Delete</button>
</td>
</tr>`;
});
html += '</tbody></table>';
$('#wc-servers-table-wrap').html(html);
$('#wc-servers-loading').hide();
});
};
loadServers();
// Show add form
$('#wc-add-server').on('click', () => {
editId = 0;
$('#wc-server-id').val('');
$('#wc-server-name, #wc-server-url, #wc-server-key').val('');
$('#wc-server-active').prop('checked', true);
$('#wc-server-form-title').text('Add Server');
$('#wc-server-form').slideDown();
});
// Edit row
$(document).on('click', '.wc-srv-edit', function () {
editId = $(this).data('id');
const $row = $(this).closest('tr');
$('#wc-server-id').val(editId);
$('#wc-server-name').val($row.data('name'));
$('#wc-server-url').val($row.data('url'));
$('#wc-server-key').val('');
$('#wc-server-active').prop('checked', true);
$('#wc-server-form-title').text('Edit Server');
$('#wc-server-form').slideDown();
$('html, body').animate({ scrollTop: 0 }, 300);
});
// Save
$('#wc-server-save').on('click', () => {
const data = {
id: $('#wc-server-id').val(),
name: $('#wc-server-name').val().trim(),
url: $('#wc-server-url').val().trim(),
api_key: $('#wc-server-key').val().trim(),
active: $('#wc-server-active').is(':checked') ? 1 : 0,
};
if (!data.name || !data.url || (!data.api_key && !editId)) {
notice($('#wc-notices'), 'error', 'Please fill in all required fields.');
return;
}
ajax('woocow_server_save', data).done(res => {
if (res.success) {
notice($('#wc-notices'), 'success', 'Server saved.');
$('#wc-server-form').slideUp();
loadServers();
} else {
notice($('#wc-notices'), 'error', res.data || 'Save failed.');
}
});
});
// Test connection
$('#wc-server-test').on('click', () => {
const $result = $('#wc-server-test-result').text('Testing…');
ajax('woocow_server_test', {
id: $('#wc-server-id').val(),
url: $('#wc-server-url').val().trim(),
api_key: $('#wc-server-key').val().trim(),
}).done(res => {
if (res.success) {
$result.html(`<span style="color:green">✓ Connected Mailcow ${esc(res.data.version)}</span>`);
} else {
$result.html(`<span style="color:red">✗ ${esc(res.data)}</span>`);
}
});
});
// Test from row
$(document).on('click', '.wc-srv-test', function () {
const id = $(this).data('id');
const $td = $(this).closest('td');
$td.append('<span class="wc-inline-test"> Testing…</span>');
ajax('woocow_server_test', { id }).done(res => {
$td.find('.wc-inline-test').html(
res.success
? `<span style="color:green"> ✓ v${esc(res.data.version)}</span>`
: `<span style="color:red"> ✗ ${esc(res.data)}</span>`
);
});
});
// Delete
$(document).on('click', '.wc-srv-del', function () {
if (!confirm('Delete this server? All domain assignments for it will also be removed.')) return;
ajax('woocow_server_delete', { id: $(this).data('id') }).done(res => {
if (res.success) loadServers();
else notice($('#wc-notices'), 'error', res.data);
});
});
$('#wc-server-cancel').on('click', () => $('#wc-server-form').slideUp());
}
// ── Assignments Page ──────────────────────────────────────────────────────
if ($('#wc-assignments-table-wrap').length) {
const loadAssignments = () => {
ajax('woocow_assignments_list').done(res => {
if (!res.success) return;
const rows = res.data;
if (!rows.length) {
$('#wc-assignments-table-wrap').html('<p>No assignments yet.</p>');
$('#wc-assignments-loading').hide();
return;
}
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr>
<th>Customer</th><th>Email</th><th>Domain</th><th>Server</th><th>Assigned</th><th>Actions</th>
</tr></thead><tbody>`;
rows.forEach(r => {
html += `<tr>
<td>${esc(r.display_name)}</td>
<td>${esc(r.user_email)}</td>
<td><strong>${esc(r.domain)}</strong></td>
<td>${esc(r.server_name)}</td>
<td>${r.created_at.split(' ')[0]}</td>
<td><button class="button button-small wc-assign-del" data-id="${r.id}" style="color:#a00">Remove</button></td>
</tr>`;
});
html += '</tbody></table>';
$('#wc-assignments-table-wrap').html(html);
$('#wc-assignments-loading').hide();
});
};
// Load servers into select
ajax('woocow_servers_list').done(res => {
if (!res.success) return;
res.data.filter(s => s.active == 1).forEach(s => {
$('#wc-assign-server').append(`<option value="${s.id}">${esc(s.name)}</option>`);
});
});
// Server → load domains
$('#wc-assign-server').on('change', function () {
const sid = $(this).val();
$('#wc-assign-domain').html('<option value="">— Loading —</option>');
if (!sid) { $('#wc-domain-row').hide(); return; }
ajax('woocow_server_domains', { server_id: sid }).done(res => {
if (!res.success) {
alert('Could not load domains: ' + res.data);
return;
}
$('#wc-assign-domain').html('<option value="">— Select domain —</option>');
res.data.forEach(d => {
$('#wc-assign-domain').append(`<option value="${esc(d.domain)}">${esc(d.domain)}</option>`);
});
$('#wc-domain-row').show();
});
});
// Customer autocomplete
let searchTimer;
$('#wc-cust-search').on('input', function () {
clearTimeout(searchTimer);
const term = $(this).val().trim();
if (term.length < 2) { $('#wc-cust-results').hide(); return; }
searchTimer = setTimeout(() => {
ajax('woocow_customers_search', { term }).done(res => {
if (!res.success || !res.data.length) { $('#wc-cust-results').hide(); return; }
let html = '';
res.data.forEach(c => {
html += `<div class="woocow-ac-item" data-id="${c.id}" data-label="${esc(c.label)}">${esc(c.label)}</div>`;
});
$('#wc-cust-results').html(html).show();
});
}, 250);
});
$(document).on('click', '.woocow-ac-item', function () {
$('#wc-cust-id').val($(this).data('id'));
$('#wc-cust-search').val('');
$('#wc-cust-selected').text($(this).data('label'));
$('#wc-cust-results').hide();
});
$(document).on('click', function (e) {
if (!$(e.target).closest('#wc-cust-results, #wc-cust-search').length) {
$('#wc-cust-results').hide();
}
});
// Save assignment
$('#wc-assign-save').on('click', () => {
const customer_id = $('#wc-cust-id').val();
const server_id = $('#wc-assign-server').val();
const domain = $('#wc-assign-domain').val();
if (!customer_id || !server_id || !domain) {
notice($('#wc-assign-notice'), 'error', 'Please select a customer, server, and domain.');
return;
}
ajax('woocow_assignment_save', { customer_id, server_id, domain }).done(res => {
if (res.success) {
notice($('#wc-assign-notice'), 'success', `Domain <strong>${esc(domain)}</strong> assigned.`);
$('#wc-cust-id').val('');
$('#wc-cust-selected').text('');
loadAssignments();
} else {
notice($('#wc-assign-notice'), 'error', res.data);
}
});
});
// Delete assignment
$(document).on('click', '.wc-assign-del', function () {
if (!confirm('Remove this domain assignment?')) return;
ajax('woocow_assignment_delete', { id: $(this).data('id') }).done(res => {
if (res.success) loadAssignments();
});
});
loadAssignments();
}
// ── Mailboxes Page ────────────────────────────────────────────────────────
if ($('#wc-mb-table-wrap').length) {
let currentServerId = null;
let currentDomain = null;
// Server → load domains
$('#wc-mb-server').on('change', function () {
const sid = $(this).val();
$('#wc-mb-domain').hide().html('<option value="">— Select domain —</option>');
$('#wc-mb-load').prop('disabled', true);
if (!sid) return;
ajax('woocow_server_domains', { server_id: sid }).done(res => {
if (!res.success) return;
res.data.forEach(d => {
$('#wc-mb-domain').append(`<option value="${esc(d.domain)}">${esc(d.domain)}</option>`);
});
$('#wc-mb-domain').show();
});
});
$('#wc-mb-domain').on('change', function () {
$('#wc-mb-load').prop('disabled', !$(this).val());
});
const loadMailboxes = () => {
currentServerId = $('#wc-mb-server').val();
currentDomain = $('#wc-mb-domain').val();
if (!currentServerId || !currentDomain) return;
$('#wc-mb-table-wrap').html('<p>Loading…</p>');
ajax('woocow_admin_mailboxes', { server_id: currentServerId, domain: currentDomain }).done(res => {
if (!res.success) {
$('#wc-mb-table-wrap').html(`<div class="notice notice-error"><p>${esc(res.data)}</p></div>`);
return;
}
const boxes = res.data.mailboxes || [];
const webmail = res.data.webmail_url;
if (!boxes.length) {
$('#wc-mb-table-wrap').html('<p>No mailboxes found for this domain.</p>');
} else {
let html = `<table class="wp-list-table widefat fixed striped woocow-table">
<thead><tr>
<th>Email</th><th>Name</th><th>Quota Used</th><th>Quota Max</th><th>Active</th><th>Actions</th>
</tr></thead><tbody>`;
boxes.forEach(m => {
const pct = m.percent_in_use || 0;
const used = formatMB(m.quota_used);
const max = formatMB(m.quota);
const bar = `<div class="woocow-quota-bar"><div style="width:${pct}%"></div></div>`;
const quotaMB = Math.round((m.quota || 0) / 1024 / 1024);
html += `<tr>
<td><a href="${esc(webmail)}" target="_blank">${esc(m.username)}</a></td>
<td>${esc(m.name)}</td>
<td>${used} ${bar}</td>
<td>${max}</td>
<td>${m.active == 1 ? '✓' : ''}</td>
<td class="woocow-actions">
<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>`;
});
html += '</tbody></table>';
$('#wc-mb-table-wrap').html(html);
}
$('#wc-mb-domain-label').text(currentDomain);
$('#wc-mb-create').show();
});
};
$('#wc-mb-load').on('click', loadMailboxes);
// Create mailbox modal
$('#wc-mb-create').on('click', () => {
$('#wc-mb-local, #wc-mb-fullname, #wc-mb-pass, #wc-mb-pass2').val('');
$('#wc-mb-quota').val(1024);
$('#wc-mb-modal-notice').text('');
$('#wc-mb-modal').show();
});
$('#wc-mb-modal-cancel').on('click', () => $('#wc-mb-modal').hide());
$(document).on('keydown', e => { if (e.key === 'Escape') $('#wc-mb-modal').hide(); });
$('#wc-mb-modal-save').on('click', () => {
const data = {
server_id: currentServerId,
domain: currentDomain,
local_part: $('#wc-mb-local').val().trim(),
name: $('#wc-mb-fullname').val().trim(),
password: $('#wc-mb-pass').val(),
password2: $('#wc-mb-pass2').val(),
quota: $('#wc-mb-quota').val(),
};
$('#wc-mb-modal-notice').text('Creating…');
ajax('woocow_admin_mailbox_create', data).done(res => {
if (res.success) {
$('#wc-mb-modal').hide();
notice($('#wc-mb-notices'), 'success', `Mailbox <strong>${esc(res.data.email)}</strong> created.`);
loadMailboxes();
} else {
$('#wc-mb-modal-notice').html(`<span style="color:red">${esc(res.data)}</span>`);
}
});
});
// Delete mailbox
$(document).on('click', '.wc-mb-del', function () {
const email = $(this).data('email');
if (!confirm(`Delete mailbox ${email}? This cannot be undone.`)) return;
ajax('woocow_admin_mailbox_delete', { server_id: currentServerId, email }).done(res => {
if (res.success) loadMailboxes();
else notice($('#wc-mb-notices'), 'error', res.data);
});
});
// ── Edit modal: Reset PW ──────────────────────────────────────────────
let editEmail = '';
let editType = '';
const openEditModal = (email, type, currentQuota) => {
editEmail = email;
editType = type;
$('#wc-mb-edit-subtitle').text(email);
$('#wc-mb-edit-notice').text('');
$('#wc-mb-edit-pw-section, #wc-mb-edit-quota-section').hide();
if (type === 'password') {
$('#wc-mb-edit-title').text('Reset Password');
$('#wc-mb-edit-pass, #wc-mb-edit-pass2').val('');
$('#wc-mb-edit-pw-section').show();
setTimeout(() => $('#wc-mb-edit-pass').trigger('focus'), 100);
} else {
$('#wc-mb-edit-title').text('Set Quota');
$('#wc-mb-edit-quota').val(currentQuota || 1024);
$('#wc-mb-edit-quota-section').show();
setTimeout(() => $('#wc-mb-edit-quota').trigger('focus'), 100);
}
$('#wc-mb-edit-modal').show();
};
$(document).on('click', '.wc-mb-reset-pw', function () {
openEditModal($(this).data('email'), 'password', null);
});
$(document).on('click', '.wc-mb-set-quota', function () {
openEditModal($(this).data('email'), 'quota', $(this).data('quota'));
});
$('#wc-mb-edit-cancel').on('click', () => $('#wc-mb-edit-modal').hide());
$(document).on('keydown', e => { if (e.key === 'Escape') $('#wc-mb-edit-modal').hide(); });
$('#wc-mb-edit-save').on('click', () => {
const $note = $('#wc-mb-edit-notice').text('Saving…');
const data = { server_id: currentServerId, email: editEmail, type: editType };
if (editType === 'password') {
data.password = $('#wc-mb-edit-pass').val();
data.password2 = $('#wc-mb-edit-pass2').val();
if (!data.password) { $note.html('<span style="color:red">Password cannot be empty.</span>'); return; }
if (data.password !== data.password2) { $note.html('<span style="color:red">Passwords do not match.</span>'); return; }
} else {
data.quota = $('#wc-mb-edit-quota').val();
if (!data.quota || data.quota < 1) { $note.html('<span style="color:red">Enter a valid quota.</span>'); return; }
}
ajax('woocow_admin_mailbox_edit', data).done(res => {
if (res.success) {
$('#wc-mb-edit-modal').hide();
const msg = editType === 'password' ? 'Password updated.' : 'Quota updated.';
notice($('#wc-mb-notices'), 'success', `<strong>${esc(editEmail)}</strong> — ${msg}`);
if (editType === 'quota') loadMailboxes(); // refresh to show new quota
} else {
$note.html(`<span style="color:red">${esc(res.data)}</span>`);
}
});
});
}
// ── Utilities ─────────────────────────────────────────────────────────────
function esc(str) {
return String(str).replace(/[&<>"']/g, m => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
})[m]);
}
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);