• Small Python service that runs on the pub Pi alongside pi-capture, polling system metrics every 30s and pushing them to the Worker for the admin dashboard
  • The dashboard at /admin/tv-health shows the latest report as a “Pi · djpi-01 · last seen Ns ago · CPU X% · Mem Y%” card with severity coloring (CPU/temp/mem/disk/transcode backlog)
  • Push-based because the Pi is NAT’d; the Worker can’t poll. Bearer-token authenticated using the same NOW_PLAYING_INGEST_TOKEN as pi-capture (no new secret to rotate)
  • Non-critical service — failure does not affect the now-playing pipeline. The dashboard card expires from KV after 10 minutes if reports stop

Sources: pi/pi-status-reporter/main.py, pi/systemd/pi-status-reporter.service, worker.js (handlePiStatusIngest, readPiStatus, PI_STATUS_TTL_SECONDS)


What it reports

Every 30s the reporter calls psutil, reads a few /proc and systemctl interfaces, and POSTs a JSON payload to https://pub.ihnyc-rc.org/api/pi-status:

GroupFields
Identityhostname, ipAddress, uptimeSeconds, kernel, distro, lastBoot
CPUcores, percent, load1, load5, load15, temperatureC
MemorytotalMb, usedMb, percent
DisktotalGb, usedGb, percent (root filesystem)
ScriptsOne key per WATCH_SCRIPTS entry → running / stopped / failed (via systemctl is-active)
TranscodetranscodeBacklog — count of files in TRANSCODE_BACKLOG_DIR (null if dir absent)
MetareporterVersion, version (payload schema)

CPU temperature is read via psutil.sensors_temperatures() with a vcgencmd measure_temp fallback for kernels where the sensor name varies.

Sources: pi/pi-status-reporter/main.py (build_payload, read_cpu_temp_c, read_script_states)


Wire format

sequenceDiagram
  participant R as pi-status-reporter
  participant W as Worker /api/pi-status
  participant KV as DJ_FLASH KV

  loop every 30s
    R->>R: build_payload() via psutil
    R->>W: POST + Bearer NOW_PLAYING_INGEST_TOKEN
    W->>W: validate + sanitise (bounds + truncation)
    W->>KV: put("pi:status", json, { expirationTtl: 600 })
    W-->>R: 200 { ok: true, reportedAt }
  end

Server-side validation bounds payload size and field length before writing to KV. Strings are truncated to per-field limits. Numeric values pass through finiteOrNull() so JSON null values are not coerced to zero by Number(null).

The Worker stamps a server-side reportedAt (ISO 8601) so the dashboard’s “last seen Nm ago” is independent of Pi clock drift.

Sources: worker.js (handlePiStatusIngest, finiteOrNull ~line 10770), tests/pi-status.test.js


TTL semantics

pi:status is written with expirationTtl: 600 (10 minutes). Trade-off:

  • Long enough to ride out a brief systemctl restart pi-status-reporter without the dashboard card disappearing
  • Short enough to mark the reporter stale within 10 minutes instead of retaining an old reading

The card hides entirely, rather than turning red, when piStatus is null. Installations without a reporter do not show a permanent offline tile.

Adding pi-status-reporter to WATCH_SCRIPTS makes the dashboard show reporter state alongside the metrics.

Sources: worker.js (PI_STATUS_TTL_SECONDS = 10 * 60), tests/pi-status.test.js (TTL assertion)


Operations

Deployment, environment variables, verification, restart, update, and pause procedures are in the Raspberry Pi audio runbook.