feat: rich Rspamd history viewer with score badges and symbol details
- Rspamd log type renders a structured table instead of raw JSON
- Each row shows: timestamp, sender, recipient, subject, colour-coded
score (green/amber/red relative to required threshold), action badge
- Expand button reveals a detail panel per message with:
- Meta grid: Sender SMTP/MIME, IP, size, process time, Message-ID
- Threshold strip showing all configured score boundaries
- Symbol table sorted by impact, with dashicons (warning/check/minus),
+/- score in colour, description, and option values
- Zero-score informational symbols shown dimmed at the bottom
- Other log types (postfix, dovecot, etc.) still render as before
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -769,6 +769,152 @@
|
||||
$('#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');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Log load ──────────────────────────────────────────────────────────
|
||||
|
||||
$('#wc-log-load').on('click', () => {
|
||||
const sid = $('#wc-log-server').val();
|
||||
const type = $('#wc-log-type').val();
|
||||
@@ -784,7 +930,14 @@
|
||||
$('#wc-log-wrap').html('<p>No log entries.</p>');
|
||||
return;
|
||||
}
|
||||
// Render as a scrollable pre block
|
||||
|
||||
if (type === 'rspamd-history') {
|
||||
renderRspamdLog(entries);
|
||||
return;
|
||||
}
|
||||
|
||||
// Plain text log
|
||||
const typeLabel = $('#wc-log-type option:selected').text();
|
||||
const text = entries.map(e => {
|
||||
if (typeof e === 'string') return e;
|
||||
if (e.time && e.message) return `[${e.time}] ${e.message}`;
|
||||
@@ -792,7 +945,7 @@
|
||||
}).join('\n');
|
||||
$('#wc-log-wrap').html(`
|
||||
<div class="woocow-log-toolbar">
|
||||
<strong>${esc(type.charAt(0).toUpperCase() + type.slice(1))} log</strong>
|
||||
<strong>${esc(typeLabel)} log</strong>
|
||||
<span style="color:#666;font-size:12px">${entries.length} entries</span>
|
||||
</div>
|
||||
<pre class="woocow-log-pre">${esc(text)}</pre>
|
||||
|
||||
Reference in New Issue
Block a user