Compare commits
1
Commits
next
...
c6f325cff5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6f325cff5 |
@@ -142,9 +142,11 @@ same-origin path `/api/v1/reports`: a random per-browser `clientId` kept in
|
||||
`localStorage`, `geo` sent as null, and each host's unreported, non-paused
|
||||
samples (timestamp, latency, error). A per-host high-water mark makes every
|
||||
report a delta, so only new samples are sent; the mark advances only on a
|
||||
delivered report, and while paused nothing is sent. Delivery failure is quiet —
|
||||
one debug-log line per outage, retried at the next interval, never blocking
|
||||
probing. The report-building step is a pure function of host state.
|
||||
delivered report, and while paused nothing is sent. Changing the interval clears
|
||||
each host's history on the page; the samples in it not yet reported still go in
|
||||
the next report. Delivery failure is quiet — one debug-log line per outage,
|
||||
retried at the next interval, never blocking probing. The report-building step
|
||||
is a pure function of host state.
|
||||
|
||||
### Backend
|
||||
|
||||
|
||||
@@ -22,6 +22,16 @@ Decide whether the repo moves to the layout `REPO_POLICIES.md` gives, with
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 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
|
||||
interval menu calls the new `HostState.clearHistory`, which moves the
|
||||
history's entries to `clearedHistory`, keeping the last 100 as the history
|
||||
does, and `buildReport` reads both, so the next report has the samples
|
||||
recorded before the change that no report has delivered. The page's history
|
||||
still starts over, and the per-host marks still leave out of a report the
|
||||
samples a delivered one had. A unit test sends a report, records samples,
|
||||
clears the history as the interval menu does and finds them in the next
|
||||
report; `src/main.js` now exports `Reporter` for it
|
||||
- 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
|
||||
repository's own entries after the shared content in `.gitignore`,
|
||||
|
||||
+21
-7
@@ -265,6 +265,9 @@ export class HostState {
|
||||
// error }, or a round skipped while paused, { timestamp,
|
||||
// latency: null, paused: true }.
|
||||
this.history = [];
|
||||
// The entries the history held before the interval menu last
|
||||
// cleared it, which the reporter may not have sent yet.
|
||||
this.clearedHistory = [];
|
||||
this.lastLatency = null;
|
||||
this.status = "pending"; // 'online' | 'offline' | 'error' | 'pending'
|
||||
this.pinned = pinned;
|
||||
@@ -293,6 +296,16 @@ export class HostState {
|
||||
this._trim();
|
||||
}
|
||||
|
||||
// Starts the history over, as the interval menu does. Its entries move
|
||||
// to clearedHistory, which keeps the last maxHistoryPoints of them, as
|
||||
// the history does, so the reporter still sends those it has not.
|
||||
clearHistory() {
|
||||
this.clearedHistory = [...this.clearedHistory, ...this.history].slice(
|
||||
-CONFIG.maxHistoryPoints,
|
||||
);
|
||||
this.history = [];
|
||||
}
|
||||
|
||||
// The min, max, median and average latency of the checks in the history
|
||||
// that got an answer.
|
||||
historyStats() {
|
||||
@@ -429,10 +442,11 @@ 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
|
||||
// when no host has an unreported sample.
|
||||
// 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. Returns null when no host has an
|
||||
// unreported sample.
|
||||
export function buildReport(hosts, clientId, now, since) {
|
||||
const reportHosts = [];
|
||||
const marks = new Map();
|
||||
@@ -440,7 +454,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({
|
||||
@@ -481,7 +495,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,7 +1516,7 @@ async function init() {
|
||||
|
||||
// Clear all host history and reset state
|
||||
for (const host of state.allHosts) {
|
||||
host.history = [];
|
||||
host.clearHistory();
|
||||
host.lastLatency = null;
|
||||
host.status = "pending";
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
latencyClass,
|
||||
latencyHex,
|
||||
measureLatency,
|
||||
Reporter,
|
||||
tick,
|
||||
} from "../../src/main.js";
|
||||
|
||||
@@ -378,6 +379,59 @@ test("a check that fails in the first round, which is discarded, writes nothing
|
||||
assert.deepEqual(consoleLines("info"), []);
|
||||
});
|
||||
|
||||
// Every report is delivered here, and only Target has checks to report.
|
||||
// Between the first report and the second the interval changes: the
|
||||
// interval menu calls clearHistory on each target. The third report would
|
||||
// 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) => {
|
||||
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 }));
|
||||
|
||||
target.pushSample(1000, { latency: 10, error: null });
|
||||
await reporter.flush();
|
||||
target.pushSample(2000, { latency: 20, error: null });
|
||||
target.pushSample(3000, { latency: null, error: "timeout" });
|
||||
target.clearHistory();
|
||||
target.pushSample(4000, { latency: 40, error: null });
|
||||
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 url = "https://target.test";
|
||||
assert.deepEqual(first, [
|
||||
{
|
||||
name,
|
||||
url,
|
||||
status: "online",
|
||||
history: [{ t: 1000, latency: 10, error: null }],
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(second, [
|
||||
{
|
||||
name,
|
||||
url,
|
||||
status: "online",
|
||||
history: [
|
||||
{ t: 2000, latency: 20, error: null },
|
||||
{ t: 3000, latency: null, error: "timeout" },
|
||||
{ t: 4000, latency: 40, error: null },
|
||||
],
|
||||
},
|
||||
]);
|
||||
// The page's history starts over.
|
||||
assert.deepEqual(target.history, [
|
||||
{ timestamp: 4000, latency: 40, error: null },
|
||||
]);
|
||||
});
|
||||
|
||||
// The page shows < > " & and ' in a row's markup as
|
||||
// < > " & and '.
|
||||
test(`a target whose name and URL hold < > " & and ' shows those characters in its row`, () => {
|
||||
|
||||
Reference in New Issue
Block a user