fix(spoolbuddy): gate display wake on stable scale reading
A noisy load cell that bounced ≥50 g around its midpoint kept the kiosk
screen permanently lit. The wake threshold check ran against last_wake_grams
which itself advanced to noisy values, so every bounce back across 50 g
re-fired display.wake(), keeping the FIFO reader pumping wlopm --on
faster than swayidle could trigger wlopm --off.
The fix gates wake on the scale's `stable` flag — only readings that
have settled within 2 g over a 1 s window count as a real spool
placement / removal. Unstable noise can't fire wake and can't advance
last_wake_grams, so the next genuine settled change is still measured
against the right baseline.
Three regression tests pin the contract: noisy ±60 g unstable readings
never wake, a settled >50 g jump wakes, a noise burst between two
settled readings doesn't poison last_wake_grams.
Bambuddy's SSH keypair under <DATA_DIR>/spoolbuddy/ssh/ regenerates whenever
the data dir is recreated (volume remount, container recreate, fresh deploy).
The daemon previously only fetched the pubkey at registration, so any
rotation after a successful boot left ~/.ssh/authorized_keys pointing at
a stale public half — every Update click then failed with "Connection
closed by authenticating user spoolbuddy [preauth]" until the daemon was
restarted by hand. Each prior registration also appended a fresh entry
without pruning, accumulating stale Bambuddy-tagged keys indefinitely.
- HeartbeatResponse now carries ssh_public_key; the heartbeat route reads
it via the same try/except shape as the register route so a missing or
unreadable backend key doesn't break telemetry.
- _deploy_ssh_key() strips lines tagged bambuddy-spoolbuddy and writes
the current key once. No-op when already in sync (no mtime churn on
every heartbeat). User-managed entries are preserved.
- Daemon heartbeat handler calls _deploy_ssh_key when the response
carries a key, so rotations propagate within one heartbeat instead
of requiring a service restart.
Tests: 5 unit (creates-when-missing, replace-stale-pileup, preserve-user-keys,
idempotent, swallows-write-errors) + 2 backend integration (heartbeat carries
the key; backend key-read failure leaves ssh_public_key None but the
heartbeat still 200s).