diff --git a/.dockerignore b/.dockerignore
index 01b8e10..e70c17c 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -3,3 +3,4 @@ dist
.git
.DS_Store
*.log
+.claude
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..c24da45
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,4 @@
+dist/
+node_modules/
+yarn.lock
+.claude/
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..0a02bce
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,3 @@
+{
+ "tabWidth": 4
+}
diff --git a/Dockerfile b/Dockerfile
index 2083384..ba19b37 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,10 +10,10 @@ RUN yarn build
FROM nginx@sha256:15e96e59aa3b0aada3a121296e3bce117721f42d88f5f64217ef4b18f458c6ab
# Remove default config
RUN rm /etc/nginx/conf.d/default.conf
-# Custom nginx config: real_ip from RFC1918, access_log to stdout
-COPY <<'EOF' /etc/nginx/conf.d/netwatch.conf
+# Config template — envsubst replaces $PORT at container start
+COPY <<'EOF' /etc/nginx/netwatch.conf.template
server {
- listen 80;
+ listen $PORT;
server_name _;
root /usr/share/nginx/html;
@@ -44,4 +44,7 @@ EOF
COPY --from=build /app/dist /usr/share/nginx/html
-EXPOSE 80
+ENV PORT=8080
+EXPOSE 8080
+
+CMD ["/bin/sh", "-c", "envsubst '$PORT' < /etc/nginx/netwatch.conf.template > /etc/nginx/conf.d/netwatch.conf && exec nginx -g 'daemon off;'"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5b83a52
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 @sneak (https://sneak.berlin)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..f4e348b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,18 @@
+.PHONY: test lint fmt fmt-check check docker
+
+test:
+ timeout 30 yarn build
+
+lint:
+ yarn prettier --check .
+
+fmt:
+ yarn prettier --write .
+
+fmt-check:
+ yarn prettier --check .
+
+check: test lint fmt-check
+
+docker:
+ timeout 300 docker build -t netwatch .
diff --git a/README.md b/README.md
index 28d3006..60df9ea 100644
--- a/README.md
+++ b/README.md
@@ -1,39 +1,6 @@
-# NetWatch
+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.
-Real-time network latency monitor SPA designed to be served from a static bucket or Docker container.
-
-## Features
-
-- **Real-time monitoring**: Updates every 2s with 300s history sparklines
-- **9 WAN hosts**: Google Cloud, AWS, GitHub, Cloudflare, Azure, DigitalOcean, Fastly, Akamai, datavi.be
-- **Local network group**: Local gateway (192.168.100.1) tracked separately
-- **Health indicator**: Overall status box — green (HEALTHY) or red (DEGRADED) based on WAN reachability
-- **Summary stats**: Reachable count, min/max/avg latency across WAN hosts only
-- **Visual latency display**: Large color-coded figures with canvas sparkline graphs
-- **Color coding**:
- - Green: <50ms (excellent)
- - Lime: <100ms (good)
- - Yellow: <200ms (moderate)
- - Orange: <500ms (poor)
- - Red: >500ms (bad)
- - Gray: offline/unreachable
-- **Fixed chart axes**: Y-axis 0–1000ms, X-axis 0–300s
-- **Play/pause**: Pause stops probes but history keeps scrolling (blank gaps, no false outage)
-- **Clickable URLs**: Service URLs open in new tabs
-- **>1000ms clamped**: Anything over 1s is treated as unreachable
-- **Zero runtime dependencies**: All resources bundled into build artifacts
-- **IPv4 only**: Designed for IPv4 connectivity testing
-
-## Technical Details
-
-- Latency measured via HEAD requests with cache-busting
-- Uses `mode: 'no-cors'` to allow cross-origin requests where CORS headers aren't present
-- 1-second timeout for unresponsive hosts (>1000ms clamped to unreachable)
-- Canvas-based sparkline rendering with fixed axes
-- Tailwind CSS v4 for styling
-- Local gateway excluded from WAN summary statistics
-
-## Build
+## Getting Started
```bash
# Install dependencies
@@ -47,34 +14,48 @@ yarn build
# Preview production build
yarn preview
-```
-## Docker
-
-```bash
+# Docker
docker build -t netwatch .
-docker run -p 8080:80 netwatch
+docker run -p 8080:8080 netwatch
```
-The nginx config:
-- Trusts `X-Forwarded-For` from RFC1918 reverse proxies (10/8, 172.16/12, 192.168/16)
-- Access log goes to stdout
-- Static assets cached with immutable headers
+## Rationale
-## Deployment
+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.
-After running `yarn build`, deploy the contents of the `dist/` directory to any static file host:
+## Design
-- AWS S3
-- Google Cloud Storage
-- Cloudflare Pages
-- Vercel
-- Netlify
-- GitHub Pages
+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:
-Or use the Docker image behind a reverse proxy.
+- **`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)
-## Output Structure
+### Monitoring targets
+
+- **9 WAN hosts**: Google Cloud Console, AWS Console, GitHub, Cloudflare, Azure, DigitalOcean, Fastly, Akamai, datavi.be
+- **1 Local host**: Local Gateway (192.168.100.1), 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/
@@ -84,15 +65,32 @@ dist/
└── index-*.js
```
-All assets are bundled and minified. No external dependencies at runtime.
+## 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 modern browser with:
-- ES modules support
-- Fetch API
-- Canvas API
-- CSS custom properties
+Requires a modern browser with ES modules, Fetch API, Canvas API, and CSS custom properties.
## Limitations
@@ -100,6 +98,18 @@ Requires modern browser with:
- **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
+MIT. See [LICENSE](LICENSE).
+
+## Author
+
+[@sneak](https://sneak.berlin)
diff --git a/REPO_POLICIES.md b/REPO_POLICIES.md
new file mode 100644
index 0000000..ad3c91d
--- /dev/null
+++ b/REPO_POLICIES.md
@@ -0,0 +1,110 @@
+# Development Policies
+
+- Docker image references by tag are server-mutable, therefore using them is
+ an RCE vulnerability. All docker image references must use cryptographic
+ hashes to securely specify the exact image that is expected.
+
+- Correspondingly, `go install` commands using things like '@latest' are
+ also dangerous RCE. Whenever writing scripts or tools, ALWAYS specify go
+ install targets using commit hashes which are cryptographically secure.
+
+- Every repo with software in it must have a Makefile in the root. Each
+ such Makefile should support `make test` (runs the project-specific
+ tests), `make lint`, `make fmt` (writes), `make fmt-check` (readonly), and
+ `make check` (has `test`, `lint`, and `fmt-check` as prereqs), `make
+docker` (builds docker image).
+
+- Every repo should have a Dockerfile. If the repo contains non-server
+ software, the Dockerfile should bring up a development environment and
+ `make check` (i.e. the docker build should fail if the branch is not
+ green).
+
+- Platform-specific standard formatting should be used. `black` for python,
+ `prettier` for js/css/etc, `go fmt` for go. The only changes to default
+ settings should be to specify four-space indents where applicable (i.e.
+ everything except `go fmt`).
+
+- If local testing is possible (it is not always), `make check` should be a
+ pre-commit hook. If it is not possible, `make lint && make fmt-check`
+ should be a pre-commit hook.
+
+- If a working `make test` takes more than 20 seconds, that's a bug that
+ needs fixing. In fact, there should be a timeout specified in the
+ `Makefile` that fails it automatically if it takes >30s.
+
+- Docker builds should time out in 5 minutes or less.
+
+- `main` must always pass `make check`, no exceptions.
+
+- Do all changes on a feature branch. You can do whatever you want on a
+ feature branch.
+
+- We have a standardized `.golangci.yml` which we reuse and is _NEVER_ to be
+ modified by an agent, only manually by the user. It can be copied from
+ `~/dev/upaas/.golangci.yml` if it exists at that location.
+
+- When specifying images or packages by hash in Dockerfiles or
+ `docker-compose.yml`, put a comment above the line and show the version
+ and date at which it was current.
+
+- For javascript, always use `yarn` over `npm`.
+
+- Whenever writing dates, ALWAYS write YYYY-MM-DD (ISO 8601).
+
+- Simple projects should be configured with environment variables, as is
+ standard for Dockerized applications.
+
+- Dockerized web services should listen on the default HTTP port of 8080
+ unless overridden with the `PORT` environment variable.
+
+- The `README.md` is a project's primary documentation. It should contain
+ at a minimum the following sections:
+ - Description
+ - Include a short and complete description of the functionality and
+ purpose of the software as the first line in the readme. It must
+ include:
+ - the name
+ - the purpose
+ - the category (web server, SPA, command line tool, etc)
+ - the license
+ - the author
+ - eg: "µPaaS is an MIT-licensed Go web application by @sneak
+ that receives git-frontend webhooks and interacts with a
+ Docker server to build and deploy applications in realtime as
+ certain branches are updated."
+ - Getting Started
+ - a code block with copy-pasteable installation/use sections
+ - Rationale
+ - why does this exist?
+ - Design
+ - how is the program structured?
+ - TODO
+ - This is your TODO list for the project - update it meticulously,
+ even in between commits. Whenever planning, put your todo list in
+ the README so that a separate agent with new context can pick up
+ where you left off.
+ - License
+ - GPL or MIT or WTFPL - ask the user when beginning a new project
+ and include a LICENSE file in the root and in a section in the
+ README.
+ - Author
+ - @sneak (link `@sneak` to `https://sneak.berlin`).
+
+- When beginning a new project, initialize a git repo and make the first
+ commit simply the first version of the README.md in the root of the repo.
+
+- For Go packages, the module root is `sneak.berlin/go/...`, such
+ as `sneak.berlin/go/dnswatcher`.
+
+- We use SemVer always.
+
+- If no tag `1.0.0` or greater exists in the repository, modify the existing
+ migrations and assume no installed base or existing databases. If
+ `>=1.0.0`, database changes add new migration files.
+
+- New repos must have at a minimum the following files:
+ - `README.md`, `.git`, `.gitignore`
+ - `POLICIES.md` (copy from `~/Documents/_PROMPTS/POLICIES.md`)
+ - `Dockerfile`, `.dockerignore`
+ - for go: `go.mod`, `go.sum`, `.golangci.yml`
+ - for js: `package.json`
diff --git a/index.html b/index.html
index b698337..3af620d 100644
--- a/index.html
+++ b/index.html
@@ -1,13 +1,17 @@
-
+
-