Files
netwatch/README.md
T
clawbot ca59368081
check / check (push) Successful in 9s
feat(frontend): post collected samples to /api/v1/reports (closes #53)
A Reporter beside AppState POSTs a JSON delta report of each host's
unreported, non-paused samples to the same-origin /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 it.
Per-host marks advance only on a delivered POST and never move backwards;
only one POST is in flight, abandoned after half the interval, so a slow
backend cannot cause re-sent samples. Failure is quiet and never blocks
probing.

The client id falls back to crypto.getRandomValues where crypto.randomUUID
is missing (plain HTTP to a non-localhost host). vite.config.js proxies
/api to 127.0.0.1:8080 for yarn dev.

Model: opus-5-5
2026-09-28 19:09:40 +00:00

7.7 KiB
Raw Blame History

NetWatch is an MIT-licensed JavaScript single-page application by @sneak that provides real-time network latency monitoring to common internet hosts, displayed with color-coded figures and sparkline graphs, served from a static bucket or Docker container.

Getting Started

# Install dependencies
yarn install

# Development server
yarn dev

# Production build
yarn build

# Preview production build
yarn preview

# Docker
docker build -t netwatch .
docker run -p 8080:8080 netwatch

yarn dev proxies /api to http://127.0.0.1:8080, so a locally running netwatch-server (see backend/) receives the reports the page posts.

Entrypoints

This repository adheres to the Scripts to Rule Them All standard: normalized scripts in script/ are the entrypoints for the development workflow, and the Makefile targets are thin shims that call them. We provide:

  • script/bootstrap — install all dependencies (pinned node via nvm if needed, yarn via corepack, yarn install --frozen-lockfile)
  • script/setup — make a fresh clone ready for development: bootstrap plus the git pre-commit hook
  • script/projectname — print the project name (used for the Docker image tag)
  • script/test — run the production build as the test (no unit tests yet)
  • script/lint — run prettier in check mode
  • script/fmt — format all files (writes)
  • script/fmt-check — check formatting (read-only)
  • script/check — run test, lint, and fmt-check
  • script/frontend-viewport-test — responsive-layout verification of the built frontend in a containerised headless Chrome (see test/viewport/README.md). Not part of script/check: it needs Docker and takes minutes.
  • script/docker — build the Docker image tagged via script/projectname
  • script/cibuild — CI entrypoint: plain docker build .
  • script/precommit — run by the git pre-commit hook; runs script/check
  • script/install-precommit — install the git pre-commit hook

Responsive layout

The narrow-viewport layout lives in the max-width: 768px media block in src/styles.css. It is verified automatically by make frontend-viewport-test, which drives a digest-pinned headless Chrome against the built dist/ and asserts on computed layout at widths derived from that CSS — one pixel either side of every breakpoint it declares, plus a 320px floor, a desktop baseline and two landscape sizes. See test/viewport/README.md for what it covers and what it genuinely cannot.

Rationale

When debugging network issues, it's useful to have a persistent at-a-glance view of latency and reachability to multiple well-known internet endpoints. NetWatch provides this as a zero-dependency SPA that can be deployed anywhere static files are served, with no backend required.

Design

The application is a single-page app built with Vite and Tailwind CSS v4. All code lives in src/main.js with a class-based architecture:

  • CONFIG: Frozen configuration object (update interval, timeouts, axis ticks, etc.)
  • HostState: Per-host state management — history buffer, latency tracking, status transitions
  • AppState: Top-level state container — WAN hosts, local hosts, pause state, aggregate stats
  • SparklineRenderer: Canvas 2D sparkline drawing with fixed axes, color-coded line segments, error regions, and DPR-aware scaling
  • UI functions: buildUI() constructs the DOM, updateHostRow() / updateSummary() / updateHealthBox() handle incremental updates
  • tick(): Main loop — measures all hosts in parallel via Promise.all, pushes samples, redraws UI. When paused, pushes blank markers (no probes, no false outage)
  • Reporter: Posts collected samples to the backend

Reporting

Every reportInterval (default 60s) the page POSTs a JSON report to the 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.

Monitoring targets

  • 22 WAN hosts: datavi.be, Anthropic API, OpenAI API, AWS Console, GCP Console, Azure, Cloudflare, Fastly, Akamai, GitHub, B2, 7 S3 regional endpoints (Cape Town, London, Bahrain, Tokyo, Sydney, Oregon, São Paulo), 4 GCS locational endpoints (Iowa, Belgium, Singapore, Sydney)
  • Local CPE: Cable modem at 192.168.100.1 (always monitored)
  • Local Gateway: Auto-detected on startup by probing common default gateway addresses (192.168.1.1, 192.168.0.1, 192.168.8.1, 10.0.0.1); first responder wins. Note: modern browsers enforce Private Network Access restrictions that block public-origin pages from reaching RFC1918 addresses, so local targets only work when NetWatch is served from localhost or a private address.

Local hosts are tracked separately from WAN stats.

Latency measurement

HEAD requests with mode: 'no-cors' and cache: 'no-store', timed with performance.now(). 1-second timeout; anything over 1000ms is clamped to unreachable. IPv4 only.

Color coding

Latency Color
< 50ms Green
< 100ms Lime
< 200ms Yellow
< 500ms Orange
>= 500ms Red
Unreachable Gray

Output structure

dist/
├── index.html
└── assets/
    ├── index-*.css
    └── index-*.js

Features

  • Real-time monitoring with 2s update interval and 300s history sparklines
  • Health indicator: green (HEALTHY) or red (DEGRADED) based on WAN reachability
  • Summary stats: reachable count, min/max/avg latency across WAN hosts only
  • Fixed chart axes: Y-axis 0–1000ms, X-axis 0–300s
  • Color-coded latency figures and sparkline line segments
  • Play/pause: pause stops probes but history keeps scrolling (blank gaps, no false outage)
  • Clickable service URLs
  • Canvas-based sparkline rendering with devicePixelRatio scaling
  • Zero runtime dependencies: all resources bundled into build artifacts

Deployment

After running yarn build, deploy the contents of the dist/ directory to any static file host (S3, GCS, Cloudflare Pages, Vercel, Netlify, GitHub Pages) or use the Docker image behind a reverse proxy.

The Docker image:

  • Listens on port 8080 by default (override with PORT env var)
  • Trusts X-Forwarded-For from RFC1918 reverse proxies (10/8, 172.16/12, 192.168/16)
  • Sends access logs to stdout
  • Caches static assets with immutable headers

Browser Compatibility

Requires a modern browser with ES modules, Fetch API, Canvas API, and CSS custom properties.

Limitations

  • CORS: Some hosts may block cross-origin HEAD requests. The app uses no-cors mode which allows the request but provides opaque responses. Latency is still measurable based on request timing.
  • Local gateway: The 192.168.100.1 endpoint requires the host to be accessible from your network.
  • Network conditions: Measurements reflect browser-to-endpoint latency, which includes your local network, ISP, and internet routing.

TODO

  • Add unit tests
  • Add eslint for JS linting (currently lint target runs prettier only)
  • Add configurable host list (environment variable or config file)
  • Add latency history export (CSV/JSON)
  • Add notification/alert when status changes to DEGRADED

License

MIT. See LICENSE.

Author

@sneak