# Developer notes ## Building the UI into the binary `make build` runs `make web` first: `npm run build` in `web/`, then `web/dist` is copied to `internal/webui/dist`, which `//go:embed` packs into the binary. `internal/webui/dist` is git-ignored (only `.gitkeep` is tracked), so a build never dirties the tree. A plain `go build` without `make web` embeds a one-line "UI not built" page from `internal/webui/placeholder/`. nmon serves the UI at `/`. Unknown paths fall back to `index.html` for the client-side router, except `/api/`, `/docs`, `/openapi*`, `/metrics` and `/schemas`, which belong to the API. Files under `/assets/` are content-hashed and sent `immutable`; `index.html` is `no-cache`; text assets are served gzipped. ## In-app help (`/help`) The operator docs are shown at `/help` and `/help/#`. The markdown stays in `docs/` and is imported raw into the web build (`web/src/pages/help/docs.ts`, `?raw` imports), so there is no copy to keep in sync and the text always matches the binary. The page is a lazy chunk (react-markdown + remark-gfm), loaded on first visit. Raw HTML in the markdown is not rendered. To add a doc: write `docs/.md` (h1 title, h2/h3 sections; the h2/h3 headings become the table of contents and search targets), add one entry to `DOCS` in `docs.ts`, and link to other docs as `docs/.md#heading` (links to files that are not in `DOCS`, such as `docs/spec/`, render as plain text). To give a Settings section a "?", add `section id: heading slug` to `web/src/pages/help/helpLinks.ts`. Heading slugs are GitHub-style (`Behind a reverse proxy` is `behind-a-reverse-proxy`). Developer-only docs (this file, milestones, spec, DEVIATIONS, MAINTAINABILITY) are deliberately not listed. `/help` is an SPA route; `/docs` stays the Swagger UI. ## OpenAPI compatibility `scripts/openapi-check.sh` compares the current spec (`nmon openapi`, else `openapi.json`) with `openapi.json` at the latest `m*`/`v*` tag and fails on a removed route or method, a removed response field, a changed field type, a new required parameter or request field, or a removed enum value. A tag without `openapi.json` passes with a note. `OPENAPI_NEW=file.json` compares that file instead and `OPENAPI_BASE_TAG=m3` picks the base. The comparison is `internal/openapidiff`. ## `nmon-devtools` (separate dev binary) Developer helpers in their own executable, `make devtools` -> `nmon-devtools`; the shipped `nmon` does not contain them and never links the simulator. They need a running `nmon serve --insecure-http`. **backfill** writes synthetic history straight into the hot store so graphs have 30 days of data without waiting 30 days: nmon-devtools backfill --hot-url http://127.0.0.1:PORT --device SLUG --iface ULID [--iface ULID...] \ [--days 31] [--step 60s] [--pattern diurnal|flat] [--mean-mbps N] Per interface it writes `ifHCInOctets`, `ifHCOutOctets` (monotonic, diurnal rate with noise and bursts) and `ifOperStatus`, ending one step before the first live sample. When live samples exist the history is shifted so its last value joins the live counter with no reset (this needs a live counter larger than the history, about 3e13 bytes for 31 days at 100 Mbit/s; otherwise a warning says the join will look like a reset). The hot URL is in the serve log (`metric stores up hot=...`). Samples older than hot retention are dropped by the store. **seed-demo** builds a small simulated network and back-fills it, for the gate and for screenshots: nmon serve --data-dir D --insecure-http --poll-interval 10s ... nmon-devtools seed-demo --api http://127.0.0.1:8443 --hot-url http://127.0.0.1:PORT \ [--env-out env] [--counters-rec testdata/snmprec/counters.snmprec] It starts the simulated SNMP agents in-process on `[::1]`, each on its own port (passed to the API as `snmp.port`), adds the devices through the API (a 48-port switch, access switches, routers, a firewall, a UPS, the counters fixture, a v1 device with 32-bit counters, and one whose agent is stopped after seeding so it reads as down), waits for live polls, back-fills 31 days, writes `NMON_DEVICE`, `NMON_DEVICE_48`, `NMON_DEVICE_32BIT` and `NMON_DEVICE_DOWN` to `--env-out`, and keeps the agents answering until interrupted. The simulated 64-bit counters start at 1e14 so history fits below them. **oidc-mock** runs `internal/auth/oidctest` as a standalone OpenID Connect issuer for the M6 gate and for screenshots: nmon-devtools oidc-mock --issuer-file F --port-file P [--client-id nmon --client-secret s3cret] curl 'http://[::1]:PORT/user?sub=viewer-1&name=Vera&groups=staff' # who the next login is The issuer approves every login at once as the current user (the control endpoint changes it); `--issuer-file` receives the issuer URL for `auth.oidc.issuer`. **bcrypt PASSWORD** prints a bcrypt hash, the content of a break-glass file. Every devtools command that calls the API sends `Authorization: Bearer $NMON_TOKEN` when that variable is set (an nmon with authentication on needs an admin token). ## Gates `make gate-m3` runs everything above end to end and writes live screenshots to `docs/screenshots/M3/`. It needs Playwright's chromium: `cd web && npx playwright install chromium`. `make gate-m6` starts nmon with a mock IdP and a break-glass file (authentication ON) and checks roles, tokens, break-glass lock-out, backup/restore, key rotation, settings reload, CSV import and the UI; its live screenshots go to `docs/screenshots/M6/`.