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 = '
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 += `| + | Symbol | +Score | +Description | +Options | +
|---|---|---|---|---|
| ${icon} | +${esc(s.name)} |
+ ${sign}${ms.toFixed(3)} | +${esc(s.description || '')} | +${esc(opts)} | +
| Time | +From | +To | +Subject | +Score | +Action | ++ |
|---|---|---|---|---|---|---|
| ${esc(dt)} | +${esc(from)} | +${esc(to)} | +${esc(subj)} | +${scr}/${req} | +${rspamdActionBadge(m.action)} | ++ |
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(text)}