diff --git a/README.md b/README.md index 04ba350..4b765ce 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,9 @@ Below it, a summary line covers the WAN hosts only: history - `Checks`: the rounds run since the page loaded or the interval changed +The health box and the summary take in a WAN host's result as soon as its row +shows it. + When no WAN host answers, the page checks 4 of them, picked at random, every half second. As soon as one answers, a new round starts. @@ -363,7 +366,8 @@ The page is built with Vite and Tailwind CSS v4. All its code is in - `HostState` and `AppState`: one host's history and status, and the whole page's state. - `measureLatency`: one check. `tick`: one round, which checks every host in - parallel and draws each row as soon as its check ends. + parallel. It draws each row as soon as its check ends, and with a WAN host's + row also the summary and the health box. - `SparklineRenderer`: the graphs. `buildUI`, `updateHostRow`, `updateSummary` and `updateHealthBox`: the page and its parts. - `Reporter`: builds and posts the reports. `buildReport` is a pure function of diff --git a/test/unit/main.test.js b/test/unit/main.test.js index d265b44..a0b8f8e 100644 --- a/test/unit/main.test.js +++ b/test/unit/main.test.js @@ -831,6 +831,44 @@ test("once every WAN target's check has failed, the summary's Reachable slot rea assert.equal(elements["#summary-reachable"].className, "text-red-400"); }); +// The network has just come back: every WAN target's last check failed, so +// the health box reads OFFLINE. In this round every WAN target but one +// answers after 10ms, and the one never answers. Each element of the summary +// and the health box is a plain object, as in the tests above; the debug +// log's panel is not there. +test("at a 30000ms interval, the summary and the health box take in each WAN target's answer while another WAN target's check is still waiting", async (t) => { + CONFIG.updateInterval = 30000; + t.mock.method(globalThis.document, "getElementById", (id) => + id.startsWith("summary-") || id.startsWith("health-") + ? (elements[`#${id}`] ??= {}) + : null, + ); + const state = new AppState([]); + for (const host of state.wan) { + host.pushSample(Date.now(), { latency: null, error: "timeout" }); + } + const silent = state.wan[0]; + mockTargets(t, (url) => (url === silent.url ? Infinity : 10)); + // The third tick, as in the tests above. + state.tickCount = 2; + + const round = tick(state, new AbortController().signal); + t.mock.timers.tick(10); + assert.equal(await settled(round), "still waiting"); + assert.equal(elements["#health-text"].textContent, "HEALTHY"); + assert.equal( + elements["#summary-reachable"].textContent, + `${state.wan.length - 1}/${state.wan.length}`, + ); + assert.equal(elements["#summary-max"].textContent, "10"); + // Checks counts finished rounds only. + assert.equal(elements["#summary-checks"].textContent, 2); + + t.mock.timers.tick(CONFIG.requestTimeout - 10); + assert.notEqual(await settled(round), "still waiting"); + assert.equal(elements["#summary-checks"].textContent, 3); +}); + // In the next two tests each of the header's clocks is a plain object, made // the first time updateClocks looks it up and kept in elements under "#" and // its id. diff --git a/web/src/main.js b/web/src/main.js index 85d16f1..2460c05 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -1126,11 +1126,6 @@ function updateHealthBox(state) { el.textContent = labels[health]; el.className = textColors[health]; box.className = `mt-4 p-3 rounded-lg border font-mono text-sm text-center ${boxColors[health]}`; - - if (health === "offline" || health === "degraded") - log.warning(`Health: ${labels[health]}`); - else if (health === "slow") log.warning(`Health: ${labels[health]}`); - else if (health === "healthy") log.notice(`Health: ${labels[health]}`); } // Writes the local time, with its time zone's name, and the UTC time into @@ -1301,12 +1296,13 @@ export async function tick(state, signal, onOffline) { log.debug(`Tick #${state.tickCount + 1} started`); - // Each host's row shows its result as soon as its check ends. The - // result is discarded if by then the user has paused or the next round - // has given up this one's checks, and in the first tick (tickCount is - // still 0), which is discarded as a whole below. The row is looked up - // when the check ends, as a pin click may have re-sorted the rows since - // the round started. + // Each host's row shows its result as soon as its check ends, and a WAN + // host's result goes into the summary and the health box at the same + // time. The result is discarded if by then the user has paused or the + // next round has given up this one's checks, and in the first tick + // (tickCount is still 0), which is discarded as a whole below. The row + // is looked up when the check ends, as a pin click may have re-sorted + // the rows since the round started. await Promise.all( state.allHosts.map(async (host) => { const r = await measureLatency(host.url, signal, host.method); @@ -1316,6 +1312,10 @@ export async function tick(state, signal, onOffline) { logCheck(host, r); host.pushSample(ts, r); updateHostRow(host, state.allHosts.indexOf(host)); + if (state.wan.includes(host)) { + updateSummary(state); + updateHealthBox(state); + } log.debug(`${host.name}: ${r.error ? r.error : r.latency + "ms"}`); }), ); @@ -1342,9 +1342,15 @@ export async function tick(state, signal, onOffline) { sortAndRebuildWAN(state); } + // Redraw the summary and the health box: a pause greys them out too, and + // Checks now counts this round. updateSummary(state); updateHealthBox(state); + const health = state.healthStatus(); + if (health === "healthy") log.notice("Health: HEALTHY"); + else log.warning(`Health: ${health.toUpperCase()}`); + const reachable = state.allHosts.filter( (h) => h.status === "online", ).length;