Skip to content

feat(stats): ambient + 4TB USB SSD temperature graphs (v0.94.0) - #206

Merged
beveradb merged 3 commits into
mainfrom
feat/sess-20260822-1922-temp-graph-stats
Aug 22, 2026
Merged

feat(stats): ambient + 4TB USB SSD temperature graphs (v0.94.0)#206
beveradb merged 3 commits into
mainfrom
feat/sess-20260822-1922-temp-graph-stats

Conversation

@beveradb

Copy link
Copy Markdown
Contributor

What

Adds two live temperature graphs to System → Stats, alongside CPU/MEM/DISK:

  • Ambient — the motherboard ACPI thermal zone (a room-temperature proxy; ~28°C on NomadPC)
  • 4TB SSD — the connected SanDisk Extreme Pro (~44°C)

Bars color cool→warm→hot by threshold. A note line surfaces the drive's lifetime SMART over-temp counters — the persistent "has it ever overheated" record (0 min on NomadPC).

Why

The mini PC lives in a South Carolina room where the AC is left off for days (100°F+ ambient). The CPU sensor alone doesn't tell us whether the drives or the room are getting dangerously hot. This surfaces both.

How

The 4TB SSD is an NVMe drive behind an ASMedia USB bridge that sensors can't see — plain SAT/SCSI/JMicron passthrough all fail. It's read via sudo smartctl -j -x -d sntasmedia /dev/sda:

  • Device auto-detected via lsblk -S -P (robust key=value parse), preferring the disk whose model identifies it as the SanDisk; re-detected each read so a reconnect that renames the node is picked up.
  • Reads cached 20s (incl. failures) so the 5s poll can't spawn a 10s-blocking sudo smartctl per request.
  • smartctl exit code is a status bitmask — stdout JSON is parsed regardless.
  • Parsing is fully guarded; a missing/absent sensor degrades gracefully (row hides), never 500s the stats endpoint. Works on a Pi with no external drive too.

GET /system/stats now also returns ambient_temp_c, ssd_temp_c, ssd_warning_time_min, ssd_critical_time_min (each omitted when unavailable).

Deploy notes

  • Requires smartmontools (already installed on NomadPC; added to MINIPC-SETUP.md for reprovisioning). The nomad service user has passwordless sudo.
  • Backend .py change → auto-deploy restarts kj-controller (interrupts active playback).

Testing

  • 16 unit tests: ambient sentinel filtering, device detection (prefer-SanDisk / fallback / non-usb / lsblk error / malformed line), smartctl parse + positive/negative caching, endpoint field shaping + null-counter omission.
  • Full backend path verified end-to-end on the real device (real smartctl + psutil): {ambient_temp_c: 27.8, ssd_temp_c: 44, ssd_warning_time_min: 0, ssd_critical_time_min: 0}.

Reviewed locally with CodeRabbit (3 findings addressed) + a fallback agent review (shlex-guard hardening).

@coderabbitai ignore

beveradb and others added 3 commits August 22, 2026 19:32
Add two live temperature graphs to System → Stats alongside CPU/MEM/DISK:
- Ambient: motherboard ACPI thermal zone (room-temperature proxy)
- 4TB SSD: connected SanDisk Extreme Pro (NVMe behind an ASMedia USB bridge,
  invisible to `sensors`; read via `sudo smartctl -j -x -d sntasmedia`, device
  auto-detected via `lsblk -S`, cached 20s)

Bars color cool→warm→hot by threshold. A note line surfaces the drive's
lifetime SMART over-temp counters (warning_temp_time / critical_comp_time) —
the persistent "has it ever overheated" record (0 min on NomadPC).

/system/stats now also returns ambient_temp_c, ssd_temp_c,
ssd_warning_time_min, ssd_critical_time_min (omitted when unavailable, so the
rows hide on a Pi with no external drive or where smartctl is absent).

Requires smartmontools (added to MINIPC-SETUP.md). Unit tests cover the
ambient sentinel filtering, smartctl parse/cache, and endpoint shaping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Cache failed SSD probes too (populated flag) so a missing/unresponsive
  smartctl no longer spawns a 10s-blocking subprocess on every 5s poll.
- Re-detect the USB SSD each read (no permanent path cache) and prefer the
  disk whose model identifies it as the SanDisk Extreme Pro, via robust
  `lsblk -P` key=value parsing — survives reconnect / avoids matching the
  wrong USB disk.
- Omit ssd_warning_time_min / ssd_critical_time_min from /system/stats when
  the drive doesn't report them (was serializing null), matching the other
  optional temp fields.
- Tests: device-detection (prefer-SanDisk / fallback / non-usb / lsblk error),
  negative-cache (two failed reads = one subprocess), null-counter omission.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Guard shlex.split() parsing inside the try in _find_usb_ssd_device so a
  malformed lsblk line (unbalanced quote → ValueError) can't propagate up and
  500 the whole /system/stats endpoint (defeating "never break stats").
- Clear a temp sparkline's history when its sensor goes absent, so a later
  reconnect starts a fresh graph instead of resuming a stale prefix.
- Surface the SSD lifetime over-temp note if EITHER counter is reported (was
  keyed on warning alone).
- Test: malformed-lsblk line returns None.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@beveradb
beveradb merged commit 1d87ba9 into main Aug 22, 2026
2 checks passed
@beveradb
beveradb deleted the feat/sess-20260822-1922-temp-graph-stats branch August 22, 2026 23:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant