Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6f325cff5 |
No files matched your search
@@ -24,16 +24,14 @@ Decide whether the repo moves to the layout `REPO_POLICIES.md` gives, with
|
|||||||
|
|
||||||
- 2026-10-14: changing the interval no longer drops the samples not yet reported
|
- 2026-10-14: changing the interval no longer drops the samples not yet reported
|
||||||
to the backend ([#124](https://git.eeqj.de/sneak/netwatch/issues/124)). The
|
to the backend ([#124](https://git.eeqj.de/sneak/netwatch/issues/124)). The
|
||||||
interval menu resets each host with the new `HostState.reset`, which moves the
|
interval menu calls the new `HostState.clearHistory`, which moves the
|
||||||
history's entries to `clearedHistory`, keeping the last 100 of all it has
|
history's entries to `clearedHistory`, keeping the last 100 as the history
|
||||||
cleared, and `buildReport` reads both, so the next report has the samples
|
does, and `buildReport` reads both, so the next report has the samples
|
||||||
recorded before the change that no report has delivered. Until its next check,
|
recorded before the change that no report has delivered. The page's history
|
||||||
such a host is sent with the status of its last check, not `pending`. The
|
still starts over, and the per-host marks still leave out of a report the
|
||||||
page's history still starts over, and the per-host marks still leave out of a
|
samples a delivered one had. A unit test sends a report, records samples,
|
||||||
report the samples a delivered one had. Unit tests reset a host as the
|
clears the history as the interval menu does and finds them in the next
|
||||||
interval menu does and find its unreported samples in the next report, and
|
report; `src/main.js` now exports `Reporter` for it
|
||||||
only the last 100 after more were cleared; `src/main.js` now exports
|
|
||||||
`Reporter` for them
|
|
||||||
- 2026-10-07: the files shared from `sneak/prompts` are its copies at commit
|
- 2026-10-07: the files shared from `sneak/prompts` are its copies at commit
|
||||||
`dd4027b` ([#113](https://git.eeqj.de/sneak/netwatch/issues/113)), with this
|
`dd4027b` ([#113](https://git.eeqj.de/sneak/netwatch/issues/113)), with this
|
||||||
repository's own entries after the shared content in `.gitignore`,
|
repository's own entries after the shared content in `.gitignore`,
|
||||||
|
|||||||
+15
-27
@@ -257,14 +257,6 @@ 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 {
|
export class HostState {
|
||||||
constructor(host, pinned = false) {
|
constructor(host, pinned = false) {
|
||||||
this.name = host.name;
|
this.name = host.name;
|
||||||
@@ -273,9 +265,8 @@ export class HostState {
|
|||||||
// error }, or a round skipped while paused, { timestamp,
|
// error }, or a round skipped while paused, { timestamp,
|
||||||
// latency: null, paused: true }.
|
// latency: null, paused: true }.
|
||||||
this.history = [];
|
this.history = [];
|
||||||
// The last 100 (CONFIG.maxHistoryPoints) entries the interval menu
|
// The entries the history held before the interval menu last
|
||||||
// has cleared from the history, which the reporter may not have
|
// cleared it, which the reporter may not have sent yet.
|
||||||
// sent yet.
|
|
||||||
this.clearedHistory = [];
|
this.clearedHistory = [];
|
||||||
this.lastLatency = null;
|
this.lastLatency = null;
|
||||||
this.status = "pending"; // 'online' | 'offline' | 'error' | 'pending'
|
this.status = "pending"; // 'online' | 'offline' | 'error' | 'pending'
|
||||||
@@ -292,7 +283,9 @@ export class HostState {
|
|||||||
});
|
});
|
||||||
this._trim();
|
this._trim();
|
||||||
this.lastLatency = result.latency;
|
this.lastLatency = result.latency;
|
||||||
this.status = statusFromError(result.error);
|
if (result.error === "timeout") this.status = "error";
|
||||||
|
else if (result.error) this.status = "offline";
|
||||||
|
else this.status = "online";
|
||||||
this.consecutiveFailures = result.error
|
this.consecutiveFailures = result.error
|
||||||
? this.consecutiveFailures + 1
|
? this.consecutiveFailures + 1
|
||||||
: 0;
|
: 0;
|
||||||
@@ -303,16 +296,14 @@ export class HostState {
|
|||||||
this._trim();
|
this._trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// What the interval menu does to each host: the history starts over and
|
// Starts the history over, as the interval menu does. Its entries move
|
||||||
// the host waits for its next check. The history's entries move to
|
// to clearedHistory, which keeps the last maxHistoryPoints of them, as
|
||||||
// clearedHistory, so the reporter still sends those it has not.
|
// the history does, so the reporter still sends those it has not.
|
||||||
reset() {
|
clearHistory() {
|
||||||
this.clearedHistory = [...this.clearedHistory, ...this.history].slice(
|
this.clearedHistory = [...this.clearedHistory, ...this.history].slice(
|
||||||
-CONFIG.maxHistoryPoints,
|
-CONFIG.maxHistoryPoints,
|
||||||
);
|
);
|
||||||
this.history = [];
|
this.history = [];
|
||||||
this.lastLatency = null;
|
|
||||||
this.status = "pending";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The min, max, median and average latency of the checks in the history
|
// The min, max, median and average latency of the checks in the history
|
||||||
@@ -454,10 +445,8 @@ function getClientId() {
|
|||||||
// of { name, url, status, clearedHistory, history }, `since` maps a host url
|
// 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
|
// 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,
|
// `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,
|
// from clearedHistory as well as history. Returns null when no host has an
|
||||||
// as the interval menu leaves it until its next check, is sent with the
|
// unreported sample.
|
||||||
// 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) {
|
export function buildReport(hosts, clientId, now, since) {
|
||||||
const reportHosts = [];
|
const reportHosts = [];
|
||||||
const marks = new Map();
|
const marks = new Map();
|
||||||
@@ -479,10 +468,7 @@ export function buildReport(hosts, clientId, now, since) {
|
|||||||
reportHosts.push({
|
reportHosts.push({
|
||||||
name: host.name,
|
name: host.name,
|
||||||
url: host.url,
|
url: host.url,
|
||||||
status:
|
status: host.status,
|
||||||
host.status === "pending"
|
|
||||||
? statusFromError(samples.at(-1).error)
|
|
||||||
: host.status,
|
|
||||||
history: samples,
|
history: samples,
|
||||||
});
|
});
|
||||||
marks.set(host.url, high);
|
marks.set(host.url, high);
|
||||||
@@ -1530,7 +1516,9 @@ async function init() {
|
|||||||
|
|
||||||
// Clear all host history and reset state
|
// Clear all host history and reset state
|
||||||
for (const host of state.allHosts) {
|
for (const host of state.allHosts) {
|
||||||
host.reset();
|
host.clearHistory();
|
||||||
|
host.lastLatency = null;
|
||||||
|
host.status = "pending";
|
||||||
}
|
}
|
||||||
state.tickCount = 0;
|
state.tickCount = 0;
|
||||||
|
|
||||||
|
|||||||
+29
-68
@@ -379,21 +379,11 @@ test("a check that fails in the first round, which is discarded, writes nothing
|
|||||||
assert.deepEqual(consoleLines("info"), []);
|
assert.deepEqual(consoleLines("info"), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The reports Reporter sent in this test, each as the hosts it sent.
|
// Every report is delivered here, and only Target has checks to report.
|
||||||
function reportedHosts() {
|
// Between the first report and the second the interval changes: the
|
||||||
return fetch.mock.calls.map(
|
// interval menu calls clearHistory on each target. The third report would
|
||||||
(call) => JSON.parse(call.arguments[1].body).hosts,
|
// hold no check the first two did not, so it is not sent.
|
||||||
);
|
test("checks recorded before an interval change and not yet reported go in the next report, and no check is reported twice", async (t) => {
|
||||||
}
|
|
||||||
|
|
||||||
// In the next two tests every report is delivered, and only Target has
|
|
||||||
// checks to report. target.reset() does to Target what the interval menu
|
|
||||||
// does to each target.
|
|
||||||
|
|
||||||
// The interval changes after the first report. The second report is sent
|
|
||||||
// before Target's first check since then, and the third after it. A fourth
|
|
||||||
// would hold no check the first three did not, so it is not sent.
|
|
||||||
test("checks recorded before an interval change and not yet reported go in the next report, with the status of the last check, and no check is reported twice", async (t) => {
|
|
||||||
const state = new AppState([
|
const state = new AppState([
|
||||||
{ name: "Target", url: "https://target.test" },
|
{ name: "Target", url: "https://target.test" },
|
||||||
]);
|
]);
|
||||||
@@ -405,42 +395,36 @@ test("checks recorded before an interval change and not yet reported go in the n
|
|||||||
await reporter.flush();
|
await reporter.flush();
|
||||||
target.pushSample(2000, { latency: 20, error: null });
|
target.pushSample(2000, { latency: 20, error: null });
|
||||||
target.pushSample(3000, { latency: null, error: "timeout" });
|
target.pushSample(3000, { latency: null, error: "timeout" });
|
||||||
target.reset();
|
target.clearHistory();
|
||||||
await reporter.flush();
|
|
||||||
target.pushSample(4000, { latency: 40, error: null });
|
target.pushSample(4000, { latency: 40, error: null });
|
||||||
await reporter.flush();
|
await reporter.flush();
|
||||||
await reporter.flush();
|
await reporter.flush();
|
||||||
|
|
||||||
|
assert.equal(fetch.mock.calls.length, 2);
|
||||||
|
const [first, second] = fetch.mock.calls.map(
|
||||||
|
(call) => JSON.parse(call.arguments[1].body).hosts,
|
||||||
|
);
|
||||||
const name = "Target";
|
const name = "Target";
|
||||||
const url = "https://target.test";
|
const url = "https://target.test";
|
||||||
assert.deepEqual(reportedHosts(), [
|
assert.deepEqual(first, [
|
||||||
[
|
{
|
||||||
{
|
name,
|
||||||
name,
|
url,
|
||||||
url,
|
status: "online",
|
||||||
status: "online",
|
history: [{ t: 1000, latency: 10, error: null }],
|
||||||
history: [{ t: 1000, latency: 10, error: null }],
|
},
|
||||||
},
|
]);
|
||||||
],
|
assert.deepEqual(second, [
|
||||||
[
|
{
|
||||||
{
|
name,
|
||||||
name,
|
url,
|
||||||
url,
|
status: "online",
|
||||||
status: "error",
|
history: [
|
||||||
history: [
|
{ t: 2000, latency: 20, error: null },
|
||||||
{ t: 2000, latency: 20, error: null },
|
{ t: 3000, latency: null, error: "timeout" },
|
||||||
{ t: 3000, latency: null, error: "timeout" },
|
{ t: 4000, latency: 40, error: null },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
status: "online",
|
|
||||||
history: [{ t: 4000, latency: 40, error: null }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
// The page's history starts over.
|
// The page's history starts over.
|
||||||
assert.deepEqual(target.history, [
|
assert.deepEqual(target.history, [
|
||||||
@@ -448,29 +432,6 @@ test("checks recorded before an interval change and not yet reported go in the n
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The interval changes twice, after 60 checks each time, before the first
|
|
||||||
// report. Of the 120 checks the interval menu cleared, the report has the
|
|
||||||
// last 100 (CONFIG.maxHistoryPoints).
|
|
||||||
test("of the checks interval changes cleared before they were reported, the next report has the last 100", async (t) => {
|
|
||||||
const state = new AppState([
|
|
||||||
{ name: "Target", url: "https://target.test" },
|
|
||||||
]);
|
|
||||||
const target = state.local[0];
|
|
||||||
const reporter = new Reporter(state, "client", CONFIG.reportInterval);
|
|
||||||
t.mock.method(globalThis, "fetch", async () => ({ ok: true }));
|
|
||||||
|
|
||||||
for (let n = 1; n <= 120; n++) {
|
|
||||||
target.pushSample(n * 1000, { latency: 10, error: null });
|
|
||||||
if (n % 60 === 0) target.reset();
|
|
||||||
}
|
|
||||||
await reporter.flush();
|
|
||||||
|
|
||||||
const sent = reportedHosts()[0][0].history;
|
|
||||||
assert.equal(sent.length, 100);
|
|
||||||
assert.equal(sent[0].t, 21000);
|
|
||||||
assert.equal(sent.at(-1).t, 120000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// The page shows < > " & and ' in a row's markup as
|
// The page shows < > " & and ' in a row's markup as
|
||||||
// < > " & and '.
|
// < > " & and '.
|
||||||
test(`a target whose name and URL hold < > " & and ' shows those characters in its row`, () => {
|
test(`a target whose name and URL hold < > " & and ' shows those characters in its row`, () => {
|
||||||
|
|||||||
Reference in new issue
Block a user