Interval change keeps unreported samples for the reporter (closes #124)
check / check (push) Waiting to run

The interval menu cleared every host's history, and the reporter builds
each report from that history, so the samples recorded since the last
delivered report were never sent. The menu now resets each host with
HostState.reset, which moves the history's entries to clearedHistory,
keeping the last 100 it has cleared, and buildReport reads both. A host
still pending after the reset is sent with the status of its last check.
The per-host marks are unchanged, so a sample a delivered report had is
not sent again, and the page's history still starts over. Reporter is
exported for the new unit tests.

Model: opus-5-5
This commit was merged in pull request #128.
This commit is contained in:
2026-10-14 12:23:13 +02:00
parent 406e073aee
commit 03bc282871
4 changed files with 148 additions and 15 deletions
+38 -12
View File
@@ -257,6 +257,14 @@ function latencyStats(latencies) {
};
}
// The status a check's error gives its host: "error" for a timeout,
// "offline" for any other error, and "online" for none.
function statusFromError(error) {
if (error === "timeout") return "error";
if (error) return "offline";
return "online";
}
export class HostState {
constructor(host, pinned = false) {
this.name = host.name;
@@ -265,6 +273,10 @@ export class HostState {
// error }, or a round skipped while paused, { timestamp,
// latency: null, paused: true }.
this.history = [];
// The last 100 (CONFIG.maxHistoryPoints) entries the interval menu
// has cleared from the history, which the reporter may not have
// sent yet.
this.clearedHistory = [];
this.lastLatency = null;
this.status = "pending"; // 'online' | 'offline' | 'error' | 'pending'
this.pinned = pinned;
@@ -280,9 +292,7 @@ export class HostState {
});
this._trim();
this.lastLatency = result.latency;
if (result.error === "timeout") this.status = "error";
else if (result.error) this.status = "offline";
else this.status = "online";
this.status = statusFromError(result.error);
this.consecutiveFailures = result.error
? this.consecutiveFailures + 1
: 0;
@@ -293,6 +303,18 @@ export class HostState {
this._trim();
}
// What the interval menu does to each host: the history starts over and
// the host waits for its next check. The history's entries move to
// clearedHistory, so the reporter still sends those it has not.
reset() {
this.clearedHistory = [...this.clearedHistory, ...this.history].slice(
-CONFIG.maxHistoryPoints,
);
this.history = [];
this.lastLatency = null;
this.status = "pending";
}
// The min, max, median and average latency of the checks in the history
// that got an answer.
historyStats() {
@@ -429,9 +451,12 @@ function getClientId() {
// Build the delta report body the backend decodes, plus the new per-host
// high-water marks. Pure function of the passed state: `hosts` is an array
// of { name, url, status, history }, `since` maps a host url to the Unix-ms
// timestamp of the last sample already reported for it, and `now` is a Date.
// Only non-paused samples newer than the mark are included. Returns null
// of { name, url, status, clearedHistory, history }, `since` maps a host url
// to the Unix-ms timestamp of the last sample already reported for it, and
// `now` is a Date. Only non-paused samples newer than the mark are included,
// from clearedHistory as well as history. A host whose status is pending,
// as the interval menu leaves it until its next check, is sent with the
// status of its last check, which is the last sample sent. Returns null
// when no host has an unreported sample.
export function buildReport(hosts, clientId, now, since) {
const reportHosts = [];
@@ -440,7 +465,7 @@ export function buildReport(hosts, clientId, now, since) {
const mark = since.get(host.url) ?? 0;
const samples = [];
let high = mark;
for (const p of host.history) {
for (const p of [...host.clearedHistory, ...host.history]) {
if (p.paused) continue;
if (p.timestamp <= mark) continue;
samples.push({
@@ -454,7 +479,10 @@ export function buildReport(hosts, clientId, now, since) {
reportHosts.push({
name: host.name,
url: host.url,
status: host.status,
status:
host.status === "pending"
? statusFromError(samples.at(-1).error)
: host.status,
history: samples,
});
marks.set(host.url, high);
@@ -481,7 +509,7 @@ export function buildReport(hosts, clientId, now, since) {
// Only one report is ever in flight, and it is abandoned after half the
// interval, so a slow POST can neither overlap the next report (which would
// carry the same samples) nor stall reporting for good.
class Reporter {
export class Reporter {
constructor(state, clientId, intervalMs) {
this.state = state;
this.clientId = clientId;
@@ -1502,9 +1530,7 @@ async function init() {
// Clear all host history and reset state
for (const host of state.allHosts) {
host.history = [];
host.lastLatency = null;
host.status = "pending";
host.reset();
}
state.tickCount = 0;