Search terms: host-status.sh · morning briefing infrastructure · UPS in briefing · daily infrastructure digest · disk headroom alert · reboot required · GPU RTD3 check · reachability check
Six lines appended to the 05:45 morning briefing, covering everything the trading side’s briefing doesn’t: UPS, drives, disk headroom, pending reboots, host reachability, and whether the GPU freeze fix is still in place.
Added 2026-08-16.
🖥️ Host
🔋 UPS: OL — battery 100%, load 10%, 62 min runtime, 121.0 V in
💽 Drives: laptop 2/2 ok (09:18) · media-server 4/4 ok (09:18)
📊 Disk: / 22% · 2TB 44% · media/ 13% · NAS 70%
🔄 Reboot required: no
🌐 Reachable: media-server ✅ · NAS ✅ · pi-hole ✅
🎮 GPU: RTD3 disabled, 0 ms suspended in 24h (0.0%)
| File | Owns | Changes |
|---|---|---|
~/.local/bin/host-status.sh |
all infrastructure logic | often — this is the one to edit |
~/.local/bin/morning-briefing.py |
presentation only | should never need editing again |
The briefing gained exactly three things: a HOST_STATUS constant, a six-line
section_host() that shells out, and one entry in fields. It learns nothing about UPS or SMART
— it prints whatever the collector hands it.
def section_host():
if not os.path.exists(HOST_STATUS):
return f"⚠️ {HOST_STATUS} is missing"
r = subprocess.run([HOST_STATUS], capture_output=True, text=True, timeout=30)
out = r.stdout.strip()
return out if out else f"⚠️ host-status.sh returned nothing (rc={r.returncode})"
("🖥️ Host", safe(section_host)),
Why split it: the briefing is the trading side’s file and runs live every morning. The
infrastructure side changes far more often. Splitting means iterating on one never risks the
other. The existing safe() wrapper means a broken collector produces a warning line rather than
killing the whole briefing.
Runs as the normal user from the 05:45 user crontab, so nothing may require root.
| Line | Source | Notes |
|---|---|---|
| UPS | upsc cyberpower on the media server |
NUT is not installed on the laptop — the UPS is attached to the server. Fetched over the same SSH round trip as everything else remote |
| Drives | last line of ~/drive-health.log, both hosts |
SMART needs root; the root cron job leaves the log, this reads it. Flags STALE past 36 h — at 05:45 the last run was yesterday 08:00, so ~22 h is normal |
| Disk | df on /, the 2 TB vault, media server /, the NAS |
⚠️ at ≥85 % |
| Reboot | /var/run/reboot-required, both hosts |
needrestart is not installed here, so nothing else prompts |
| Reachable | ping -c1 -W2 |
media server, NAS, Pi-hole |
| GPU | /proc/driver/nvidia/params + runtime_suspended_time |
Never power/control — gpu-manager rewrites it to auto at every login, harmlessly |
One SSH round trip fetches UPS, media-server disk, its reboot flag and its drive log together. Four separate calls would be slower and four times as many things to hang. Whole script runs in ~0.6 s with an 8 s hard SSH timeout, so a dead host delays the briefing rather than breaking it.
Per When a Check Doesn’t Measure What It Claims, every line was validated by breaking the thing it watches:
| Broken | Output |
|---|---|
| media server unreachable | 🔴 UPS: unknown — could not reach media-server and media-server ? / media/ ? — rows present, not dropped |
| drive log 3 days old | laptop STALE (74h) |
| drive log shows faults | laptop 3 PROBLEM (08:00) |
| disk over threshold | ⚠️ / 22% per filesystem |
| RTD3 re-enabled | 🔴 GPU: RTD3 is ENABLED (DynamicPowerManagement=2) — the freeze fix has been undone |
| no NVIDIA driver | 🎮 GPU: no NVIDIA driver loaded |
Reproduce any of them by copying the script and overriding the constant at the top:
sed 's/^MEDIA_HOST=.*/MEDIA_HOST="no-such-host"/' ~/.local/bin/host-status.sh > /tmp/t.sh && bash /tmp/t.sh
That fifth row is the point of the GPU line. If an NVIDIA driver update ever renames Ubuntu’s
/usr/lib/modprobe.d/nvidia-runtimepm.conf and the basename collision returns, the briefing says
so the next morning — instead of a frozen display saying so three weeks later. See
NVIDIA GPU Hangs.
morning-briefing.py posts when run — there is no dry-run flag. To exercise it safely, run a
copy with both webhook paths pointed at files that do not exist; get_webhook() returns None
and it prints to console only:
python3 - <<'PY'
s=open("/home/aztechguy/.local/bin/morning-briefing.py").read()
s=s.replace('discord-webhooks/briefing.txt','NEVER-POST-1')
s=s.replace('discord-webhooks/stocks.txt','NEVER-POST-2')
open("/tmp/test-brief.py","w").write(s)
PY
/home/aztechguy/Projects/calendar-automation/.venv/bin/python /tmp/test-brief.py
The collector alone is always safe — it only prints:
~/.local/bin/host-status.sh
Learned the hard way the same morning:
smart-health-check.sh --digestposts, only--dry-runprints. Three test messages reached the live#drive-alertschannel before that registered. Check which flag prints before testing anything that owns a webhook.
Constants at the top of host-status.sh:
MEDIA_HOST="media-server"
NAS_IP="10.0.0.15"
PIHOLE_IP="10.0.0.33"
DISK_WARN=85 # % used before a filesystem is flagged
SSH_TIMEOUT=8 # hard cap; the briefing must never hang on a dead host
Adding a seventh line means editing only host-status.sh. The briefing picks it up
automatically.
cyberpower directly? rather than going quiet