You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
/api/v1/drives reclassifies drive types and enumerates loop*
devices. The ghw bump (v0.6.1 → v0.25.0, reaching nitr via
nitr-core v0.2.0) changes what type reports: ram* devices are
reclassified ssd → unknown (v0.6.1 was mislabelling RAM disks as
SSDs), and loop* devices are now enumerated at all, typed virtual
— 22 → 24 entries on the test host. ssd does not disappear
universally: ghw v0.25 still emits it for nvme*/mmc* and
non-rotational sd* drives, so real SSDs keep their type. The new
values are passed through deliberately rather than mapped back onto
the old enum — calling a loop device an SSD would be re-asserting
data now known to be wrong. Consumers branching on type == "ssd"
must be updated. Loop devices are deliberately not filtered
server-side; type: "virtual" is the documented way to filter them
client-side. Root cause lives in the github.com/bitcav/nitr-core
dependency (v0.2.0, bb3fb9b).
/api/v1/disks no longer lists bind mounts. gopsutil v4's disk.Partitions(false) now skips bind mounts outright, so mounts
that duplicate an already-listed device — e.g. /snap or WSL's docker-desktop-user-distro bind — no longer appear (5 → 3 entries
on the test host). This is more correct — the duplicated filesystem's
space was being double-counted — but it is an output change:
consumers matching on mountPoint should expect fewer entries.
Root cause lives in the github.com/bitcav/nitr-core dependency
(v0.2.0, bb3fb9b).
/api/v1/swap renames its four page-fault keys. The handler
serializes gopsutil's SwapMemoryStat directly, and the v2 → v4 bump
camel-cased its JSON tags: pgin → pgIn, pgout → pgOut, pgfault → pgFault, pgmajfault → pgMajFault. Consumers reading
the old lowercase keys get undefined after upgrading and must switch
to the new names. The other six keys (total, used, free, usedPercent, sin, sout) are unchanged. Verified by struct-tag
diff of v2.20.7 against v4.26.6 and against the live response.
/api/v1/sensors renames sensorTemperature → temperature and adds sensorHigh/sensorCritical. Same mechanism: the handler serializes
gopsutil's TemperatureStat directly, and gopsutil v4 changed the tag
(the struct also moved from the host package to the new sensors
package). The two threshold keys are additive — new keys are not
breaking on their own — but the rename is: consumers reading sensorTemperature must switch to temperature.sensorKey is
unchanged. Verified by struct-tag diff of v2.20.7 against v4.26.6.
/api/v1/gpu no longer fabricates a graphics card on hosts whose
display device is not on a PCI bus — it now returns [] there. ghw
v0.6.1 took the third-from-last component of the /sys/class/drm/cardN
symlink target as the card's PCI address with no validation, so on an
ARM cloud VM (observed on the linux/arm64 CI runner: /gpu went from
one populated entry to [] across the migration) it emitted a GraphicsCard whose address was a junk path fragment and whose
vendor/model could not resolve. ghw v0.25 scans the path for a
component matching its PCI-address regex and skips the card when none
is found — the same defect class as the assetTag-carrying-the-product-name
and ram*-labelled-ssd bugs already fixed in this release: wrong
data retired in favour of no data. Filed under breaking changes rather
than fixes because /gpu is user-visible on a published platform and a
consumer branching on a non-empty list (e.g. alerting on "GPU
present") sees a changed answer. x86 hosts with a real PCI GPU are
unaffected — they resolve a valid PCI address and are reported
exactly as before.
/api/v1/product no longer emits assetTag; the product name moved to a
new name key. The value serialized under assetTag was always the
product name (ghw.Product().Name), never an asset tag — ghw's ProductInfo has no asset-tag field — so anyone building an asset inventory
off /product was filling the asset-tag column with product names. The key
is removed outright rather than left permanently empty: an always-"" assetTag reads as "this machine has no asset tag", which is worse than the
key being absent. The machine's real asset tag is already served, correctly,
at GET /api/v1/baseboard (assetTag, from ghw.Baseboard().AssetTag) —
read it there instead. The new name key carries the product name under its
own name. Root cause lives in the github.com/bitcav/nitr-core dependency
(v0.1.0, 51c5cc9) and reaches nitr via the go.mod bump to v0.1.0.
/api/v1/memory no longer returns 200 OK with a null body when the
memory collector fails. It previously printed the error to server stdout
and returned 200 with an empty body, so a caller could not distinguish
"needs root", "broken", or "no memory devices" from success — every poll
looked healthy. It now returns 403 Forbidden when the underlying error is
a permission error and 500 Internal Server Error otherwise, with the
standard error envelope ({"message": "...", "status": <code>}), so
callers branching on the HTTP status (rather than the body) must react.
Verified on the wire, running non-root: GET /api/v1/memory → 403 {"message":"open /dev/mem: permission denied","status":403}. The DMI
endpoints (/product, /chassis, /baseboard) keep their per-field "unknown" degradation behaviour and are unchanged. (728e195)
Added
Configuration via CLI flags, NITR_* environment variables, and a config
file, with a documented precedence: --flags > NITR_* env > config
file > built-in defaults. Four new persistent flags — --config, --port, --host (alias --bind), and --data-dir — bind to the same
keys the config file reads, and viper.SetEnvPrefix("NITR") plus AutomaticEnv means every config key (not just the four with flags — save_logs, cors_origins, metrics_push_interval, …) takes a NITR_
uppercase env override, which is what makes the Docker image configurable
without bind-mounting a config file. --host 127.0.0.1 makes localhost-only binding possible for the first time — previously the
server always listened on all interfaces; the default remains 0.0.0.0
(all interfaces), so behaviour without configuration is unchanged and the
security-relevant default did not change. --data-dir relocates nitr.db
(the directory is created if missing); nitr.log follows --data-dir
when save_logs is on. config.ini is not affected and stays in the
working directory. The config file is named config.ini but is parsed as YAML — write key: value, not INI key=value or [sections] — and keeps its name to avoid breaking existing
installs. Verified against the built binary: --port, --host, --data-dir, NITR_PORT, and --config <path> all behave as documented,
and a flag beats the matching env var which beats the file value which
beats the default. (73029b8)
Liveness and readiness probes at GET /health and GET /ready, plus a
Docker HEALTHCHECK that polls /health. Both routes are registered on the
root app before the x-api-key middleware and the panel session auth, so
they answer with no credentials and never redirect to the login page —
the point being that Docker / Compose / Kubernetes / uptime checkers can
poll them unauthenticated. /health does no I/O and returns 200 {"status":"ok","version":"0.9.0"}. /ready does an os.Stat(database.DBPath()) — the resolved nitr.db path, honouring --data-dir — and nothing more: 200 {"status":"ready"} when the
file is present, 503 {"status":"not ready","error":"..."} (the raw stat
error) when it is not. It confirms the database file exists; it does not
confirm bolt can be opened right now, nor that the DB is uncorrupted — database.SetupDB opens bolt with nil options whose exclusive flock has no
timeout and blocks forever under contention, so a probe that opened the DB
could pile up blocked goroutines when polled every few seconds. The Docker HEALTHCHECK (wget -q -O /dev/null http://localhost:8000/health,
30s interval / 5s timeout / 10s start period / 3 retries) makes docker ps
report a health status for the container. (0262123)
The server now applies a baseline middleware stack to every response — rate limiting, opt-in CORS, gzip compression, ETags, security headers,
and per-request IDs — with three new config.ini keys. Rate
limiting: the login POST / is capped at rate_limit_login_max
requests per minute per client IP (default 20) against password
brute-forcing, and /api/v1/* plus /metrics at rate_limit_api_max
(default 300); over the limit the server returns 429 Too Many Requests,
which API pollers should expect and back off from. CORS is
deny-by-default: browser origins get no Access-Control-Allow-Origin
header unless listed in the new cors_origins key (comma-separated;
empty, the default, denies all) — a browser-hosted dashboard on another
origin needs it set before it can call the API cross-origin. Responses may
now be gzipped when the client sends Accept-Encoding: gzip
(compress), and may answer 304 Not Modified to conditional If-None-Match requests (etag), so polling the slow-moving endpoints
skips unchanged bodies. Every response carries the baseline security
headers (helmet) and an X-Request-Id unique to the request, which the
access log records per line when save_logs is on — a client-side error
report can now be matched against the server's log. Verified against the
running server: with cors_origins unset, a request carrying an Origin
header comes back with no Access-Control-Allow-Origin; the login POST
returns 302 for the first rate_limit_login_max attempts and 429
thereafter; /api/v1/cpu returns 429 after 300 requests in a minute;
and a repeated If-None-Match request returns 304. (22a5ac8)
The panel now shows live CPU, RAM, and disk usage, streamed to it over
the existing /status WebSocket. The socket previously carried only
inbound messages; the server now pushes a metrics frame (host overview plus
per-disk usage) to every connected panel client — the first frame
immediately on connect, then one every metrics_push_interval seconds (a
new config.ini key, default 3, clamped to a 1s floor) — and the panel
renders CPU/RAM/disk widgets from the stream. The route sits behind the
panel session auth, so the stream is only available to logged-in sessions.
Each connection's writer goroutine stops when the client disconnects, and
both the read loop and the writer recover their own panics, so a
metrics-collection panic cannot take the process down. The WebSocket
handler also joins the writer goroutine before returning: a metrics tick
landing after the handler had released the connection wrote to freed
state, a use-after-free the race detector flagged, so the handler now
waits for the writer to stop first — and CI runs the suite with -race
to keep it that way. (c46e4b5, 752a686)
Service lifecycle commands: nitr install / uninstall / start / stop / status. The binary could always run as a system service, but
shipped no way to register or control one — installation was a manual
exercise per platform. The new commands drive the host's service manager
(systemd, launchd, Windows SCM, via kardianos/service) under the service
name NitrService. status distinguishes installed-and-running,
installed-but-stopped, and not-installed; start/stop against a host
with no installed unit say to run nitr install first; and a failure that
reads like a permission denial carries a "try running as root or with
sudo" hint, so "needs root" stays distinguishable from "broken". Verified
against the built binary: nitr status on a bare host prints "NitrService" is not installed on this host (linux-systemd). and exits 0. (032fac2)
nitr server — an explicit subcommand for starting the host service.
Bare nitr still starts it too and is not deprecated: the Dockerfile CMD ["./nitr"] and the README Windows double-click flow both depend on
the zero-argument default, which is unchanged. nitr server exists so the
spelling is self-documenting — it is what nitr --help lists, what the
installed system service's ExecStart invokes (ServiceConfig.Arguments
is now ["server"]), and what reads clearly in a process list. It
inherits the root command's persistent flags (nitr server --port 9000
works). Verified against the built binary: nitr --help lists server,
and both nitr and nitr server reach the server entry point. (96e3361)
Admin panel redesign at /: dark mode following the OS prefers-color-scheme with a manual toggle in the header (the choice
persists in localStorage and overrides the OS in both directions); tabbed navigation split into Overview and Metrics where the page
previously had a single view; live CPU and RAM line charts on the
Metrics tab, fed from the existing /status WebSocket stream; and a mobile-usable layout that reflows to a single column with pinch-zoom
re-enabled (the viewport meta no longer pins maximum-scale=1.0). The
charts are session-only browser history — roughly the last six
minutes (120 samples at the default 3s push interval), held in the
browser, nothing persisted server-side — and start empty on each
page load; historical retention is a separate, unbuilt feature. The
uPlot chart library (~50 KB) is vendored into the binary, so no CDN is
used and no build step was added — the panel still works air-gapped.
Verified by logging into the running panel: the Overview/Metrics tabs,
the theme toggle (with nitr-theme in localStorage), and the cpuChart/ramChart containers all render. (ab5c032)
CLI info commands that read the collectors directly — no server, no
API key, no network — one subcommand per API path segment: nitr cpu, ram, memory, disks, drives, bios, chassis, baseboard, product, gpu, network, bandwidth, isp, processes, devices, host, and overview. Each prints an aligned
key/value table by default; --json prints the raw JSON payload
byte-identical to the matching API response (the same encoding/json.Marshal path fiber's c.JSON uses), and --watch / -w re-fetches and re-renders on an interval (--watch=5s; a bare --watch defaults to 2s). Until now the only way to read a metric was
to start the server, authenticate, and make an HTTP request, even
though every collector is a plain Go function returning a struct; this
makes nitr usable standalone, in the neofetch/inxi/hwinfo niche.
Verified against the built binary: nitr cpu, nitr ram, and nitr disks print their tables with no server running, nitr cpu --json
emits the {"vendor":...,"cores":12,...} payload, and nitr cpu --watch=1s re-renders the table each second. (a54b802)
An OpenAPI 3.1 spec is now the API reference source of truth,
checked in at docs/openapi.json and served two ways: raw at GET /openapi.json (no credentials required) and rendered at GET /docs
(behind the panel session auth), which fetches /openapi.json
client-side so the rendered docs can never drift from the spec. It
covers all 17 GET /api/v1/* endpoints with schemas taken from the
actual Go structs, the x-api-key security scheme, and the privilege
and deprecation notes that used to live in docs/API.md's prose —
which is removed; its hand-maintained schema tables had already
drifted from the real /processes shape. A test
(TestOpenAPISpecCoversAllRegisteredRoutes) fails the build if a
registered route is missing from the spec. Verified against the
running server: /openapi.json returns 200 application/json with "openapi": "3.1.0" and a path entry for every v1 route including /loadavg, /swap, and /sensors, and /docs redirects
unauthenticated clients to the login page. (9ae8e37)
Three new collectors behind the same x-api-key auth: GET /api/v1/loadavg returns the 1/5/15-minute load average
({"load1":0.19,"load5":0.15,"load15":0.23}); it is not implemented
on Windows (gopsutil has no equivalent concept there) and surfaces
that as 501 Not Implemented rather than a silently empty 200. GET /api/v1/swap returns swap total/used/free/used-percent plus
page-in/out counters — /ram only reports physical memory, so a host
swapping heavily previously looked healthy. GET /api/v1/sensors
returns temperature/fan sensor readings; a host with no exposed
sensors is not an error and returns null, matching the other list
endpoints' behaviour on an empty result. Verified against the running
server on Linux: /loadavg and /swap return the shapes above; /sensors returns null on the machine tested (a WSL guest with no
hwmon sensors exposed). (71b8382)
install.sh curl-pipe quick start in the README: curl -fsSL https://raw.githubusercontent.com/bitcav/nitr/master/install.sh | bash
downloads the right release binary for the host OS/arch into the
current directory, sets the exec bit, and runs nitr version to
confirm it works — modeled on direnv's installer, with no root or sudo
step anywhere (the README's previous sudo install step is gone). (4935256)
Opt-in metric history retention in nitr.db, with time-range queries
on the four usage-metric endpoints. Three new config keys follow the
standard --flag > NITR_* env > config.ini > default precedence: history_enabled (--history-enabled, NITR_HISTORY_ENABLED, default off), history_interval (--history-interval, NITR_HISTORY_INTERVAL, default 10 seconds), and history_retention_hours (--history-retention-hours, NITR_HISTORY_RETENTION_HOURS, default 24). When enabled, a
background sampler writes one CPU/RAM/disk/bandwidth sample every history_interval seconds — all four in a single transaction, one
fsync per tick — and prunes samples older than the retention window in
the same transaction, so steady state at the defaults is 8640 samples
per metric (24h at 10s) and the bbolt file plateaus at that high-water
size rather than growing without bound. Retention defaults off
deliberately, not as an oversight: sustained small writes wear
flash/SD storage, and Raspberry Pi is an explicitly targeted
deployment, so SBC users opt in knowing the tradeoff. On the query
side, /api/v1/cpu, /ram, /disks, and /bandwidth now accept ?from=, ?to=, and ?resolution=: with any of them present the
response switches to a series of retained samples, [{"timestamp":"2026-07-29T15:44:28.502661692Z","data":{…}}, …],
where each data carries the exact payload the endpoint's
instantaneous form returns; from/to take RFC3339 or Unix seconds
(defaulting to the oldest retained sample and now), and resolution
thins to at most one sample per that many seconds, keeping the first
sample in each window without averaging or altering payloads. With
none of the parameters present the response is byte-identical to
before this change — existing calls are unaffected. With retention
disabled, any range parameter returns 400 {"message":"metric history retention is disabled; set history_enabled (off by default) to use from/to/resolution","status":400}.
Verified against the built binary: a default run leaves no history
bucket in nitr.db and range parameters return the 400 above; with --history-enabled --history-interval 1 the bucket appears with 10
samples per metric after ~10s, the instantaneous /cpu payload is
unchanged, ?resolution=5 thins 10 samples to 3, and ?from=garbage
returns 400 invalid from. (bd6dbd9)
A linux/arm64 cross-compile target and an arm64-probe CI job,
the evidence-gathering step toward shipping ARM64 (Raspberry Pi / ARM
SBC / ARM VPS). CI now builds nitr_linux_arm64 and uploads it with
the other build artifacts, and a new job executes that exact binary on
a real ARM64 runner (ubuntu-24.04-arm), probes every /api/v1/*
endpoint via scripts/linux_arm64_endpoint_probe.sh, and uploads a
per-endpoint report. The job is evidence-only, not a release gate:
it fails CI only if the binary won't run, the server won't start, or
the API key can't be obtained — a red endpoint cell in the report is a
finding, not a failure (a follow-up commit restores the exec bit the
artifact download drops, so a download artifact is not misdiagnosed as
a platform verdict). nitr_linux_arm64 is deliberately absent from
the Draft Release artifact list — it joins only once the probe shows
the core endpoints working on real ARM64 hardware — so linux/arm64
is not a published or supported target yet; the README's "under
evaluation" framing stands. (31fe814, 25ffecf)
linux/arm64 release binaries are now published. With the arm64 /cpu and /gpu panics fixed (see Fixed below) and the arm64-probe
CI job green on real ARM64 hardware (ubuntu-24.04-arm runner, 19 of
20 endpoints healthy — the one failure is the pre-existing privileged /memory issue that behaves identically on amd64), nitr_linux_arm64
joins the Draft Release artifact list alongside the amd64 and 386
builds. The arm64-probe job added earlier stays in CI, continuing to
execute the exact shipped binary on real ARM64 hardware and probe every /api/v1/* endpoint.
Changed
Hardware-introspection stack modernized: gopsutil v2 → v4
(github.com/shirou/gopsutil/v4 v4.26.6), ghw v0.6.1 → v0.25.0,
nitr-core v0.1.1 → v0.2.0. gopsutil v2.20.7 was +incompatible and
is now completely gone from go.mod/go.sum — if both modules
remained, both would compile, and v2 is what breaks windows/arm64.
ghw arrives transitively through nitr-core. No local replace
directive is involved — go.mod resolves the published modules
directly. The call-site delta in nitr itself was small: import paths
everywhere, process.Status() now returns a slice (collapsed to its
first element — byte-identical to v2's string on every supported
platform), and SensorsTemperatures moved from the host package to
the new sensors package. The endpoint-visible consequences are the
breaking entries above. The before/after JSON harness covered the 14 nitr-core collectors only — everything they serve is byte-identical
modulo live-sampled values — but /swap and /sensors are serialized
straight from gopsutil by nitr's own handlers and never touch nitr-core,
so they were outside the harness; those endpoints were verified
separately by struct-tag comparison of v2.20.7 against v4.26.6, which
is how the two key renames above were found.
windows/arm64 now builds.GOOS=windows GOARCH=arm64 go build .
succeeds for the first time — it previously failed inside gopsutil
v2 / go-ole. The full matrix — windows/{arm64,amd64,386},
linux/{amd64,386,arm64} — builds. windows/arm64 is build-only in
this release: it is deliberately not added to the release artifacts,
which need an evidence run on real windows-11-arm hardware first.
Bumped github.com/bitcav/nitr-core from the v0.0.0-20200823224936-5500912f5599 pseudo-version to the tagged v0.1.0
release. This is the dependency bump that delivers the /product fixes
above (correct family, deprecated familiy, name, dropped assetTag);
it is recorded separately because it is the mechanism, not just the symptom.
No local replace directive is involved — go.mod resolves the published
v0.1.0 directly.
Static assets and views are now embedded with the standard library's go:embed instead of go.rice. This removes two dependencies
(github.com/GeertJohan/go.rice and github.com/daaku/go.zipexe), deletes
637 KB of committed generated source (rice-box.go), and drops the make rice-box regeneration step — the embedded copy is compiled from app/assets and app/views at build time, so it can never go stale, and
there is no generated file left to commit. It also retires the go.zipexe
runtime-executable-parsing path whose init() panic killed every published
v0.8.0 binary (see [0.8.1]): with no runtime parsing left, that failure
class is gone rather than patched. Nothing consumer-facing changes — the
panel and /assets are served exactly as before, and go build remains
the only build step. (207bc2a)
GET /api/v1/processes returns a much richer — and purely additive —
response shape, and gains query parameters. Each entry was {pid, name} and is now {pid, ppid, name, user, cmdline, status, cpu_percent, mem_percent, rss, start_time}; no keys were removed, so
clients reading the old two keys are unaffected. New query parameters: ?sort=cpu|mem|name|pid (default pid), ?order=asc|desc (default asc), ?limit=<n>, and ?search=<substring> matched
case-insensitively against name and cmdline. A process that exits
mid-scan is skipped rather than failing the whole call. Verified
against the running server: ?sort=cpu&limit=3 returns three objects
carrying all ten fields, and ?search=nitr filters the list to
matching processes. (44d4710)
/api/v1/bandwidth and /api/v1/isp no longer block the request. /bandwidth previously stalled every caller for ~1s while bandwidth.Info() computed its rx/tx delta inline; a background
sampler now refreshes a cache every 5s and the handler serves the
cache immediately — until the first sample lands it returns null. /isp previously made an outbound speedtest.net call with no timeout,
hanging indefinitely on a slow or air-gapped host; it now caches a
successful lookup for an hour and races a fresh lookup against a 5s
timeout, falling back to the last good (or empty) cached value.
Verified against the running server: the first /bandwidth call
returns null in ~6ms (previously ~1s), and /isp answers in
~270ms with {"isp":...,"ip":...,"lat":...,"lon":...}. (d7fe474)
The README screenshots are now reproducible: scripts/regen-images.sh
re-captures them via scripts/web-screenshots.mjs and two committed
vhs tapes, replacing manual screenshotting. Contributor tooling; no
shipped behaviour changes. (2eb84fb)
Internal: the database layer now reuses a single bbolt handle instead
of opening and closing the file on every call (AuthAPI alone opened
it twice per authenticated request), and a second nitr process against
the same nitr.db now fails startup after a 5s lock wait with database is locked by another nitr process instead of blocking
forever — observed in practice during verification. SetAPIData
propagates setup failures instead of logging and swallowing them, so a
broken database fails startup loudly. Plus an idiom sweep (%w error
wrapping, unified fiber.Status* constants) and lint/gofmt fixes
keeping CI green. (7c118c9, 1a35322, 4a71ecb, 385a73e, ec41333)
Fixed
install.sh no longer claims it "installed" anything, and warns when
another nitr shadows the download. The script downloads into the
current directory and never touches PATH, but its old output — [installer] installed nitr followed by a bare version line — read as a
system install, so a user with an older nitr already on PATH ran nitr version one command later and saw the old version. It now
states the absolute path written and that it is not on PATH, labels the
version line as belonging to the downloaded binary, prints the one sudo mv … /usr/local/bin/nitr command to make it the system nitr,
and warns explicitly — naming the resolved path and its version — when command -v nitr resolves to a different binary that will keep
shadowing the download. No behaviour change: it still downloads to the
current directory and never writes to system paths.
install.sh no longer refuses arm64 hosts. The uname -m mapping
handled only x86_64/amd64 and i386/i686 and died on anything
else with a message claiming nitr ships linux_amd64 and linux_386
only — false since nitr_linux_arm64 joined the release artifacts (see
Added above), and contradicting the README's platform table two screens
down. A Raspberry Pi user running the documented curl-pipe one-liner was
told their platform was unsupported while the matching binary sat in the
same release. aarch64/arm64 now map to the nitr_linux_arm64 asset
and the error message names the architectures actually shipped. arm64
remains Linux-only — there is no nitr_windows_arm64 asset — which
needs no handling here because the script already dies on non-Linux
kernels before reaching the architecture mapping.
The embedded OpenAPI spec now matches the migrated wire format. Drive.type documents the lowercase unknown/hdd/fdd/odd/ssd/virtual
enum ghw v0.25 actually serializes (previously it omitted virtual and showed
wrong casing), the /sensors and /swap schemas use the key names gopsutil
v4 emits (see the breaking entries above), and /disks notes that bind
mounts are excluded.
Six endpoints no longer panic when their hardware probe fails. /api/v1/baseboard, /api/v1/bios, /api/v1/chassis, /api/v1/product, /api/v1/drives and /api/v1/devices logged a
ghw error and then dereferenced the nil result anyway — a guaranteed
crash on exactly the hosts where probes fail: ARM boards without DMI
tables and VMs without a PCI bus (the same defect class that v0.1.1
fixed in /cpu and /gpu). Each collector now guards on both error
and nil and degrades to the zero struct / empty array, with
regression tests that were verified to fail without the guards.
Fixed in the github.com/bitcav/nitr-core dependency (v0.2.0, 7ba07df).
/api/v1/product now emits family (the documented, correctly spelled
key).Product.Family was serialized under the misspelled JSON tag familiy, so clients written against the documented family key silently
read an absent field. The misspelled familiy key is retained for one
release as a deprecated duplicate carrying the same value, to avoid a silent
break, and will be removed in a later breaking release — switch to family. Fixed in the github.com/bitcav/nitr-core dependency (v0.1.0, 51c5cc9).
An unauthenticated POST / with a malformed body killed the whole
server.LoginSubmit — reachable with no credentials — called log.Fatal when BodyParser failed, which os.Exit(1)s straight past
fiber's recover middleware: anyone who could reach the login page could
terminate the process with a single bad request, a remote denial of
service. The authenticated PasswordSubmit had the same bug. Both now
return 400 Bad Request. The same commit fixes a second kill-the-server
path: GetLocalIP called log.Fatal when the dial fails, which happens on
any host with no default route (air-gapped box, restricted container,
egress-filtered network) — loading the panel on such a host took the
server down. It now returns an error that surfaces as a 500. (d35c6ed)
Concurrent API-key generation raced and could produce duplicate or
correlated keys.utils.RandString — used for API keys (POST /generate from the panel) and for the default user's key at first start —
shared a single *rand.Rand across goroutines, and rand.Rand is not
safe for concurrent use: simultaneous generation raced on its internal
state, and in a racy build the output can repeat or correlate — two keys
that are equal, or predictable from each other. It now uses the math/rand/v2 package-level source, which is goroutine-safe and
auto-seeded. (1f038bf)
database.GetUserByID and database.SetUserData no longer panic against a nitr.db that exists but is missing its users bucket (a touched or
restored-empty file): they return an error naming the database instead of
dereferencing a nil bucket. SetAPIData now re-runs bucket creation
unconditionally, so such a database self-heals on the next server start
instead of panicking. (b8e7193)
The five CLI password prompts no longer ignore input errors, and every
failure path in the credential commands now exits non-zero.nitr passwd (three prompts), nitr key, and nitr qr called fmt.Scan
without checking its return, so a closed or broken stdin left the password
variable empty and the command silently compared that empty string against
the stored hash — printing "Wrong password." for what was really a read
failure. Each prompt now reports the read error and aborts. On top of
that, the three commands converted from cobra Run to RunE: all
failure paths — a read error, a wrong password, mismatched new passwords,
a database error — now exit 1, where previously each printed its message
and exited 0. Any script wrapping nitr passwd / key / qr can now
branch on the exit status, and must: a wrong password previously looked
like success to the shell. Failures are reported as Error: <message>
(lowercased, e.g. Error: wrong password, Error: passwords don't match). Verified against the built binary: nitr passwd < /dev/null
prints Error: failed to read password: EOF and exits 1; nitr key with
a wrong password prints Error: wrong password and exits 1. (9517dbc, a4fdc70, 5e28eb9)
/api/v1/cpu and /api/v1/gpu panicked (HTTP 500) on linux/arm64. /cpu returned 500 index out of range [0] with length 0 and /gpu 500 nil pointer dereference on real ARM64 hardware. Fixed in the github.com/bitcav/nitr-core dependency (v0.1.1) and consumed via
the go.mod bump (c4042c2).
ARM hosts now get real CPU data — vendor, model, and core count parsed
from /proc/cpuinfo — with clockSpeed reported as 0 because ARM
exposes BogoMIPS rather than MHz. Verified by the arm64-probe CI job
on a real ubuntu-24.04-arm runner: /cpu now returns 200 populated
and /gpu returns 200 populated.