Show what each proxy host is actually doing, and mark the ones behind Authelia

NPM writes an access log per host and counts nothing, so 475 MB of logs held the only answer to
"is anything using this". Aggregated on a schedule and read from a few kB of JSON; the row also
now says whether an auth_request block is in front of the site, which nothing showed before.
This commit is contained in:
Gmer4Lfe
2026-08-15 19:20:11 -04:00
parent 3db594af86
commit 1041c4bba8
5 changed files with 376 additions and 10 deletions
+95 -9
View File
@@ -239,7 +239,28 @@ require_once dirname(__DIR__) . '/include/ai_chat.php';
/* ── Misc ────────────────────────────────────────────────────────────────── */
.vv-au-empty { padding:24px;text-align:center;font-size:11px;color:#333; }
.vv-au-domain { font-size:12px;color:#bbb;font-weight:bold; }
.vv-au-fwd { font-size:10px;color:#444; }
.vv-au-fwd { font-size:10px;color:#444;font-family:monospace;margin-top:1px; }
/* ── Proxy row marks and traffic ─────────────────────────────────────────── */
/* Small marks rather than four more columns. Each one is a fact about the host that had no
column at all — most importantly whether an auth_request block is in front of it. */
.vv-au-marks { display:flex;flex-wrap:wrap;gap:3px;align-items:center; }
.vv-au-m { font-size:9px;padding:1px 5px;border-radius:2px;background:#141414;
border:1px solid #232323;color:#666;white-space:nowrap; }
.vv-au-m.dim { color:#3a3a3a;border-color:#1c1c1c; }
/* 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; }
/* 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; }
.vv-au-loading { color:#333;font-size:11px;padding:16px;text-align:center; }
/* ── Certs panel ─────────────────────────────────────────────────────────── */
@@ -325,7 +346,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>Domain</th><th>Forward</th><th>SSL</th><th>Status</th><th></th>
<th>Host</th><th>Flags</th><th>Traffic</th><th>On</th><th></th>
</tr></thead>
<tbody id="vv-au-proxy-body"></tbody>
</table>
@@ -479,6 +500,8 @@ const IS_OWNER = <?= $isOwner ? 'true' : 'false' ?>;
// ── State ─────────────────────────────────────────────────────────────────────
let _proxies = [], _certs = [];
// Keyed by proxy-host id, from Tools/npm_access_stats.sh. Empty until it has run once.
let _proxyStats = {};
let _users = [], _groups = [];
let _rules = [], _defaultPolicy = 'deny';
// The trailing comment on the default_policy line, carried so a save puts it back. The block is
@@ -613,6 +636,9 @@ function _loadProxies() {
_renderProxies();
}
// Stats are optional decoration — the list must render whether or not the aggregator has run,
// so this neither blocks _check() nor fails the load.
_get('npm_stats', r => { _proxyStats = (r && r.ok && r.hosts) ? r.hosts : {}; if (proxiesLoaded) _renderProxies(); });
_get('npm_certs', r => { _certs = r.certs || []; certsLoaded = true; _check(); });
_get('npm_proxies', r => {
if (!r.ok) { loading.innerHTML = '<span style="color:#ef5350">'+_esc(r.error)+'</span>'; return; }
@@ -631,14 +657,43 @@ function _renderProxies() {
const domains = (p.domain_names || []).join(', ');
const fwd = p.forward_scheme + '://' + p.forward_host + ':' + p.forward_port;
const hasSsl = p.certificate_id && p.certificate_id !== '0';
const sslBadge = hasSsl
? `<span class="vv-au-badge ssl">SSL</span>`
: `<span class="vv-au-badge nossl">none</span>`;
const enabled = p.enabled;
return `<tr>
<td><div class="vv-au-domain">${_esc(domains)}</div></td>
<td><div class="vv-au-fwd">${_esc(fwd)}</div></td>
<td>${sslBadge}</td>
const st = _proxyStats[String(p.id)] || null;
// Everything the host is doing that used to be invisible, as one row of small marks rather
// than four more columns: whether it is protected by an auth_request block, whether SSL is
// forced, and whether it carries custom nginx at all. 25 of these have config the page could
// not previously show, and the auth ones are the important case.
const adv = (p.advanced_config || '').trim();
const guarded = /auth_request/.test(adv);
const marks = [
hasSsl ? `<span class="vv-au-badge ssl" title="${_esc(_certName(p.certificate_id))}">SSL</span>`
: `<span class="vv-au-badge nossl">none</span>`,
p.ssl_forced ? '<span class="vv-au-m" title="HTTP redirected to HTTPS">force</span>' : '',
guarded ? '<span class="vv-au-m auth" title="auth_request — behind Authelia">auth</span>' : '',
adv && !guarded ? '<span class="vv-au-m" title="has Custom Nginx Configuration">nginx</span>' : '',
p.http2_support ? '<span class="vv-au-m dim" title="HTTP/2">h2</span>' : '',
p.hsts_enabled ? '<span class="vv-au-m dim" title="HSTS">hsts</span>' : '',
p.block_exploits ? '<span class="vv-au-m dim" title="Block common exploits">blk</span>' : '',
].filter(Boolean).join('');
// 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>` : '';
return `<tr class="${enabled ? '' : 'vv-au-off'}">
<td>
<div class="vv-au-domain">${_esc(domains)}</div>
<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>${_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">
@@ -650,6 +705,37 @@ function _renderProxies() {
}).join('');
}
function _certName(id) {
const c = _certs.find(x => String(x.id) === String(id));
return c ? (c.nice_name || (c.domain_names || []).join(', ')) : 'certificate ' + id;
}
// Compact because these sit in a column beside a domain name: 664,673 is wider than the name it
// belongs to and the exact figure is in the title attribute either way.
function _fmtNum(n) {
n = Number(n) || 0;
if (n >= 1e6) return (n / 1e6).toFixed(n < 1e7 ? 1 : 0) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(n < 1e4 ? 1 : 0) + 'k';
return String(n);
}
// Decimal, matching every network tool this will be compared against. Memory is the binary one.
function _fmtBytes(b) {
b = Number(b) || 0;
const u = ['B','kB','MB','GB','TB'];
let i = 0;
while (b >= 1000 && i < u.length - 1) { b /= 1000; i++; }
return (b < 10 && i ? b.toFixed(1) : Math.round(b)) + ' ' + u[i];
}
function _ago(ts) {
const s = Math.max(0, Math.floor(Date.now() / 1000 - ts));
if (s < 90) return s + 's';
if (s < 5400) return Math.round(s / 60) + 'm';
if (s < 172800)return Math.round(s / 3600) + 'h';
return Math.round(s / 86400) + 'd';
}
function _proxyModal(id) {
const p = id ? _proxies.find(x => x.id === id) : null;
const certOptions = _certs.map(c =>