Track what happens to every certificate, and show it on the Certs tab
NPM knows what a certificate is today and nothing about what it was, so ten of them could fail renewal for months — 1001 certbot runs, zero successes — without anything on any page saying so. Counts start at zero and are only ever observed; only first_seen is seeded, from NPM's own date.
This commit is contained in:
@@ -243,6 +243,32 @@ require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||
.vv-au-cert-days { font-size:28px;font-weight:700;line-height:1;margin:6px 0 2px; }
|
||||
.vv-au-cert-bar { height:3px;border-radius:2px;background:#1a1a1a;overflow:hidden;margin-top:8px; }
|
||||
.vv-au-cert-fill { height:100%;border-radius:2px;transition:width .3s; }
|
||||
|
||||
/* ── Cert history ────────────────────────────────────────────────────────── */
|
||||
.vv-au-tot { display:grid;grid-template-columns:repeat(auto-fit,minmax(96px,1fr));gap:8px; }
|
||||
.vv-au-tot-b { background:#161616;border:1px solid #222;border-radius:5px;padding:8px 10px;text-align:center; }
|
||||
.vv-au-tot-n { font-size:19px;font-weight:700;color:#bbb;line-height:1.1; }
|
||||
.vv-au-tot-n.bad { color:#ef5350; }
|
||||
.vv-au-tot-l { font-size:9px;color:#444;text-transform:uppercase;letter-spacing:.05em;margin-top:2px; }
|
||||
/* Fixed columns rather than flex: forty rows of counters only read as a table if the numbers line
|
||||
up down the page, and a domain name is the one part whose width varies. */
|
||||
.vv-au-hist-row { display:grid;grid-template-columns:1fr 34px 34px 74px 84px auto;gap:6px;
|
||||
align-items:center;padding:4px 12px;border-bottom:1px solid #1a1a1a;font-size:11px; }
|
||||
.vv-au-hist-row:last-child { border-bottom:none; }
|
||||
.vv-au-hist-row:hover { background:#141414; }
|
||||
.vv-au-hist-row.retired { background:#150c0c; }
|
||||
.vv-au-hist-row.removed { opacity:.45; }
|
||||
.vv-au-hist-dom { color:#bbb;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
|
||||
.vv-au-hist-n { text-align:right;font-weight:600;font-size:11px; }
|
||||
.vv-au-hist-n.ok { color:#4caf50; }
|
||||
.vv-au-hist-n.bad{ color:#ef5350; }
|
||||
.vv-au-hist-n.dim{ color:#333; }
|
||||
.vv-au-hist-t { color:#666;font-size:10px;text-align:right; }
|
||||
.vv-au-hist-e { color:#444;font-size:10px;font-family:monospace;text-align:right; }
|
||||
.vv-au-hist-note { font-size:9px;color:#3a3a3a;font-weight:normal;text-transform:none;letter-spacing:0; }
|
||||
.vv-au-strike { font-size:9px;color:#ff9800;background:#1f1200;border:1px solid #3a2800;
|
||||
border-radius:2px;padding:0 5px;white-space:nowrap; }
|
||||
.vv-au-dot { width:7px;height:7px;border-radius:50%;flex-shrink:0;display:inline-block; }
|
||||
</style>
|
||||
|
||||
<?php
|
||||
@@ -375,6 +401,24 @@ $tabs = ['proxies' => 'Proxies', 'users' => 'Users & Groups',
|
||||
<div class="vv-au-loading">Loading…</div>
|
||||
</div>
|
||||
<div id="vv-au-cert-cfg" style="margin-top:10px;font-size:10px;color:#3a3a3a;"></div>
|
||||
|
||||
<!-- Everything below is history rather than current state, and comes from
|
||||
DB_DIR/cert_history.json which Tools/cert_history.sh writes. The grid above answers "is
|
||||
this cert about to expire"; this answers "has this domain ever been trouble". -->
|
||||
<div id="vv-au-hist-totals" style="margin-top:14px"></div>
|
||||
<div class="vv-au-ug-grid" style="margin-top:10px;grid-template-columns:2fr 1fr">
|
||||
<div class="vv-au-card">
|
||||
<div class="vv-au-card-h">
|
||||
<span class="vv-au-card-title">Domain history</span>
|
||||
<span class="vv-au-hist-note" id="vv-au-hist-when"></span>
|
||||
</div>
|
||||
<div id="vv-au-hist-list"><div class="vv-au-loading">Loading…</div></div>
|
||||
</div>
|
||||
<div class="vv-au-card">
|
||||
<div class="vv-au-card-h"><span class="vv-au-card-title">DDNS</span></div>
|
||||
<div id="vv-au-ddns"><div class="vv-au-loading">Loading…</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -1769,9 +1813,84 @@ function _renderNpmCerts(data) {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── History, totals and DDNS ─────────────────────────────────────────────────
|
||||
// Read-only: the counters advance when Tools/cert_history.sh runs, not when this page is opened.
|
||||
// A page that wrote the history it displays would count a refresh as an observation.
|
||||
function _loadCertHistory() {
|
||||
const list = document.getElementById('vv-au-hist-list');
|
||||
const tot = document.getElementById('vv-au-hist-totals');
|
||||
const ddns = document.getElementById('vv-au-ddns');
|
||||
if (!list) return;
|
||||
fetch(CERT_API + '?action=history')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (!d.ok) throw new Error(d.error || 'history unavailable');
|
||||
|
||||
const t = d.totals || {};
|
||||
if (!t.tracked) {
|
||||
tot.innerHTML = '';
|
||||
list.innerHTML = '<div class="vv-au-empty">No history yet — run '
|
||||
+ '<code style="color:#5c9fd4">Tools/cert_history.sh</code> once to start tracking.</div>';
|
||||
ddns.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Failures are only red when there are any: a zero in an alarm colour trains you to ignore
|
||||
// the colour rather than the number.
|
||||
tot.innerHTML = `<div class="vv-au-tot">
|
||||
${_totBox(t.tracked, 'domains tracked')}
|
||||
${_totBox(t.active, 'active')}
|
||||
${_totBox(t.renewals, 'renewals seen')}
|
||||
${_totBox(t.failures, 'failures', t.failures ? 'bad' : '')}
|
||||
${_totBox(t.retired, 'retired', t.retired ? 'bad' : '')}
|
||||
${_totBox(t.checks, 'checks')}
|
||||
${_totBox(t.oldest_span || '—', 'longest tracked')}
|
||||
</div>`;
|
||||
|
||||
const when = document.getElementById('vv-au-hist-when');
|
||||
if (when) when.textContent = d.last_pass
|
||||
? 'last pass ' + new Date(d.last_pass * 1000).toLocaleString()
|
||||
: 'never run';
|
||||
|
||||
list.innerHTML = (d.rows || []).map(r => {
|
||||
const cls = r.state === 'retired' ? ' retired' : (r.state === 'removed' ? ' removed' : '');
|
||||
// Strikes are only worth showing while they are accruing; a retired domain already says so.
|
||||
const strike = (r.strikes && r.state === 'active')
|
||||
? `<span class="vv-au-strike">${r.strikes}/${d.strike_limit} strikes</span>` : '';
|
||||
const tag = r.state === 'retired' ? '<span class="vv-au-badge deny">retired</span>'
|
||||
: r.state === 'removed' ? '<span class="vv-au-badge nossl">removed</span>' : '';
|
||||
return `<div class="vv-au-hist-row${cls}">
|
||||
<span class="vv-au-hist-dom" title="${_esc(r.domain)}">${_esc(r.domain)}</span>
|
||||
<span class="vv-au-hist-n ok" title="renewals observed">${r.renewals}</span>
|
||||
<span class="vv-au-hist-n ${r.failures ? 'bad' : 'dim'}" title="times found expired">${r.failures}</span>
|
||||
<span class="vv-au-hist-t" title="first seen ${_esc(new Date(r.first_seen*1000).toISOString().slice(0,10))}">${_esc(r.tracked)}</span>
|
||||
<span class="vv-au-hist-e" title="current expiry">${_esc(r.expires || '—')}</span>
|
||||
${strike}${tag}
|
||||
</div>`;
|
||||
}).join('') || '<div class="vv-au-empty">Nothing tracked yet.</div>';
|
||||
|
||||
ddns.innerHTML = (d.ddns || []).length
|
||||
? d.ddns.map(c => `<div class="vv-au-user-row">
|
||||
<span class="vv-au-dot" style="background:${c.running ? '#4caf50' : '#ef5350'}"></span>
|
||||
<span class="vv-au-user-name" style="flex:1">${_esc(c.name)}</span>
|
||||
<span class="vv-au-user-email">${_esc(c.status)}</span>
|
||||
</div>`).join('')
|
||||
: '<div class="vv-au-empty">No DDNS containers configured.</div>';
|
||||
})
|
||||
.catch(e => {
|
||||
list.innerHTML = `<div class="vv-au-empty" style="color:#ef5350">${_esc(e.message || 'Failed')}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function _totBox(n, label, cls) {
|
||||
return `<div class="vv-au-tot-b"><div class="vv-au-tot-n ${cls || ''}">${_esc(String(n))}</div>`
|
||||
+ `<div class="vv-au-tot-l">${_esc(label)}</div></div>`;
|
||||
}
|
||||
|
||||
function _loadCerts(onDone) {
|
||||
const grid = document.getElementById('vv-au-cert-grid');
|
||||
grid.innerHTML = '<div class="vv-au-loading">Loading…</div>';
|
||||
_loadCertHistory();
|
||||
fetch(CERT_API + '?action=npm')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
|
||||
Reference in New Issue
Block a user