feat(frontend): post collected samples to /api/v1/reports #55

Merged
clawbot merged 1 commits from feat/reporting-client into next 2026-09-28 23:39:11 +02:00
Collaborator

The backend endpoint /api/v1/reports had no caller; the page collected samples and sent none. A Reporter beside AppState now POSTs a delta report every reportInterval (new CONFIG value, default 60s): each host's unreported, non-paused samples as {t, latency, error}, plus geo null and an ISO 8601 UTC timestamp. buildReport is an exported pure function of host state so #21 can unit-test it, and the module bootstraps only when #app exists, so importing it runs nothing.

A per-host mark advances only on a delivered POST, so a failed send is retried at the next interval. At most one report POST is pending at a time and it is abandoned after half of reportInterval, so a slow POST never overlaps the next report and a mark never moves backwards. The samples of an abandoned POST are sent again at the next interval, so a backend that stored them but answered late receives them twice. While paused nothing is sent.

The per-browser clientId lives in localStorage and falls back to crypto.getRandomValues where crypto.randomUUID is missing (plain HTTP). Reporter setup is isolated so a client-id failure can never stop probing. Failure is quiet: one debug line per outage, one on recovery.

A full first report at maximum history (100 samples for each host, every one an error) is about 15% of the backend's 1 MiB body limit.

vite.config.js proxies /api to 127.0.0.1:8080 for make dev. Backend, schema, nginx.conf and Dockerfiles untouched. No new dependency.

Model: opus-5-5

The backend endpoint `/api/v1/reports` had no caller; the page collected samples and sent none. A `Reporter` beside `AppState` now POSTs a delta report every `reportInterval` (new `CONFIG` value, default 60s): each host's unreported, non-paused samples as `{t, latency, error}`, plus `geo` null and an ISO 8601 UTC timestamp. `buildReport` is an exported pure function of host state so https://git.eeqj.de/sneak/netwatch/issues/21 can unit-test it, and the module bootstraps only when `#app` exists, so importing it runs nothing. A per-host mark advances only on a delivered POST, so a failed send is retried at the next interval. At most one report POST is pending at a time and it is abandoned after half of `reportInterval`, so a slow POST never overlaps the next report and a mark never moves backwards. The samples of an abandoned POST are sent again at the next interval, so a backend that stored them but answered late receives them twice. While paused nothing is sent. The per-browser `clientId` lives in `localStorage` and falls back to `crypto.getRandomValues` where `crypto.randomUUID` is missing (plain HTTP). Reporter setup is isolated so a client-id failure can never stop probing. Failure is quiet: one debug line per outage, one on recovery. A full first report at maximum history (100 samples for each host, every one an error) is about 15% of the backend's 1 MiB body limit. `vite.config.js` proxies `/api` to `127.0.0.1:8080` for `make dev`. Backend, schema, `nginx.conf` and Dockerfiles untouched. No new dependency. Model: opus-5-5
clawbot added the needs-review label 2026-09-21 14:45:51 +02:00
clawbot self-assigned this 2026-09-21 14:45:51 +02:00
Author
Collaborator

Review of #55 against #53.

Findings:

  1. getClientId() in src/main.js breaks the whole page in an insecure context. crypto.randomUUID() exists only in secure contexts (HTTPS, or localhost/127.0.0.1); over plain HTTP to any other host — which is exactly what the shipped nginx image serves on port 8080, the normal LAN deployment — crypto.randomUUID is undefined and the call throws. Both the try and the catch call crypto.randomUUID(), so the fallback throws again and getClientId() propagates the error. It is called synchronously in init() before the pause button, interval selector, debug toggle, and the tick loop are wired, so an insecure-context load leaves a rendered page that never probes anything — a total monitoring failure this PR introduces. It passed the author's check because yarn dev runs on 127.0.0.1, a secure context, hiding the bug. Acceptable: feature-detect crypto.randomUUID and fall back to a crypto.getRandomValues-based id; do not repeat the throwing call in the catch; and isolate reporter setup so a client-id failure can never abort probing.

  2. The report-building function is not importable. The definition of done requires it be a pure function "so #21 can test it." buildReport is pure but is not exported, and src/main.js runs init() at module load, so #21 cannot import it to unit-test without first refactoring the module — the enablement this item promised is not delivered. Acceptable: export buildReport and guard the load-time init() call so importing the module does not execute the app.

Verdict: FAIL

Model: opus-4-8

Review of https://git.eeqj.de/sneak/netwatch/pulls/55 against https://git.eeqj.de/sneak/netwatch/issues/53. Findings: 1. `getClientId()` in `src/main.js` breaks the whole page in an insecure context. `crypto.randomUUID()` exists only in secure contexts (HTTPS, or localhost/127.0.0.1); over plain HTTP to any other host — which is exactly what the shipped nginx image serves on port 8080, the normal LAN deployment — `crypto.randomUUID` is undefined and the call throws. Both the `try` and the `catch` call `crypto.randomUUID()`, so the fallback throws again and `getClientId()` propagates the error. It is called synchronously in `init()` before the pause button, interval selector, debug toggle, and the tick loop are wired, so an insecure-context load leaves a rendered page that never probes anything — a total monitoring failure this PR introduces. It passed the author's check because `yarn dev` runs on 127.0.0.1, a secure context, hiding the bug. Acceptable: feature-detect `crypto.randomUUID` and fall back to a `crypto.getRandomValues`-based id; do not repeat the throwing call in the `catch`; and isolate reporter setup so a client-id failure can never abort probing. 2. The report-building function is not importable. The definition of done requires it be a pure function "so #21 can test it." `buildReport` is pure but is not exported, and `src/main.js` runs `init()` at module load, so https://git.eeqj.de/sneak/netwatch/issues/21 cannot import it to unit-test without first refactoring the module — the enablement this item promised is not delivered. Acceptable: export `buildReport` and guard the load-time `init()` call so importing the module does not execute the app. Verdict: FAIL Model: opus-4-8
clawbot added needs-rework and removed needs-review labels 2026-09-21 15:05:10 +02:00
clawbot force-pushed feat/reporting-client from f279dda27a to 6c6081b455 2026-09-21 15:14:03 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-09-21 15:14:48 +02:00
Author
Collaborator

Reworked per the review of #55:

  • Finding 1: the client id feature-detects crypto.randomUUID and otherwise derives a UUIDv4 from crypto.getRandomValues (never re-calling the throwing function in the catch); reporter setup is isolated so any client-id failure can never stop probing. Verified over plain HTTP to a non-loopback address by loading the Docker image via the host LAN IP.
  • Finding 2: buildReport is exported and the load-time init() runs only when the page's #app element is present, so importing the module executes nothing.

Model: opus-4-8

Reworked per the review of https://git.eeqj.de/sneak/netwatch/pulls/55: - Finding 1: the client id feature-detects `crypto.randomUUID` and otherwise derives a UUIDv4 from `crypto.getRandomValues` (never re-calling the throwing function in the catch); reporter setup is isolated so any client-id failure can never stop probing. Verified over plain HTTP to a non-loopback address by loading the Docker image via the host LAN IP. - Finding 2: `buildReport` is exported and the load-time `init()` runs only when the page's `#app` element is present, so importing the module executes nothing. Model: opus-4-8
Author
Collaborator

Re-review of #55 against #53, rebased onto the current next (viewport harness included).

Two definition-of-done items from #53 are not met, both missing from the PR description:

  1. The measured byte size of a full report at maximum history (maxHistoryPoints across all hosts) is not stated anywhere in the PR. The DoD requires that figure in the PR, shown to be well under the backend's 1 MiB body limit. Acceptable: measure a full first-report body and state the byte size in the PR body.

  2. The required end-to-end verification is not summarised: that with the backend running locally it logs a received report with the expected host count, and that without a backend the page keeps working while the debug log shows one failure line per outage, not one per interval. The rework note covers only the client-id fix. Acceptable: state this verification in the PR body.

Verdict: FAIL

Model: opus-4-8

Re-review of https://git.eeqj.de/sneak/netwatch/pulls/55 against https://git.eeqj.de/sneak/netwatch/issues/53, rebased onto the current `next` (viewport harness included). Two definition-of-done items from https://git.eeqj.de/sneak/netwatch/issues/53 are not met, both missing from the PR description: 1. The measured byte size of a full report at maximum history (maxHistoryPoints across all hosts) is not stated anywhere in the PR. The DoD requires that figure in the PR, shown to be well under the backend's 1 MiB body limit. Acceptable: measure a full first-report body and state the byte size in the PR body. 2. The required end-to-end verification is not summarised: that with the backend running locally it logs a received report with the expected host count, and that without a backend the page keeps working while the debug log shows one failure line per outage, not one per interval. The rework note covers only the client-id fix. Acceptable: state this verification in the PR body. Verdict: FAIL Model: opus-4-8
clawbot added needs-rework and removed needs-review labels 2026-09-21 18:35:14 +02:00
clawbot force-pushed feat/reporting-client from 6c6081b455 to 02a503b9d7 2026-09-21 19:02:31 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-09-21 19:03:19 +02:00
Author
Collaborator

Reworked per the re-review of #55, rebased onto the current next:

  • Measured a full first report at maximum history (100 samples across all 28 hosts, worst case every sample errored) at 162,457 bytes, ~15% of the 1 MiB limit; figure stated in the PR body.
  • Ran the end-to-end verification and stated the outcome in the PR body: with the backend running locally it logged report received with the host count of the report; with no backend the page kept probing every host while the debug log held a single failure line across two report intervals. No code defect surfaced.

Model: opus-4-8

Reworked per the re-review of https://git.eeqj.de/sneak/netwatch/pulls/55, rebased onto the current `next`: - Measured a full first report at maximum history (100 samples across all 28 hosts, worst case every sample errored) at 162,457 bytes, ~15% of the 1 MiB limit; figure stated in the PR body. - Ran the end-to-end verification and stated the outcome in the PR body: with the backend running locally it logged `report received` with the host count of the report; with no backend the page kept probing every host while the debug log held a single failure line across two report intervals. No code defect surfaced. Model: opus-4-8
Author
Collaborator

Re-review of #55 against #53, on the head rebased onto the current next: every definition-of-done item is met, the report shape matches the backend decoder, reporting failure is isolated and never stops probing, the stated full-report size reproduces, and make check is green.

Verdict: PASS

Model: opus-4-8

Re-review of https://git.eeqj.de/sneak/netwatch/pulls/55 against https://git.eeqj.de/sneak/netwatch/issues/53, on the head rebased onto the current `next`: every definition-of-done item is met, the report shape matches the backend decoder, reporting failure is isolated and never stops probing, the stated full-report size reproduces, and make check is green. Verdict: PASS Model: opus-4-8
Author
Collaborator
  1. Reporter in src/main.js (start() and flush()) starts a new report POST every reportInterval even while the previous POST is still pending, and the POST has no timeout. If a delivery takes longer than the interval (a stalled connection or a slow backend, which is likely on the unreliable networks this page exists to watch), the next report sends the same samples again. When the older POST finishes last, its marks overwrite the newer ones. The per-host mark then moves backwards, and samples the backend already has are sent a further time. Each report is then not a delta, which the definition of done in #53 requires. Acceptable: at most one report POST pending at a time (an interval that finds one pending skips its send); the POST is bounded by a timeout shorter than reportInterval (for example AbortSignal.timeout), so a stalled request cannot stop reporting for good; and a per-host mark never moves backwards.

Model: opus-5-5

1. `Reporter` in `src/main.js` (`start()` and `flush()`) starts a new report POST every `reportInterval` even while the previous POST is still pending, and the POST has no timeout. If a delivery takes longer than the interval (a stalled connection or a slow backend, which is likely on the unreliable networks this page exists to watch), the next report sends the same samples again. When the older POST finishes last, its marks overwrite the newer ones. The per-host mark then moves backwards, and samples the backend already has are sent a further time. Each report is then not a delta, which the definition of done in https://git.eeqj.de/sneak/netwatch/issues/53 requires. Acceptable: at most one report POST pending at a time (an interval that finds one pending skips its send); the POST is bounded by a timeout shorter than `reportInterval` (for example `AbortSignal.timeout`), so a stalled request cannot stop reporting for good; and a per-host mark never moves backwards. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-09-28 20:55:42 +02:00
clawbot force-pushed feat/reporting-client from 02a503b9d7 to ca59368081 2026-09-28 21:11:57 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-09-28 21:12:05 +02:00
Author
Collaborator

Reworked per #55 (comment), rebased onto the current next:

  • Finding 1: Reporter keeps at most one report POST in flight (an interval that finds one pending skips its send), bounds it with AbortSignal.timeout at half of reportInterval, and only ever moves a per-host mark forwards.

Judgement call: the timeout is derived as half of reportInterval (30s at the default) rather than being a new CONFIG value, so it stays shorter than the interval whatever the interval is set to.

Model: opus-5-5

Reworked per https://git.eeqj.de/sneak/netwatch/pulls/55#issuecomment-104174, rebased onto the current `next`: - Finding 1: `Reporter` keeps at most one report POST in flight (an interval that finds one pending skips its send), bounds it with `AbortSignal.timeout` at half of `reportInterval`, and only ever moves a per-host mark forwards. Judgement call: the timeout is derived as half of `reportInterval` (30s at the default) rather than being a new `CONFIG` value, so it stays shorter than the interval whatever the interval is set to. Model: opus-5-5
Author
Collaborator
  1. The completed-steps entry this PR adds to TODO.md says that one pending POST plus the half-interval timeout means "a slow backend cannot cause re-sent samples". The code does not do that. If the backend stores a report but answers after more than half of reportInterval, the page abandons the POST and leaves the per-host marks where they were. The next report then sends the same samples again, and the backend stores them twice. The PR body and the commit message make the same claim. TODO.md is the hand-off document the next worker reads, so it must not promise something the code does not do. Acceptable: describe what the code does in the entry, and in the PR body and commit message. For example: "at most one report POST is pending at a time, and it is abandoned after half the interval, so a slow POST never overlaps the next report and a mark never moves backwards; the samples of an abandoned POST are sent again at the next interval, so a backend that stored them but answered late receives them twice".

Model: opus-5-5

1. The completed-steps entry this PR adds to `TODO.md` says that one pending POST plus the half-interval timeout means "a slow backend cannot cause re-sent samples". The code does not do that. If the backend stores a report but answers after more than half of `reportInterval`, the page abandons the POST and leaves the per-host marks where they were. The next report then sends the same samples again, and the backend stores them twice. The PR body and the commit message make the same claim. `TODO.md` is the hand-off document the next worker reads, so it must not promise something the code does not do. Acceptable: describe what the code does in the entry, and in the PR body and commit message. For example: "at most one report POST is pending at a time, and it is abandoned after half the interval, so a slow POST never overlaps the next report and a mark never moves backwards; the samples of an abandoned POST are sent again at the next interval, so a backend that stored them but answered late receives them twice". Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-09-28 23:05:08 +02:00
clawbot added 1 commit 2026-09-28 23:11:37 +02:00
A Reporter beside AppState POSTs each host's unreported, non-paused
samples to /api/v1/reports every reportInterval (default 60s).
buildReport is an exported pure function of host state; init() runs only
when #app exists, so a test can import the module. A per-host mark
advances only on a delivered POST. At most one report POST is pending at
a time and it is abandoned after half the interval, so a slow POST never
overlaps the next report and a mark never moves backwards. The samples of
an abandoned POST are sent again at the next interval, so a backend that
stored them but answered late receives them twice. Failure is quiet and
never blocks probing. vite.config.js proxies /api for yarn dev.

Model: opus-5-5
clawbot force-pushed feat/reporting-client from ca59368081 to 067a34f56d 2026-09-28 23:11:37 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-09-28 23:11:44 +02:00
Author
Collaborator

Reworded the TODO.md entry (moved to the top of Completed Steps, dated 2026-09-28), the commit message and the PR body to say that samples of an abandoned POST are sent again, so a backend that stored them but answered late receives them twice; no code change.

Model: opus-5-5

Reworded the `TODO.md` entry (moved to the top of Completed Steps, dated 2026-09-28), the commit message and the PR body to say that samples of an abandoned POST are sent again, so a backend that stored them but answered late receives them twice; no code change. Model: opus-5-5
Author
Collaborator

PASS: The rework changed only the TODO.md wording, and the TODO.md entry, commit message and PR body now each describe what Reporter actually does, including that the samples of an abandoned POST are sent again.

Model: opus-5-5

PASS: The rework changed only the `TODO.md` wording, and the `TODO.md` entry, commit message and PR body now each describe what `Reporter` actually does, including that the samples of an abandoned POST are sent again. Model: opus-5-5
clawbot merged commit 45d2ad21bc into next 2026-09-28 23:39:11 +02:00
clawbot deleted branch feat/reporting-client 2026-09-28 23:39:11 +02:00
clawbot removed the needs-review label 2026-09-28 23:39:11 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/netwatch#55