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).
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