All checks were successful
check / check (push) Successful in 22s
Verifies the mobile layout from #5 with a real browser engine instead of by hand on a phone. make frontend-viewport-test builds dist/, serves it from the same digest-pinned nginx image and the same nginx.conf the shipping container uses, and drives a digest-pinned headless Chrome against it over CDP. Viewport widths are derived from the app's own CSS rather than from a list of phone models: the @media conditions in src/styles.css and any Tailwind responsive prefixes in the markup are parsed, and each breakpoint is tested one pixel below, exactly on, and one pixel above. max-width: 768px matches at 768, and a generic 375px test sails past that boundary entirely. Four anchor viewports are added with stated reasons: a 320px floor, a desktop baseline, and two phone-landscape sizes straddling the breakpoint. Assertions are on computed layout, not screenshots: horizontal overflow, elements past the viewport edge, clipped text (deliberate ellipsis truncation excluded), 44x44 minimum tap targets, and genuine reflow of the host rows checked on both flex-direction and geometry. Probing and gateway detection are asserted to still run at narrow widths, since the early-return mobile path rejected in #8 is what would silently regress. Screenshots are written to tmp/viewport/ as artifacts alongside the results, not as the evidence. puppeteer-core rather than playwright: it is the one variant of either that never downloads or bundles a browser, so the browser stays a digest-pinned image and the npm side is pinned by yarn.lock integrity. The browser container runs on an --internal docker network with no route off the host; the harness answers the app's latency probes itself from a fixed delay table so the rows render a realistic spread of value widths. Kept out of make check: it needs Docker and takes minutes, where make test has to stay under 20 seconds. Every check guards its own presence, so none can pass against a page it is not measuring. The tap-target check in particular would otherwise be inert: an empty undersized set means both "all controls are big enough" and "the selectors have gone stale", and the size comparison alone cannot tell those apart. Each selector therefore declares the minimum number of visible instances the page must contain, per selector rather than in total, so one stale selector out of four fails rather than only all four at once. Layout expectation is likewise refused rather than guessed. A width is narrow when a max-width block matches (desktop-first, what the app does today) or, for a min-width-only mobile-first set, when it falls below every breakpoint; a set mixing both cannot be resolved from the conditions alone, because which block owns the reflow is a property of the rules inside it, so the run fails with an explanation instead of testing the right widths against the wrong expectation. The harness was observed failing before being trusted, four times: a planted 900px fixed-width element in a host row; the mobile reflow rule neutered; the tap-target threshold lowered so nothing was undersized and .pin-btn then renamed, which took the check from 8/8 green at every touch viewport to failing at all six, naming the stale selector; and a second media block added so the breakpoint set mixed max and min, which aborted the run. All reverted. Against the current layout it reports two real defects, filed as #42 (horizontal overflow at 320px) and #43 (tap targets below 44x44).
188 lines
6.9 KiB
Markdown
188 lines
6.9 KiB
Markdown
NetWatch is an MIT-licensed JavaScript single-page application by
|
||
[@sneak](https://sneak.berlin) 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
|
||
|
||
```bash
|
||
# 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
|
||
```
|
||
|
||
## Entrypoints
|
||
|
||
This repository adheres to the
|
||
[Scripts to Rule Them All](https://github.com/github/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](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](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)
|
||
|
||
### 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](LICENSE).
|
||
|
||
## Author
|
||
|
||
[@sneak](https://sneak.berlin)
|