diff --git a/assets/css/woocow.css b/assets/css/woocow.css index 98b256d..463f0d3 100644 --- a/assets/css/woocow.css +++ b/assets/css/woocow.css @@ -418,6 +418,85 @@ color: #fff; } +/* ── Rspamd history viewer ───────────────────────────────────── */ +.woocow-badge-blue { background: #cce5ff; color: #004085; } +.woocow-badge-orange { background: #fff3cd; color: #856404; } +.woocow-badge-red { background: #f8d7da; color: #721c24; } + +.woocow-rspamd-table th, +.woocow-rspamd-table td { vertical-align: middle; } +.woocow-rspamd-table .woocow-actions { white-space: nowrap; } + +.woocow-score { + display: inline-block; + padding: 2px 7px; + border-radius: 4px; + font-family: monospace; + font-weight: 700; + font-size: 12px; + white-space: nowrap; +} +.woocow-score-clean { background: #d4edda; color: #155724; } +.woocow-score-low { background: #fff3cd; color: #856404; } +.woocow-score-high { background: #f8d7da; color: #721c24; } + +/* Rspamd detail panel */ +.woocow-rspamd-detail td { padding: 0 !important; background: #f9fafc !important; } +.woocow-rspamd-symbols { + padding: 14px 20px; + border-top: 2px solid #e0e4ea; +} +.woocow-rspamd-meta { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 8px 16px; + margin-bottom: 12px; + padding: 10px 0; + border-bottom: 1px solid #e8e8e8; +} +.woocow-rspamd-meta-item label { + display: block; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + color: #999; + margin-bottom: 2px; +} +.woocow-rspamd-meta-item { + font-size: 12px; + word-break: break-all; +} +.woocow-rspamd-thresholds { + font-size: 11px; + color: #888; + margin: 4px 0 10px; +} +.woocow-rspamd-sym-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} +.woocow-rspamd-sym-table th { + background: #f0f1f5; + padding: 5px 8px; + text-align: left; + font-weight: 700; + border-bottom: 1px solid #dde; + white-space: nowrap; +} +.woocow-rspamd-sym-table td { + padding: 4px 8px; + border-bottom: 1px solid #eef; + vertical-align: top; +} +.woocow-rspamd-sym-table tr:hover td { background: #f5f7ff; } +.woocow-rspamd-sym-table tr.sym-zero td { opacity: .55; } + +.woocow-sym-score-pos { color: #c0392b; font-weight: 700; } +.woocow-sym-score-neg { color: #27ae60; font-weight: 700; } +.woocow-sym-score-zero { color: #aaa; } +.woocow-sym-name code { font-size: 11px; background: none; padding: 0; } + /* ── Spam filter panel ───────────────────────────────────────── */ .woocow-spam-panel { font-size: 13px; } .woocow-spam-panel input[type=range] { vertical-align: middle; } diff --git a/assets/js/woocow-admin.js b/assets/js/woocow-admin.js index c58f29a..f46f2ef 100644 --- a/assets/js/woocow-admin.js +++ b/assets/js/woocow-admin.js @@ -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 `${label}`; + }; + + 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 = '
'; + + // Meta grid + const size = m.size ? ((m.size / 1024).toFixed(1) + ' KB') : '—'; + const proc = m.time_real ? (m.time_real.toFixed(3) + 's') : '—'; + html += `
+
${esc(m.sender_smtp || '—')}
+
${esc(m.sender_mime || '—')}
+
${esc(m.ip || '—')}
+
${size}
+
${proc}
+
${esc(m['message-id'] || '—')}
+
`; + + // Thresholds + if (m.thresholds) { + const parts = Object.entries(m.thresholds) + .sort((a, b) => a[1] - b[1]) + .map(([k, v]) => `${esc(k)}: ${v}`) + .join(' · '); + html += `

Thresholds: ${parts}

`; + } + + // 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 += ` + + + + + + + `; + + [...triggered, ...info].forEach(s => { + const ms = s.metric_score || 0; + const icon = ms > 0 + ? '' + : ms < 0 + ? '' + : ''; + 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 += ` + + + + + + `; + }); + html += '
SymbolScoreDescriptionOptions
${icon}${esc(s.name)}${sign}${ms.toFixed(3)}${esc(s.description || '')}${esc(opts)}
'; + } + html += '
'; + return html; + }; + + const renderRspamdLog = (entries) => { + let html = `
+ Rspamd History + ${entries.length} messages (click row for details) +
+ + + + + + + + + + `; + + 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 += ` + + + + + + + + + + + `; + }); + html += '
TimeFromToSubjectScoreAction
${esc(dt)}${esc(from)}${esc(to)}${esc(subj)}${scr}/${req}${rspamdActionBadge(m.action)}
'; + $('#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('

No log entries.

'); 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(`
- ${esc(type.charAt(0).toUpperCase() + type.slice(1))} log + ${esc(typeLabel)} log ${entries.length} entries
${esc(text)}