Report files were named by a millisecond timestamp and created with
O_EXCL, so two flushes in the same millisecond, such as a flush for
size and the final flush at shutdown, got the same name and the second
failed, losing its reports. Each name now carries a number after the
timestamp that counts the files written since the server started, so
names still sort by time and never repeat. The new test flushes pairs
until one falls within one millisecond; the 1 ms pauses earlier tests
used to dodge the collision are gone.
Model: opus-5-5
POST /api/v1/reports stays unauthenticated but is bounded. Each client
address, as the trusted-proxy logic resolves it, may send
REPORTS_PER_MINUTE reports a minute (default 60, counted by
go-chi/httprate over a sliding minute); past that it gets 429 with
Retry-After. reportbuf refuses a report that would take the report
files past DATA_DIR_MAX_BYTES (default 1 GiB), counting the files
already in DATA_DIR and unwritten reports at their uncompressed size;
the handler answers 507. CORS adds nothing unless CORS_ALLOWED_ORIGINS
lists origins. A limit that is not a positive number, or an origin
that is not a plain scheme://host[:port], stops the server from
starting.
Model: opus-5-5
The root Dockerfile builds the only image; Dockerfile.backend is gone.
Its stages: lint, a Go stage that runs the tests and builds
netwatch-server, the node stage, and an nginx runtime. nginx serves
dist/ on 8080 and proxies /api/ and /.well-known/healthcheck to the
backend on 127.0.0.1:8081. bin/entrypoint.sh starts both, turns TERM or
INT into a stop of both, and exits non-zero when either exits on its
own. The backend runs as user netwatch and keeps reports on the /data
volume. New setting BIND_ADDRESS (empty: every interface). STOPSIGNAL is
SIGTERM, since the nginx image's SIGQUIT would miss the entrypoint.
script/docker is the org model verbatim.
Model: opus-5-5
Root make check, and with it the pre-commit hook, now gates the Go
backend too. The backend's Makefile targets are shims over
backend/script/*; script/cibuild builds both images and is the
workflow's only build step. Root make test runs both halves within one
30-second timeout.
Root make lint runs golangci-lint only in Docker, by building the lint
stage of Dockerfile.backend without the cache; the .golangci.yml drift
check moved into backend/script/lint. script/bootstrap installs no
linter: it reuses a Go at least as new as backend/go.mod asks for,
otherwise installs the pinned, hash-verified release, linked into
~/.local/bin without replacing anything it did not create. With VERSION
unset or empty, the backend version falls back to git describe inside a
git checkout, then to dev.
Model: opus-5-5
Add ReadHeaderTimeout and IdleTimeout to the http.Server as named constants beside the existing timeouts. Add a SecurityHeaders middleware (HSTS, a JSON-API CSP of default-src 'none'; frame-ancestors 'none', X-Frame-Options DENY, nosniff, Referrer-Policy, Permissions-Policy), registered before CORS so preflight responses carry it. Resolve the client IP from X-Forwarded-For / X-Real-IP only when the direct peer is in the trusted-proxy allowlist (loopback plus RFC1918 by default, configurable via TRUSTED_PROXIES); an untrusted peer's forwarded headers are ignored. Uses net/netip; no new dependency.
Model: opus-4-8 (implementation and review); claude-fable-5 (merge)
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.