- Syslog-style table (postfix, dovecot, sogo, netfilter, acme): Time | Priority badge (colour-coded debug→info→notice→warn→error→crit) | Process | Message — columns hidden when unused by that log type - API access log: Time | Method badge (GET=green, POST=blue, PUT=orange, DELETE=red) | Endpoint | Remote IP | Data - Autodiscover: Time | User | Service badge (ActiveSync, CalDAV, CardDAV, IMAP, SMTP) | User Agent - Watchdog: Time | Service | Health badge (Healthy/Degraded/Critical based on lvl) | Processes (now/total) | Change (+/-/±0 coloured) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1227 lines
60 KiB
JavaScript
1227 lines
60 KiB
JavaScript
/**
|
||
* 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 => ({
|
||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||
})[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;
|
||
let domainsData = {}; // domain name → full data object
|
||
|
||
$('#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();
|
||
domainsData = {};
|
||
});
|
||
|
||
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;
|
||
domainsData = {};
|
||
domains.forEach(d => { domainsData[d.domain] = d; });
|
||
|
||
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>Mailboxes</th>
|
||
<th>Quota Used</th>
|
||
<th>Active</th>
|
||
<th>Actions</th>
|
||
</tr></thead><tbody>`;
|
||
domains.forEach(d => {
|
||
const mboxes = `${d.mboxes_in}/${d.mailboxes || '∞'}`;
|
||
const qUsed = formatMB(d.quota_used * 1024 * 1024);
|
||
const qTotal = d.quota ? formatMB(d.quota * 1024 * 1024) : '∞';
|
||
const active = d.active == 1
|
||
? '<span class="woocow-badge woocow-badge-green">Active</span>'
|
||
: '<span class="woocow-badge woocow-badge-grey">Inactive</span>';
|
||
html += `<tr>
|
||
<td><strong>${esc(d.domain)}</strong>${d.description ? `<br><small class="description">${esc(d.description)}</small>` : ''}</td>
|
||
<td>${mboxes}</td>
|
||
<td>${qUsed} / ${qTotal}</td>
|
||
<td>${active}</td>
|
||
<td class="woocow-actions">
|
||
<button class="button button-small woocow-icon-btn wc-dom-dns"
|
||
title="DNS Records" data-domain="${esc(d.domain)}">
|
||
<span class="dashicons dashicons-admin-site"></span>
|
||
</button>
|
||
<button class="button button-small woocow-icon-btn wc-dom-edit"
|
||
title="Edit" data-domain="${esc(d.domain)}">
|
||
<span class="dashicons dashicons-edit"></span>
|
||
</button>
|
||
<button class="button button-small woocow-icon-btn wc-dom-del"
|
||
title="Delete" data-domain="${esc(d.domain)}" style="color:#a00">
|
||
<span class="dashicons dashicons-trash"></span>
|
||
</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>`);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ── Edit Domain modal ─────────────────────────────────────────────────
|
||
|
||
$(document).on('click', '.wc-dom-edit', function () {
|
||
const domain = $(this).data('domain');
|
||
const d = domainsData[domain];
|
||
if (!d) return;
|
||
|
||
$('#wc-dom-edit-domain').val(domain);
|
||
$('#wc-dom-edit-name').text(domain);
|
||
$('#wc-dom-edit-desc').val(d.description || '');
|
||
$('#wc-dom-edit-mboxes').val(d.mailboxes || 10);
|
||
$('#wc-dom-edit-aliases').val(d.aliases || 400);
|
||
$('#wc-dom-edit-quota').val(d.quota || 10240);
|
||
$('#wc-dom-edit-defquota').val(d.defquota || 3072);
|
||
$('#wc-dom-edit-rl-value').val(d.rl_value || 0);
|
||
$('#wc-dom-edit-rl-frame').val(d.rl_frame || 's');
|
||
$('#wc-dom-edit-active').prop('checked', d.active == 1);
|
||
$('#wc-dom-edit-notice').text('');
|
||
|
||
// Load relayhosts into transport dropdown
|
||
const $sel = $('#wc-dom-edit-relayhost').html('<option value="0">— Direct delivery (no relay) —</option>');
|
||
ajax('woocow_admin_relayhosts_list', { server_id: domServerId }).done(rh => {
|
||
if (rh.success && Array.isArray(rh.data)) {
|
||
rh.data.forEach(r => {
|
||
$sel.append(`<option value="${r.id}">${esc(r.hostname)}</option>`);
|
||
});
|
||
$sel.val(d.relayhost || '0');
|
||
}
|
||
});
|
||
|
||
$('#wc-dom-edit-modal').show();
|
||
});
|
||
|
||
$('#wc-dom-edit-cancel').on('click', () => $('#wc-dom-edit-modal').hide());
|
||
|
||
$('#wc-dom-edit-save').on('click', () => {
|
||
const $note = $('#wc-dom-edit-notice').text('Saving…');
|
||
const domain = $('#wc-dom-edit-domain').val();
|
||
ajax('woocow_admin_domain_edit', {
|
||
server_id: domServerId,
|
||
domain,
|
||
description: $('#wc-dom-edit-desc').val(),
|
||
mailboxes: $('#wc-dom-edit-mboxes').val(),
|
||
aliases: $('#wc-dom-edit-aliases').val(),
|
||
quota: $('#wc-dom-edit-quota').val(),
|
||
defquota: $('#wc-dom-edit-defquota').val(),
|
||
rl_value: $('#wc-dom-edit-rl-value').val(),
|
||
rl_frame: $('#wc-dom-edit-rl-frame').val(),
|
||
relayhost: $('#wc-dom-edit-relayhost').val(),
|
||
active: $('#wc-dom-edit-active').is(':checked') ? 1 : 0,
|
||
}).done(res => {
|
||
if (res.success) {
|
||
$('#wc-dom-edit-modal').hide();
|
||
notice($('#wc-dom-notices'), 'success', `Domain <strong>${esc(domain)}</strong> updated.`);
|
||
loadDomains();
|
||
} else {
|
||
$note.html(`<span style="color:red">${esc(res.data)}</span>`);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ── Delete Domain ─────────────────────────────────────────────────────
|
||
|
||
$(document).on('click', '.wc-dom-del', function () {
|
||
const domain = $(this).data('domain');
|
||
if (!confirm(`Delete domain ${domain}? This will also delete all mailboxes, aliases, and data on the Mailcow server. This cannot be undone!`)) return;
|
||
ajax('woocow_admin_domain_delete', { server_id: domServerId, domain }).done(res => {
|
||
if (res.success) {
|
||
notice($('#wc-dom-notices'), 'success', `Domain <strong>${esc(domain)}</strong> deleted.`);
|
||
loadDomains();
|
||
} else {
|
||
notice($('#wc-dom-notices'), 'error', res.data);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 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());
|
||
});
|
||
|
||
// ── Rspamd history renderer ───────────────────────────────────────────
|
||
|
||
const rspamdActionBadge = (action) => {
|
||
const map = {
|
||
'no action': ['woocow-badge-green', 'No Action'],
|
||
'add header': ['woocow-badge-blue', 'Add Header'],
|
||
'rewrite subject': ['woocow-badge-orange', 'Rewrite Subject'],
|
||
'greylist': ['woocow-badge-orange', 'Greylist'],
|
||
'soft reject': ['woocow-badge-orange', 'Soft Reject'],
|
||
'reject': ['woocow-badge-red', 'Reject'],
|
||
};
|
||
const [cls, label] = map[(action || '').toLowerCase()] || ['woocow-badge-grey', esc(action || '—')];
|
||
return `<span class="woocow-badge ${cls}">${label}</span>`;
|
||
};
|
||
|
||
const rspamdScoreClass = (score, req) => {
|
||
const pct = (score || 0) / (req || 25);
|
||
if (pct < 0.3) return 'woocow-score-clean';
|
||
if (pct < 0.7) return 'woocow-score-low';
|
||
return 'woocow-score-high';
|
||
};
|
||
|
||
const rspamdDetail = (m) => {
|
||
let html = '<div class="woocow-rspamd-symbols">';
|
||
|
||
// Meta grid
|
||
const size = m.size ? ((m.size / 1024).toFixed(1) + ' KB') : '—';
|
||
const proc = m.time_real ? (m.time_real.toFixed(3) + 's') : '—';
|
||
html += `<div class="woocow-rspamd-meta">
|
||
<div class="woocow-rspamd-meta-item"><label>Sender SMTP</label>${esc(m.sender_smtp || '—')}</div>
|
||
<div class="woocow-rspamd-meta-item"><label>Sender MIME</label>${esc(m.sender_mime || '—')}</div>
|
||
<div class="woocow-rspamd-meta-item"><label>IP</label>${esc(m.ip || '—')}</div>
|
||
<div class="woocow-rspamd-meta-item"><label>Size</label>${size}</div>
|
||
<div class="woocow-rspamd-meta-item"><label>Process Time</label>${proc}</div>
|
||
<div class="woocow-rspamd-meta-item"><label>Message-ID</label><span style="word-break:break-all">${esc(m['message-id'] || '—')}</span></div>
|
||
</div>`;
|
||
|
||
// Thresholds
|
||
if (m.thresholds) {
|
||
const parts = Object.entries(m.thresholds)
|
||
.sort((a, b) => a[1] - b[1])
|
||
.map(([k, v]) => `<strong>${esc(k)}:</strong> ${v}`)
|
||
.join(' · ');
|
||
html += `<p class="woocow-rspamd-thresholds">Thresholds: ${parts}</p>`;
|
||
}
|
||
|
||
// Symbols table
|
||
if (m.symbols) {
|
||
const syms = Object.values(m.symbols);
|
||
const triggered = syms.filter(s => (s.metric_score || 0) !== 0)
|
||
.sort((a, b) => Math.abs(b.metric_score) - Math.abs(a.metric_score));
|
||
const info = syms.filter(s => (s.metric_score || 0) === 0)
|
||
.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
|
||
|
||
html += `<table class="woocow-rspamd-sym-table">
|
||
<thead><tr>
|
||
<th style="width:24px"></th>
|
||
<th style="width:30%">Symbol</th>
|
||
<th style="width:64px">Score</th>
|
||
<th>Description</th>
|
||
<th>Options</th>
|
||
</tr></thead><tbody>`;
|
||
|
||
[...triggered, ...info].forEach(s => {
|
||
const ms = s.metric_score || 0;
|
||
const icon = ms > 0
|
||
? '<span class="dashicons dashicons-warning" style="font-size:14px;color:#c0392b"></span>'
|
||
: ms < 0
|
||
? '<span class="dashicons dashicons-yes-alt" style="font-size:14px;color:#27ae60"></span>'
|
||
: '<span class="dashicons dashicons-minus" style="font-size:14px;color:#bbb"></span>';
|
||
const cls = ms > 0 ? 'woocow-sym-score-pos' : ms < 0 ? 'woocow-sym-score-neg' : 'woocow-sym-score-zero';
|
||
const sign = ms > 0 ? '+' : '';
|
||
const opts = (s.options || []).join(', ');
|
||
const zero = ms === 0 ? ' sym-zero' : '';
|
||
html += `<tr class="${zero}">
|
||
<td>${icon}</td>
|
||
<td class="woocow-sym-name"><code>${esc(s.name)}</code></td>
|
||
<td><span class="${cls}">${sign}${ms.toFixed(3)}</span></td>
|
||
<td>${esc(s.description || '')}</td>
|
||
<td style="color:#888">${esc(opts)}</td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table>';
|
||
}
|
||
html += '</div>';
|
||
return html;
|
||
};
|
||
|
||
const renderRspamdLog = (entries) => {
|
||
let html = `<div class="woocow-log-toolbar">
|
||
<strong>Rspamd History</strong>
|
||
<span style="color:#666;font-size:12px">${entries.length} messages (click row for details)</span>
|
||
</div>
|
||
<table class="wp-list-table widefat fixed striped woocow-rspamd-table woocow-table">
|
||
<thead><tr>
|
||
<th style="width:130px">Time</th>
|
||
<th>From</th>
|
||
<th>To</th>
|
||
<th>Subject</th>
|
||
<th style="width:90px">Score</th>
|
||
<th style="width:110px">Action</th>
|
||
<th style="width:34px"></th>
|
||
</tr></thead><tbody>`;
|
||
|
||
entries.forEach((m, i) => {
|
||
const dt = m.unix_time ? new Date(m.unix_time * 1000).toLocaleString() : '—';
|
||
const from = m.sender_mime || m.sender_smtp || '—';
|
||
const to = (m.rcpt_mime || m.rcpt_smtp || []).join(', ') || '—';
|
||
const subj = m.subject || '—';
|
||
const req = m.required_score || 25;
|
||
const scr = (m.score || 0).toFixed(2);
|
||
const cls = rspamdScoreClass(m.score, req);
|
||
html += `<tr>
|
||
<td style="font-size:11px;white-space:nowrap">${esc(dt)}</td>
|
||
<td style="font-size:11px">${esc(from)}</td>
|
||
<td style="font-size:11px">${esc(to)}</td>
|
||
<td style="font-size:12px">${esc(subj)}</td>
|
||
<td><span class="woocow-score ${cls}">${scr}<span style="opacity:.6;font-weight:400">/${req}</span></span></td>
|
||
<td>${rspamdActionBadge(m.action)}</td>
|
||
<td><button class="button button-small woocow-icon-btn wc-rspamd-toggle" data-idx="${i}" title="Details">
|
||
<span class="dashicons dashicons-arrow-down-alt2"></span>
|
||
</button></td>
|
||
</tr>
|
||
<tr class="woocow-rspamd-detail" id="woocow-rspamd-${i}" style="display:none">
|
||
<td colspan="7">${rspamdDetail(m)}</td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table>';
|
||
$('#wc-log-wrap').html(html);
|
||
};
|
||
|
||
$(document).on('click', '.wc-rspamd-toggle', function () {
|
||
const i = $(this).data('idx');
|
||
const $det = $(`#woocow-rspamd-${i}`);
|
||
const $ico = $(this).find('.dashicons');
|
||
if ($det.is(':visible')) {
|
||
$det.hide();
|
||
$ico.removeClass('dashicons-arrow-up-alt2').addClass('dashicons-arrow-down-alt2');
|
||
} else {
|
||
$det.show();
|
||
$ico.removeClass('dashicons-arrow-down-alt2').addClass('dashicons-arrow-up-alt2');
|
||
}
|
||
});
|
||
|
||
// ── Ratelimited log renderer ──────────────────────────────────────────
|
||
|
||
const renderRatelimitLog = (entries) => {
|
||
let html = `<div class="woocow-log-toolbar">
|
||
<strong>Rate-Limited Messages</strong>
|
||
<span style="color:#666;font-size:12px">${entries.length} entries</span>
|
||
</div>
|
||
<table class="wp-list-table widefat fixed striped woocow-table">
|
||
<thead><tr>
|
||
<th style="width:130px">Time</th>
|
||
<th>Sender</th>
|
||
<th>Recipient</th>
|
||
<th>Subject</th>
|
||
<th style="width:110px">IP</th>
|
||
<th style="width:120px">Rate Limit Rule</th>
|
||
<th style="width:110px">Queue ID</th>
|
||
</tr></thead><tbody>`;
|
||
|
||
entries.forEach(e => {
|
||
const dt = e.time ? new Date(e.time * 1000).toLocaleString() : '—';
|
||
const sender = e.header_from || e.from || '—';
|
||
const subject = e.header_subject || '—';
|
||
html += `<tr>
|
||
<td style="font-size:11px;white-space:nowrap">${esc(dt)}</td>
|
||
<td style="font-size:11px">${esc(sender)}</td>
|
||
<td style="font-size:11px">${esc(e.rcpt || '—')}</td>
|
||
<td style="font-size:12px">${esc(subject)}</td>
|
||
<td><code style="font-size:11px">${esc(e.ip || '—')}</code></td>
|
||
<td>
|
||
<span class="woocow-badge woocow-badge-orange" title="${esc(e.rl_info || '')}">${esc(e.rl_name || '—')}</span>
|
||
</td>
|
||
<td><code style="font-size:11px">${esc(e.qid || '—')}</code></td>
|
||
</tr>`;
|
||
});
|
||
|
||
html += '</tbody></table>';
|
||
$('#wc-log-wrap').html(html);
|
||
};
|
||
|
||
// ── Syslog-style renderer (postfix, dovecot, sogo, netfilter, acme) ──
|
||
|
||
const priorityBadge = (priority) => {
|
||
const p = (priority || 'info').toLowerCase();
|
||
const styles = {
|
||
debug: 'background:#f0f0f0;color:#888',
|
||
info: 'background:#e9ecef;color:#495057',
|
||
notice: 'background:#cce5ff;color:#004085',
|
||
warn: 'background:#fff3cd;color:#856404',
|
||
warning: 'background:#fff3cd;color:#856404',
|
||
err: 'background:#f8d7da;color:#721c24',
|
||
error: 'background:#f8d7da;color:#721c24',
|
||
crit: 'background:#721c24;color:#fff',
|
||
critical: 'background:#721c24;color:#fff',
|
||
alert: 'background:#721c24;color:#fff',
|
||
emerg: 'background:#1a1a1a;color:#fff',
|
||
};
|
||
const style = styles[p] || 'background:#e9ecef;color:#495057';
|
||
const label = p.charAt(0).toUpperCase() + p.slice(1);
|
||
return `<span class="woocow-badge" style="${style}">${label}</span>`;
|
||
};
|
||
|
||
const renderSyslogTable = (entries, label) => {
|
||
const hasProgram = entries.some(e => e.program);
|
||
const hasPriority = entries.some(e => e.priority);
|
||
let html = `<div class="woocow-log-toolbar">
|
||
<strong>${esc(label)}</strong>
|
||
<span style="color:#666;font-size:12px">${entries.length} entries</span>
|
||
</div>
|
||
<table class="wp-list-table widefat fixed striped woocow-table">
|
||
<thead><tr>
|
||
<th style="width:140px">Time</th>
|
||
${hasPriority ? '<th style="width:80px">Priority</th>' : ''}
|
||
${hasProgram ? '<th style="width:150px">Process</th>' : ''}
|
||
<th>Message</th>
|
||
</tr></thead><tbody>`;
|
||
entries.forEach(e => {
|
||
const dt = e.time ? new Date(parseInt(e.time) * 1000).toLocaleString() : '—';
|
||
html += `<tr>
|
||
<td style="font-size:11px;white-space:nowrap">${esc(dt)}</td>
|
||
${hasPriority ? `<td>${priorityBadge(e.priority)}</td>` : ''}
|
||
${hasProgram ? `<td><code style="font-size:11px">${esc(e.program || '—')}</code></td>` : ''}
|
||
<td style="font-size:12px;word-break:break-word">${esc(e.message || '—')}</td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table>';
|
||
$('#wc-log-wrap').html(html);
|
||
};
|
||
|
||
// ── API access log renderer ───────────────────────────────────────────
|
||
|
||
const renderApiLog = (entries) => {
|
||
const methodBadge = (method) => {
|
||
const colors = { GET:'#27ae60', POST:'#2271b1', PUT:'#e67e22', DELETE:'#c0392b', PATCH:'#8e44ad' };
|
||
const bg = colors[(method || '').toUpperCase()] || '#555';
|
||
return `<span style="display:inline-block;padding:1px 7px;border-radius:3px;background:${bg};color:#fff;font-size:11px;font-weight:700;font-family:monospace">${esc(method || '?')}</span>`;
|
||
};
|
||
let html = `<div class="woocow-log-toolbar">
|
||
<strong>API Access Log</strong>
|
||
<span style="color:#666;font-size:12px">${entries.length} requests</span>
|
||
</div>
|
||
<table class="wp-list-table widefat fixed striped woocow-table">
|
||
<thead><tr>
|
||
<th style="width:140px">Time</th>
|
||
<th style="width:72px">Method</th>
|
||
<th>Endpoint</th>
|
||
<th style="width:110px">Remote IP</th>
|
||
<th style="width:160px">Data</th>
|
||
</tr></thead><tbody>`;
|
||
entries.forEach(e => {
|
||
const dt = e.time ? new Date(e.time * 1000).toLocaleString() : '—';
|
||
html += `<tr>
|
||
<td style="font-size:11px;white-space:nowrap">${esc(dt)}</td>
|
||
<td>${methodBadge(e.method)}</td>
|
||
<td><code style="font-size:11px">${esc(e.uri || '—')}</code></td>
|
||
<td><code style="font-size:11px">${esc(e.remote || '—')}</code></td>
|
||
<td style="font-size:11px;color:#888;word-break:break-all">${esc(e.data || '—')}</td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table>';
|
||
$('#wc-log-wrap').html(html);
|
||
};
|
||
|
||
// ── Autodiscover log renderer ─────────────────────────────────────────
|
||
|
||
const renderAutodiscoverLog = (entries) => {
|
||
const svcBadge = (svc) => {
|
||
const map = {
|
||
activesync: ['woocow-badge-blue', 'ActiveSync'],
|
||
caldav: ['woocow-badge-green', 'CalDAV'],
|
||
carddav: ['woocow-badge-green', 'CardDAV'],
|
||
imap: ['woocow-badge-grey', 'IMAP'],
|
||
smtp: ['woocow-badge-grey', 'SMTP'],
|
||
};
|
||
const [cls, label] = map[(svc || '').toLowerCase()] || ['woocow-badge-grey', esc(svc || '—')];
|
||
return `<span class="woocow-badge ${cls}">${label}</span>`;
|
||
};
|
||
let html = `<div class="woocow-log-toolbar">
|
||
<strong>Autodiscover Log</strong>
|
||
<span style="color:#666;font-size:12px">${entries.length} requests</span>
|
||
</div>
|
||
<table class="wp-list-table widefat fixed striped woocow-table">
|
||
<thead><tr>
|
||
<th style="width:140px">Time</th>
|
||
<th>User</th>
|
||
<th style="width:120px">Service</th>
|
||
<th>User Agent</th>
|
||
</tr></thead><tbody>`;
|
||
entries.forEach(e => {
|
||
const dt = e.time ? new Date(e.time * 1000).toLocaleString() : '—';
|
||
html += `<tr>
|
||
<td style="font-size:11px;white-space:nowrap">${esc(dt)}</td>
|
||
<td style="font-size:12px">${esc(e.user || '—')}</td>
|
||
<td>${svcBadge(e.service)}</td>
|
||
<td style="font-size:11px;color:#666">${esc(e.ua || '—')}</td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table>';
|
||
$('#wc-log-wrap').html(html);
|
||
};
|
||
|
||
// ── Watchdog log renderer ─────────────────────────────────────────────
|
||
|
||
const renderWatchdogLog = (entries) => {
|
||
const healthBadge = (lvl) => {
|
||
const n = parseInt(lvl) || 0;
|
||
if (n >= 100) return '<span class="woocow-badge woocow-badge-green">Healthy</span>';
|
||
if (n >= 50) return '<span class="woocow-badge woocow-badge-orange">Degraded</span>';
|
||
return '<span class="woocow-badge woocow-badge-red">Critical</span>';
|
||
};
|
||
let html = `<div class="woocow-log-toolbar">
|
||
<strong>Watchdog Log</strong>
|
||
<span style="color:#666;font-size:12px">${entries.length} entries</span>
|
||
</div>
|
||
<table class="wp-list-table widefat fixed striped woocow-table">
|
||
<thead><tr>
|
||
<th style="width:140px">Time</th>
|
||
<th style="width:140px">Service</th>
|
||
<th style="width:90px">Status</th>
|
||
<th style="width:100px">Processes</th>
|
||
<th style="width:70px">Change</th>
|
||
</tr></thead><tbody>`;
|
||
entries.forEach(e => {
|
||
const dt = e.time ? new Date(parseInt(e.time) * 1000).toLocaleString() : '—';
|
||
const diff = parseInt(e.hpdiff) || 0;
|
||
const diffHtml = diff > 0
|
||
? `<span style="color:#27ae60;font-weight:700">+${diff}</span>`
|
||
: diff < 0
|
||
? `<span style="color:#c0392b;font-weight:700">${diff}</span>`
|
||
: `<span style="color:#bbb">±0</span>`;
|
||
html += `<tr>
|
||
<td style="font-size:11px;white-space:nowrap">${esc(dt)}</td>
|
||
<td><strong>${esc(e.service || '—')}</strong></td>
|
||
<td>${healthBadge(e.lvl)}</td>
|
||
<td style="font-family:monospace">${esc(e.hpnow || '0')}/${esc(e.hptotal || '0')}</td>
|
||
<td>${diffHtml}</td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table>';
|
||
$('#wc-log-wrap').html(html);
|
||
};
|
||
|
||
// ── Log load ──────────────────────────────────────────────────────────
|
||
|
||
$('#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;
|
||
}
|
||
const typeLabel = $('#wc-log-type option:selected').text();
|
||
if (type === 'rspamd-history') { renderRspamdLog(entries); return; }
|
||
if (type === 'ratelimited') { renderRatelimitLog(entries); return; }
|
||
if (type === 'api') { renderApiLog(entries); return; }
|
||
if (type === 'autodiscover') { renderAutodiscoverLog(entries); return; }
|
||
if (type === 'watchdog') { renderWatchdogLog(entries); return; }
|
||
// postfix, dovecot, sogo, netfilter, acme → syslog table
|
||
renderSyslogTable(entries, typeLabel + ' Log');
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Admin Quarantine Page ─────────────────────────────────────────────────
|
||
|
||
if ($('#wc-quar-server').length) {
|
||
let quarServerId = null;
|
||
|
||
$('#wc-quar-server').on('change', function () {
|
||
quarServerId = $(this).val();
|
||
$('#wc-quar-load').prop('disabled', !quarServerId);
|
||
});
|
||
|
||
const loadQuarantine = () => {
|
||
$('#wc-quar-wrap').html('<p>Loading…</p>');
|
||
ajax('woocow_admin_quarantine', { server_id: quarServerId }).done(res => {
|
||
if (!res.success) {
|
||
$('#wc-quar-wrap').html(`<div class="notice notice-error"><p>${esc(res.data)}</p></div>`);
|
||
return;
|
||
}
|
||
const msgs = Array.isArray(res.data) ? res.data : [];
|
||
if (!msgs.length) {
|
||
$('#wc-quar-wrap').html('<p>No quarantined messages.</p>');
|
||
return;
|
||
}
|
||
let html = `<table class="wp-list-table widefat fixed striped woocow-quarantine-table">
|
||
<thead><tr>
|
||
<th>Date</th><th>Sender</th><th>Recipient</th><th>Subject</th><th>Score</th><th>Actions</th>
|
||
</tr></thead><tbody>`;
|
||
msgs.forEach(m => {
|
||
const date = m.created ? new Date(m.created * 1000).toLocaleString() : '—';
|
||
const domain = (m.rcpt || '').split('@')[1] || '';
|
||
html += `<tr>
|
||
<td>${esc(date)}</td>
|
||
<td><code>${esc(m.sender)}</code></td>
|
||
<td>${esc(m.rcpt)}</td>
|
||
<td>${esc(m.subject)}</td>
|
||
<td>${esc(m.score)}</td>
|
||
<td class="woocow-actions">
|
||
<button class="button button-small woocow-icon-btn wc-quar-del"
|
||
title="Delete" data-qid="${m.id}" style="color:#a00">
|
||
<span class="dashicons dashicons-trash"></span>
|
||
</button>
|
||
<button class="button button-small woocow-icon-btn wc-quar-block"
|
||
title="Blacklist sender" data-sender="${esc(m.sender)}" data-domain="${esc(domain)}">
|
||
<span class="dashicons dashicons-shield-alt"></span>
|
||
</button>
|
||
</td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table>';
|
||
html += `<p class="description">${msgs.length} quarantined message(s). <em>Note: To release a message to inbox, use the link in the quarantine notification email or Webmail.</em></p>`;
|
||
$('#wc-quar-wrap').html(html);
|
||
});
|
||
};
|
||
|
||
$('#wc-quar-load').on('click', loadQuarantine);
|
||
|
||
$(document).on('click', '.wc-quar-del', function () {
|
||
if (!confirm('Permanently delete this quarantined message?')) return;
|
||
const qid = $(this).data('qid');
|
||
ajax('woocow_admin_quarantine_delete', { server_id: quarServerId, qid }).done(res => {
|
||
if (res.success) loadQuarantine();
|
||
else notice($('#wc-quar-notices'), 'error', res.data);
|
||
});
|
||
});
|
||
|
||
$(document).on('click', '.wc-quar-block', function () {
|
||
const sender = $(this).data('sender');
|
||
const domain = $(this).data('domain');
|
||
if (!domain) { notice($('#wc-quar-notices'), 'error', 'Could not determine recipient domain.'); return; }
|
||
if (!confirm(`Add ${sender} to the blacklist for domain ${domain}?`)) return;
|
||
ajax('woocow_admin_quarantine_block', {
|
||
server_id: quarServerId,
|
||
domain,
|
||
object_from: sender,
|
||
}).done(res => {
|
||
if (res.success) notice($('#wc-quar-notices'), 'success', `Sender <strong>${esc(sender)}</strong> blacklisted for <strong>${esc(domain)}</strong>.`);
|
||
else notice($('#wc-quar-notices'), 'error', res.data);
|
||
});
|
||
});
|
||
}
|
||
|
||
})(jQuery);
|