feat: rich log viewers for all remaining log types

- 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>
This commit is contained in:
2026-02-27 09:52:17 +01:00
parent 332efe59eb
commit 3cc73dc9ec

View File

@@ -952,6 +952,168 @@
$('#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', () => {
@@ -969,24 +1131,14 @@
$('#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; }
// 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}`;
return JSON.stringify(e);
}).join('\n');
$('#wc-log-wrap').html(`
<div class="woocow-log-toolbar">
<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>
`);
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');
});
});
}