Root `make check` only ever ran the frontend, so the "main is always
green" policy was satisfied vacuously: the Go backend could be entirely
broken and the root gate stayed green.
- The backend moves onto scripts-to-rule-them-all. Its test, lint, fmt,
fmt-check, build, run and clean implementations now live in
`backend/script/`, and `backend/Makefile` is thin shims. The backend
is its own project (own module, README, LICENSE, linter config,
Dockerfile stage), and `Dockerfile.backend` only copies `backend/`
into its builder, so its scripts have to live under `backend/`.
- The root `script/test`, `script/lint`, `script/fmt` and
`script/fmt-check` now run the frontend step and then the matching
`backend/script/*` step, so `script/check` — and therefore the
pre-commit hook — gates both halves. The frontend-only steps moved
into `script/frontend-*` so nothing is duplicated.
- `script/bootstrap` now provisions the backend's toolchain as well,
because widening the gate without widening bootstrap left the
documented fresh-clone path (`make setup`) installing a pre-commit
hook that rejected every commit with `golangci-lint: not found`.
golangci-lint is installed at exactly `2.7.2`, the version
`Dockerfile.backend` pins, so local findings match CI. Go is reused
only when the installed version falls inside a window — at least
`backend/go.mod`'s floor, and no newer in major.minor than the Go the
pinned linter was built with — otherwise `go1.25.7` is installed. The
upper bound is load-bearing: golangci-lint links `go/types` from its
own build toolchain, so the pinned `2.7.2` (built with `go1.25.4`)
panics with "file requires newer Go version go1.26" against a host Go
1.26, which would leave `make setup` exiting 0 and every commit
rejected. Both tools come from a specific release archive whose sha256
is hardcoded here and verified before anything is unpacked — never an
install script piped to a shell — and both are symlinked onto `PATH`,
since nvm-style activation does not reach `make` or the git hook.
- `script/bootstrap` links only into `~/.local/bin` and never into a
system-wide prefix. `/usr/local/bin` is shared with other users and
with a package manager — on an Intel Mac it is the Homebrew prefix —
and pointing an entry there at one user's `$HOME` breaks it for
everyone else. It also refuses, non-zero, to replace anything it did
not create: only a symlink already pointing into its own toolchain
directory is overwritten, so a pre-existing binary is reported rather
than deleted. `corepack enable` is given `--install-directory` so its
four shims (`yarn`, `yarnpkg`, `pnpm`, `pnpx`) land inside that same
toolchain directory instead of beside the corepack binary, and only
`yarn` is linked onto `PATH`.
- `script/bootstrap` exits non-zero when it cannot guarantee the pinned
toolchain is the one the gate will run. Reporting success while
knowing a different linter or a newer Go precedes `~/.local/bin` is
the same defect this commit exists to remove, so the final step
re-resolves `go`, `gofmt`, `golangci-lint`, `node` and `yarn` against
the caller's own `PATH` and fails with what it found and how to fix
it. The three tools that carry a version constraint are re-checked
with the same predicates their installs use, not for bare presence:
`gofmt` is a gate tool — `backend/script/fmt-check` runs it — and its
output is not guaranteed identical across Go releases, so a `gofmt`
built by a different Go than the one that compiles the code counts as
missing. `go` and `gofmt` are relinked on every run in which the
pinned toolchain is the one in use, rather than only on the run that
unpacked the archive, so a deleted link is repaired instead of
falling through to whatever `gofmt` the host happens to have. The
failure text separates a tool that resolves to the wrong build
(something shadows `~/.local/bin`) from one that does not resolve at
all (nothing is shadowing it, it was never installed), and always
names a real directory rather than interpolating an unset one.
- `script/frontend-check` is the frontend half of the gate, exposed as
the `frontend-check` target, for the frontend Dockerfile: its build
stage is a node image with no Go toolchain. The backend half is gated
by `Dockerfile.backend`, and `script/cibuild` builds both images, so
the two Dockerfiles together still gate the whole repo. The
`backend-check` target is the mirror of it. Both targets are named
after the script they shim, like every other target.
- `script/cibuild` builds both images through one `build_image` helper,
and the Gitea workflow's only build step is `script/cibuild`; the raw
`docker build -f Dockerfile.backend .` is gone from the workflow.
`script/docker` likewise builds and tags both images.
- `backend/Makefile`'s `hooks` target is removed. It wrote the same
`.git/hooks/pre-commit` as `script/install-precommit`, so the two
clobbered each other and the developer silently ended up gating on
only one half of the repo. `script/install-precommit` is now the only
installer, and the hook it writes runs the repo-wide `script/check`.
- `backend/Makefile`'s `docker` target is removed too: the backend image
builds from the repo root with a root-level Dockerfile, so it belongs
to the root `script/docker` and `script/cibuild` rather than to a
backend script that would have to reach outside `backend/`.
- `backend/script/lint` verifies that `.golangci.yml` still matches its
pinned sha256 before running the linter. Offline hash comparison, no
network. The pin is marked provisional in the file: it is the config
currently on `main`, and the comment names PR #31 and the canonical
hash that must replace it when #31 lands.
- Every script locates the repo root with the mandated
`$(cd "$(dirname "$0")/.." && pwd -P)` idiom, `cd`s there, and calls
siblings as `"$ROOT/script/<name>"`; the `SCRIPT_DIR` variant is gone.
READMEs at the root and in `backend/` document every script, the
backend's Getting Started separates commands run from `backend/` from
those run at the repo root, and `TODO.md` records the change.
Resolves#11. The frontend/root `Dockerfile` ran only `RUN yarn build`, so `script/lint` and `script/fmt-check` (prettier) never gated CI — only a broken build failed it. (`script/cibuild`'s comment even claimed "the Dockerfile runs make check", which was false.) The backend `Dockerfile.backend` already runs `make check`; nothing covered the frontend's lint/fmt-check.
Change (single file, `Dockerfile`):
- `apk add ... git` -> `apk add ... git make` (build stage needs `make`).
- `RUN yarn build` -> `RUN make check` — which runs `script/test` (`yarn build`, producing `dist/`) then `script/lint` + `script/fmt-check`. `dist/` is still produced in one build (no redundant rebuild); the final nginx runtime image is unchanged.
Verified via a fresh clone (a worktree's `.git` pointer breaks `vite`'s `git rev-parse`, so builds must come from a real checkout — as CI's `actions/checkout` provides): positive `docker build` succeeds with in-image `make check` green; a negative test (a prettier-violating but build-valid file) makes the build fail at `make check`, confirming CI now goes red on a check regression, not just a broken build.
Left open for review (not merged).
Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #12
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
Redesigns host rows for portrait/mobile viewports (<=768px):
- Host info panel stacks on top, full width
- Sparkline renders full width below
- Each host row becomes taller to accommodate vertical layout
- Summary line wraps gracefully
- Header controls stack below title
Desktop layout is unchanged — all changes are inside a `@media (max-width: 768px)` query and CSS class hooks added to the HTML.
Closes#2
Co-authored-by: user <user@Mac.lan guest wan>
Reviewed-on: #5
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
Detect mobile devices via user agent and viewport width (<=768px).
On mobile, skip all checker initialization and render only the
header, description, and a styled 'Not yet available on mobile' box.
Desktop behavior is completely unchanged — the mobile check returns
early before any existing code runs.
Place the backend Dockerfile at repo root as Dockerfile.backend so
the build context includes .git, giving git describe access for
version stamping. Fix .gitignore pattern to anchor /netwatch-server
so it does not exclude cmd/netwatch-server/. Remove .git from
.dockerignore. Update CI workflow and backend Makefile docker target.
Update base image from golang:1.24-alpine to golang:1.25-alpine
to match go.mod requirement. Install golangci-lint by pinned commit
hash so make check passes inside the container. Update runtime
image to alpine:3.23.
Add .gitea/workflows/check.yml that builds both the root and
backend Docker images on push. Add LICENSE and README.md to the
backend subproject to match repo standards.
Introduce the Go backend (netwatch-server) with an HTTP API that
accepts telemetry reports and persists them as zstd-compressed JSONL
files. Reports are buffered in memory and flushed to disk when the
buffer reaches 10 MiB or every 60 seconds.
All updateHostRow() and greyOutUI() className assignments were
dropping col-span-2, causing the stats line to only span column 1
instead of both grid columns.
- Name cell gets min-w-[200px] so titles don't over-truncate
- Grid is flex-shrink-0 at 420px so it never squeezes
- Removed overflow-hidden that was clipping the stats line
Use minmax(0,1fr) for the name column so it can shrink below its
min-content width, and add overflow-hidden on the grid container
to clip any overflow at the boundary.
Replace absolute positioning with a 2-column CSS grid so the stats
line (col-span-2, text-right) is contained within the 480px block
and cannot extend past the left edge of the row.
- Nginx: extract config from Dockerfile heredoc to nginx.conf, hardcode
port 8080, remove envsubst templating
- Host row: add bottom padding so stats line stays within the row well
- Host row: two-layer layout with name/URL on the left (normal flow)
and latency/stats on the right (absolute positioned), preventing
overlap and keeping sparklines aligned
- Docker: install git in build stage and include .git in context so
vite can resolve commit hash for footer
- Add S3 ap-southeast-1 (Singapore) endpoint for AWS peering comparison
- Debug log: togglable panel with timestamped, level-tagged, color-coded
entries (error/warning/notice/info/debug) from throughout the app
- Median latency: added to per-host stats and summary (min/med/avg/max)
- Recovery probe: rapid 500ms polling of 4 random hosts when hard offline,
triggers normal tick as soon as connectivity returns
- Health status: multi-level (healthy/slow/degraded/offline) with
hard-offline detection for recovery probe activation
- First tick discarded to avoid DNS/TLS cold-start latency skew
- Added Google, S3 ap-southeast-1 (Singapore) to monitored hosts
- UI: reduced row padding, larger sparkline canvas, bigger axis labels,
pin icon hidden (but space preserved) for local network hosts
- Commit hash shown in footer via vite define plugin
Summary now shows current min/avg/max and history-window min/max.
Each host row has a pin icon that pins it to the top. Pinned hosts
sort alphabetically, unpinned sort by latency. datavi.be is pinned
by default.
Hetzner speed test servers drop the connection on HEAD requests,
causing fetch to throw a network error. GET works universally and
with no-cors mode the response is opaque anyway.
GCS locational endpoints were too slow (>1500ms). Replace with 6
Hetzner speed test servers (Nuremberg DE, Falkenstein DE, Helsinki
FI, Ashburn VA-US, Hillsboro OR-US, Singapore SG) which are genuine
per-DC HTTPS endpoints. Bump update interval from 2s to 3s.
The max-w-7xl (1280px) constraint left too much dead space between
the host wells and the window edges. Remove it so the layout uses
all available width.
The avg latency text below each host's big number is now color-coded
using the same thresholds as the main figure. The sparkline Y-axis
stays 0-1000ms — values between 1000-1500ms pin to the top of the
chart but still show their real value in the latency display.
Local CPE (192.168.100.1) is always monitored. On startup, probe
192.168.1.1, 192.168.0.1, 192.168.8.1, and 10.0.0.1 in parallel
and add whichever responds first as "Local Gateway".
- Add prettier (4-space indents) and reformat all files
- Add Makefile with test/lint/fmt/fmt-check/check/docker targets
- Add MIT LICENSE file
- Add REPO_POLICIES.md
- Fix Dockerfile: listen on 8080 with PORT env var via envsubst
- Restructure README.md with all required sections
- Set up pre-commit hook (make check)
- Update .prettierignore, .gitignore, .dockerignore
Architecture:
- Extract AppState and HostState classes (no global mutable state)
- Extract SparklineRenderer class with static methods
- Extract CONFIG object for all constants
- Break monolithic functions into focused helpers
Features:
- Clickable service URLs (open in new tab, existing styling)
- Health status box above summary (red DEGRADED if >half unreachable)
- Local Gateway separated into bottom group
- Local Gateway excluded from WAN min/max/avg summary stats
- Pause stops probes but history keeps scrolling (blank gaps, no false outage)
- WAN_HOSTS / LOCAL_HOSTS separation with indexed rendering