Split requests and errors into their own columns, and label every figure

A host serving thirty-five thousand requests that are all failures read as a busy host when the
two counts sat side by side in one run of numbers.
This commit is contained in:
Gmer4Lfe
2026-08-15 19:25:56 -04:00
parent 1041c4bba8
commit c0ace5a0ca
+40 -16
View File
@@ -251,13 +251,21 @@ require_once dirname(__DIR__) . '/include/ai_chat.php';
/* The one mark worth finding at a glance: it is the difference between a service the household
can reach and one the whole internet can. */
.vv-au-m.auth { background:#1a0d2a;border-color:#2a1a4a;color:#9c6ff7;font-weight:600; }
.vv-au-stats { display:flex;gap:8px;align-items:baseline;font-size:10px;white-space:nowrap; }
.vv-au-hits { color:#bbb;font-weight:600;font-size:11px; }
.vv-au-hits.dim { color:#2e2e2e;font-weight:normal; }
.vv-au-sent { color:#4a7a8a; }
.vv-au-err-n { color:#8a7a4a; }
.vv-au-err-n.bad{ color:#ef5350; }
.vv-au-last { color:#3a3a3a;margin-left:auto; }
/* Requests and errors are their own columns now. Right-aligned so the magnitudes line up down the
page — the whole point of the errors column is spotting the one host that is all failures. */
.vv-au-num { text-align:right;white-space:nowrap;width:1%; }
.vv-au-stat-n { font-size:13px;font-weight:700;color:#bbb;line-height:1.15; }
.vv-au-stat-n.dim { color:#2e2e2e;font-weight:normal;font-size:12px; }
.vv-au-stat-n.ok { color:#2d4a2d; }
.vv-au-stat-n.warn { color:#ff9800; }
.vv-au-stat-n.bad { color:#ef5350; }
/* The unit rides with the number rather than in the header alone: a column of bare figures next
to another column of bare figures is two things you have to look up to tell apart. */
.vv-au-stat-u { font-size:9px;font-weight:400;color:#4a4a4a;text-transform:uppercase;
letter-spacing:.05em;margin-left:4px; }
.vv-au-stat-s { font-size:9px;color:#3a3a3a;margin-top:1px; }
.vv-au-stat-s.warn { color:#8a6a2a; }
.vv-au-stat-s.bad { color:#a34; }
/* A disabled host is still listed — it is a thing you might re-enable — but it should not read
as part of what is currently serving. */
.vv-au-off td { opacity:.42; }
@@ -346,7 +354,7 @@ $tabs = ['proxies' => 'Proxies', 'users' => 'Users & Groups',
<div class="vv-au-loading" id="vv-au-proxy-loading">Loading…</div>
<table class="vv-au-tbl" id="vv-au-proxy-tbl" style="display:none">
<thead><tr>
<th>Host</th><th>Flags</th><th>Traffic</th><th>On</th><th></th>
<th>Host</th><th>Flags</th><th style="text-align:right">Requests</th><th style="text-align:right">Errors</th><th>On</th><th></th>
</tr></thead>
<tbody id="vv-au-proxy-body"></tbody>
</table>
@@ -677,15 +685,30 @@ function _renderProxies() {
p.block_exploits ? '<span class="vv-au-m dim" title="Block common exploits">blk</span>' : '',
].filter(Boolean).join('');
// Two columns, not one run of numbers. Requests and errors answer different questions — "is
// anything using this" and "is it working" — and a host serving thirty-five thousand requests
// that are all failures read as a busy host when the two sat side by side.
//
// Only shown once the aggregator has run. A dash is honest; a zero would read as "nobody has
// ever visited this" when it means "nothing has counted yet".
const hits = st ? `<span class="vv-au-hits" title="requests seen since tracking began">${_fmtNum(st.requests)}</span>` : '<span class="vv-au-hits dim">—</span>';
const sent = st ? `<span class="vv-au-sent" title="bytes sent to clients">${_fmtBytes(st.bytes)}</span>` : '';
const errs = (st && (st.s4xx || st.s5xx))
? `<span class="vv-au-err-n${st.s5xx ? ' bad' : ''}" title="${_fmtNum(st.s4xx)} client errors, ${_fmtNum(st.s5xx)} server errors">${_fmtNum(st.s4xx + st.s5xx)} err</span>`
: '';
const last = (st && st.last_seen)
? `<span class="vv-au-last" title="last request">${_ago(st.last_seen)}</span>` : '';
const bad = st ? (st.s4xx || 0) + (st.s5xx || 0) : 0;
const pct = (st && st.requests) ? (bad / st.requests * 100) : 0;
// Banded rather than a gradient: the question is which of "fine", "worth a look" and "this is
// not working" applies, and three answers do not need a hundred shades.
const pctCls = pct >= 50 ? ' bad' : (pct >= 5 ? ' warn' : '');
// Every figure carries its own word. A bare "15m" next to a byte count is a number you have to
// hover to identify, and the sub-line is the one place there is room to just say it.
const reqCell = st
? `<div class="vv-au-stat-n">${_fmtNum(st.requests)}<span class="vv-au-stat-u">requests</span></div>
<div class="vv-au-stat-s">${_fmtBytes(st.bytes)} sent${st.last_seen ? ' · last request ' + _ago(st.last_seen) + ' ago' : ' · never hit'}</div>`
: '<div class="vv-au-stat-n dim">—</div>';
const errCell = !st ? '<div class="vv-au-stat-n dim">—</div>'
: (bad === 0
? `<div class="vv-au-stat-n ok">0<span class="vv-au-stat-u">errors</span></div>`
: `<div class="vv-au-stat-n${pctCls}" title="${_fmtNum(st.s4xx)} client (4xx), ${_fmtNum(st.s5xx)} server (5xx)">${_fmtNum(bad)}<span class="vv-au-stat-u">errors</span></div>
<div class="vv-au-stat-s${pctCls}">${pct >= 10 ? Math.round(pct) : pct.toFixed(1)}% of requests</div>`);
return `<tr class="${enabled ? '' : 'vv-au-off'}">
<td>
@@ -693,7 +716,8 @@ function _renderProxies() {
<div class="vv-au-fwd">→ ${_esc(fwd)}</div>
</td>
<td><div class="vv-au-marks">${marks}</div></td>
<td><div class="vv-au-stats">${hits}${sent}${errs}${last}</div></td>
<td class="vv-au-num">${reqCell}</td>
<td class="vv-au-num">${errCell}</td>
<td>${_togHtml('proxy-' + p.id, enabled, enabled ? 'Enabled — click to disable' : 'Disabled — click to enable')}</td>
<td style="text-align:right">
<div class="vv-au-rule-acts">