Show how each domain has behaved over a day, a week, a month and a year, not just now
This commit is contained in:
@@ -300,6 +300,52 @@ require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||
.vv-au-c-tog { width:5%; }
|
||||
.vv-au-c-act { width:6%; }
|
||||
|
||||
/* ── Uptime history card ─────────────────────────────────────────────────── */
|
||||
/* One grid for the header row and every data row, declared once, so the four period columns line
|
||||
up down the card without each row measuring its own. The domain column takes what is left and
|
||||
the four periods are equal — they hold the same kind of figure and an unequal split would read
|
||||
as one of them mattering more. */
|
||||
/* The domain column is capped rather than 1fr: at 1fr on a 3440 monitor it took half the card and
|
||||
squeezed the four graphs — the names are the label here, the graphs are the content. */
|
||||
.vv-au-hb-head, .vv-au-hb-row {
|
||||
display:grid; grid-template-columns:minmax(160px,300px) repeat(4, minmax(96px,1fr));
|
||||
gap:10px; align-items:center; padding:5px 12px;
|
||||
}
|
||||
.vv-au-hb-head { font-size:9px; letter-spacing:.06em; text-transform:uppercase; color:#3a3a3a;
|
||||
border-bottom:1px solid #1e1e1e; padding-top:7px; padding-bottom:7px; }
|
||||
.vv-au-hb-head span:not(:first-child) { text-align:center; }
|
||||
.vv-au-hb-row { border-bottom:1px solid #141414; }
|
||||
.vv-au-hb-row:last-child { border-bottom:none; }
|
||||
.vv-au-hb-row:hover { background:#141414; }
|
||||
.vv-au-hb-dom { font-size:11px; color:#bbb; overflow-wrap:anywhere; }
|
||||
.vv-au-hb-dom .vv-au-dot { margin-right:5px; }
|
||||
/* Figure over strip, both centred on the column. The number is what gets read; the strip is there
|
||||
to say whether that number is one long outage or a hundred small ones. */
|
||||
.vv-au-hb-cell { display:flex; flex-direction:column; align-items:center; gap:3px; }
|
||||
/* The state modifiers are namespaced, unlike the bare `ok2`/`warn`/`bad` the cells above use.
|
||||
Unraid's default-base.css carries `span.warn { background:var(--yellow-200); display:block;
|
||||
width:100% }` — element-qualified, so it only bites spans, and it beats nothing here on colour
|
||||
while still painting a pale yellow band the full width of the cell. Same lesson as the Tailwind
|
||||
`.fixed` collision: a bare state word on an element is a name somebody else already owns. */
|
||||
.vv-au-hb-pct { font-size:11px; font-family:monospace; line-height:1; }
|
||||
.vv-au-hb-pct.vv-au-hb-ok2 { color:#4caf50; }
|
||||
.vv-au-hb-pct.vv-au-hb-warn { color:#cddc39; }
|
||||
.vv-au-hb-pct.vv-au-hb-bad { color:#ef5350; }
|
||||
.vv-au-hb-pct.vv-au-hb-dim { color:#3a3a3a; }
|
||||
/* Bars are flex-1 with no fixed width, so twenty-four hourly bars and twelve monthly ones both
|
||||
fill their column exactly. min-width:1px keeps a bar visible rather than collapsing to nothing
|
||||
if the card is ever squeezed. */
|
||||
.vv-au-hb-bars { display:flex; align-items:flex-end; gap:1px; height:16px; width:100%;
|
||||
background:#111; border-radius:2px; padding:1px; }
|
||||
.vv-au-hb-bars i { flex:1; min-width:1px; border-radius:1px; align-self:flex-end; }
|
||||
.vv-au-hb-bars i.vv-au-hb-ok2 { background:#2d5a2d; }
|
||||
.vv-au-hb-bars i.vv-au-hb-warn { background:#5a5a1e; }
|
||||
.vv-au-hb-bars i.vv-au-hb-bad { background:#5a1e1e; }
|
||||
/* Not measured is not the same as measured at zero, and must never look like it. A gap reads as
|
||||
absence — flat, unsaturated, no height to compare against the bars beside it. */
|
||||
.vv-au-hb-bars i.vv-au-hb-gap { background:#191919; height:2px !important; }
|
||||
.vv-au-hb-cover { font-size:9px; color:#3a3a3a; font-family:monospace; line-height:1; }
|
||||
|
||||
/* ── Why ─────────────────────────────────────────────────────────────────── */
|
||||
/* On the figure's own line, not under the strip: the strip is 179px of fixed-width bars and is what
|
||||
sets this column's width, so anything below it would widen every row for the four that need it. */
|
||||
@@ -464,6 +510,27 @@ $tabs = ['proxies' => 'Proxies', 'users' => 'Users & Groups',
|
||||
</table>
|
||||
<div class="vv-au-empty" id="vv-au-proxy-empty" style="display:none">No proxy hosts configured.</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Uptime history ──────────────────────────────────────────────────────
|
||||
The table above answers "is this host up now, and how was the last hour". This answers
|
||||
"has it always been like this", which is the question the 24h figure cannot: a domain at
|
||||
99.9% today and 62% last month is a domain that got fixed, and one at 100% today and 100%
|
||||
for the year is a different thing entirely from one with no history at all.
|
||||
|
||||
Four windows side by side rather than a period toggle, because the comparison between them
|
||||
is the finding. A toggle would make "fine this week, bad this month" something you have to
|
||||
remember across two clicks. -->
|
||||
<div class="vv-au-card vv-au-hist-card" style="margin-top:12px">
|
||||
<div class="vv-au-card-h">
|
||||
<span class="vv-au-card-title">Uptime history</span>
|
||||
<span class="vv-au-hist-note" id="vv-au-up-note"></span>
|
||||
</div>
|
||||
<div class="vv-au-hb-head">
|
||||
<span></span>
|
||||
<span>24 hours</span><span>7 days</span><span>30 days</span><span>12 months</span>
|
||||
</div>
|
||||
<div id="vv-au-up-hist"><div class="vv-au-loading">Loading…</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -637,6 +704,7 @@ let _proxies = [], _certs = [];
|
||||
let _proxyStats = {};
|
||||
// Keyed by hostname, from Tools/uptime_probe.sh. Empty until the probe has run once.
|
||||
let _uptime = {};
|
||||
let _uptimePass = null;
|
||||
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
|
||||
@@ -777,7 +845,14 @@ function _loadProxies() {
|
||||
// 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_uptime', r => { _uptime = (r && r.ok && r.domains) ? r.domains : {}; if (proxiesLoaded) _renderProxies(); });
|
||||
_get('npm_uptime', r => {
|
||||
_uptime = (r && r.ok && r.domains) ? r.domains : {};
|
||||
_uptimePass = (r && r.last_pass) || null;
|
||||
if (proxiesLoaded) _renderProxies();
|
||||
// Independent of the proxy list: this card is keyed by what was probed, not by proxy id, and
|
||||
// it is the one thing on the tab that still says something when NPM itself is unreachable.
|
||||
_renderUptimeHistory();
|
||||
});
|
||||
_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; }
|
||||
@@ -861,6 +936,82 @@ function _renderProxies() {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Uptime history card ───────────────────────────────────────────────────────
|
||||
// Keyed by domain, not by proxy host, because that is the unit that was probed. The percentages
|
||||
// and the series both arrive from the server already rolled up — see vv_uptime_period() — so this
|
||||
// draws what it is given and never re-derives a window, which is what kept the figure and the
|
||||
// strip beside it from disagreeing on the row above.
|
||||
const _UP_PERIODS = [['h24', '24 hours'], ['d7', '7 days'], ['d30', '30 days'], ['m12', '12 months']];
|
||||
|
||||
// The same three-state banding the table uses. One rule, so a domain that is amber up there is
|
||||
// never green down here. Namespaced class names — see the CSS note on span.warn.
|
||||
function _upCls(pct) {
|
||||
if (pct === null || pct === undefined) return 'vv-au-hb-dim';
|
||||
return pct >= 99.5 ? 'vv-au-hb-ok2' : (pct >= 95 ? 'vv-au-hb-warn' : 'vv-au-hb-bad');
|
||||
}
|
||||
|
||||
function _renderUptimeHistory() {
|
||||
const box = document.getElementById('vv-au-up-hist');
|
||||
if (!box) return;
|
||||
const doms = Object.keys(_uptime);
|
||||
if (!doms.length) {
|
||||
box.innerHTML = '<div class="vv-au-empty">Nothing probed yet — the first pass runs within a minute.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Worst first, and "worst" is the lowest figure the domain has in any window: a host that is
|
||||
// fine today and was terrible last month is exactly what this card exists to surface, and
|
||||
// sorting on the 24h figure alone would bury it among the healthy ones. Unmeasured windows do
|
||||
// not count as bad — a domain with no history sinks rather than floats.
|
||||
const score = d => {
|
||||
const h = _uptime[d].hist || {};
|
||||
const vals = _UP_PERIODS.map(([k]) => h[k] && h[k].pct).filter(v => v !== null && v !== undefined);
|
||||
return vals.length ? Math.min(...vals) : 101;
|
||||
};
|
||||
doms.sort((a, b) => score(a) - score(b) || a.localeCompare(b));
|
||||
|
||||
box.innerHTML = doms.map(d => {
|
||||
const rec = _uptime[d], h = rec.hist || {};
|
||||
// Three states, not two: a domain the probe has never reached a verdict on is grey. Painting
|
||||
// it green because it is "not down" would be the card asserting something it does not know.
|
||||
const dot = rec.state === 'down' ? '#ef5350' : (rec.state === 'up' ? '#2d5a2d' : '#333');
|
||||
const dotT = rec.state === 'down' ? 'down now' : (rec.state === 'up' ? 'up now' : 'not yet probed');
|
||||
const cells = _UP_PERIODS.map(([k]) => {
|
||||
const p = h[k];
|
||||
if (!p) return '<div class="vv-au-hb-cell"><span class="vv-au-hb-pct vv-au-hb-dim">—</span></div>';
|
||||
|
||||
// A bar per calendar slot, null included. Height is the percentage, floored at 2px so a
|
||||
// month that was 100% down still draws something to point at — a zero-height bar is
|
||||
// indistinguishable from a slot that was never measured, and those two mean opposite things.
|
||||
const bars = (p.series || []).map(v => v === null
|
||||
? '<i class="vv-au-hb-gap" title="not measured"></i>'
|
||||
: `<i class="${_upCls(v)}" style="height:${Math.max(2, Math.round(v * 0.14))}px" title="${v}%"></i>`
|
||||
).join('');
|
||||
|
||||
// Partial windows say so instead of printing a percentage that is technically true of the
|
||||
// sample and misleading about the period. The store began on 2026-08-15; a month takes a
|
||||
// month, and pretending otherwise is how a dashboard earns distrust.
|
||||
const partial = p.have < p.want;
|
||||
const pct = (p.pct === null || p.pct === undefined)
|
||||
? '<span class="vv-au-hb-pct vv-au-hb-dim">—</span>'
|
||||
: `<span class="vv-au-hb-pct ${_upCls(p.pct)}">${p.pct >= 99.95 ? '100' : p.pct.toFixed(p.pct >= 99 ? 2 : 1)}<span style="color:#3a3a3a">%</span></span>`;
|
||||
return `<div class="vv-au-hb-cell">${pct}<div class="vv-au-hb-bars">${bars}</div>`
|
||||
+ (partial ? `<span class="vv-au-hb-cover">${p.have} of ${p.want}</span>` : '')
|
||||
+ `</div>`;
|
||||
}).join('');
|
||||
|
||||
return `<div class="vv-au-hb-row">
|
||||
<span class="vv-au-hb-dom"><span class="vv-au-dot" style="background:${dot}"
|
||||
title="${dotT}"></span>${_esc(d)}</span>
|
||||
${cells}
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
const note = document.getElementById('vv-au-up-note');
|
||||
if (note) note.textContent = doms.length + ' domains · probed every minute'
|
||||
+ (_uptimePass ? ' · last pass ' + _ago(_uptimePass) + ' ago' : '');
|
||||
}
|
||||
|
||||
// Uptime for the host, from Tools/uptime_probe.sh. A proxy host can carry several domains, so the
|
||||
// worst of them is what the row reports — a host is only as reachable as its least reachable name,
|
||||
// and averaging would hide one dead domain behind three healthy ones.
|
||||
|
||||
Reference in New Issue
Block a user