A task-oriented index for debugging a pod in the field: symptom → first checks →
where the authoritative detail lives. This does not restate the ADRs and
topic docs — it points at them. The desktop Diagnostics console (/debug)
surfaces most of these signals live.
Pods run sshd on port 8822 (key-only), not 22.
ssh -p 8822 root@<pod-ip>
journalctl -u sleepypod.service -n 200 --no-pager # core app
journalctl -u frank -n 200 --no-pager # firmware: what the DAC actually didFor a support handoff, sp-bundle-logs includes the firmware journal, a combined
firmware/app timeline, and pod model, firmware revision/build date, and SleepyPod
build metadata. sp-info prints that compact context; sp-status and sp-logs
include it too. The model comes from the local device-status API; when hardware
is unreachable it is reported as unknown, so include the model manually.
To capture an install incident even after a reboot, select its UTC time window:
sp-bundle-logs --since '2026-10-05 19:50:00 UTC' --until '2026-10-05 20:05:00 UTC'
sp-logs --firmware --since '2026-10-05 19:50:00 UTC' --until '2026-10-05 20:05:00 UTC'Explicit windows search all retained boots. Without a window, bundles capture
the current boot and sp-logs --firmware follows app and firmware logs together.
On older installs without these options, use:
journalctl -u frank.service -u sleepypod.service --since '2026-10-05 19:50:00 UTC' --until '2026-10-05 20:05:00 UTC' --no-pagerSSH setup and hardening: scripts/README.md, docs/DEPLOYMENT.md.
| What | Path | Authoritative doc |
|---|---|---|
| App database | /persistent/sleepypod-data/ |
docs/DEPLOYMENT.md |
| RAW frames (hot, tmpfs) | /persistent/biometrics/*.RAW |
docs/adr/0018-tmpfs-raw-frames.md |
| RAW frames pinned for archiving | /persistent/biometrics/.pending/*.RAW (hard links) |
docs/adr/0018-tmpfs-raw-frames.md |
| RAW archive (cold, eMMC) | /persistent/biometrics-archive/*.RAW.gz |
docs/adr/0012-biometrics-module-system.md |
| DAC socket | /persistent/deviceinfo/dac.sock (Pod 5) · /deviceinfo/dac.sock (Pod 3/4) |
docs/DEPLOYMENT.md |
RAW_DATA_DIR (default /persistent) overrides where readers look for RAW
frames — see src/streaming/piezoStream.ts and src/server/routers/raw.ts.
The Biometrics page in the console shows a live data-flow banner — if it reads red/amber while the bed is occupied, the ingest pipeline has stalled.
- Open Settings → Device → Sensor data source first. It names the
firmware generation (NATS JetStream vs the
.RAWvariants), which transport the core picked at startup, and the age of the last frame. A note under the rows calls out a fallback or an env override. This is the in-app equivalent of thebiometrics pipelinesection ofsp-status(docs/nats-frame-readers.md→ "Surfacing the selection in the app"). On NATS firmware, stop here: there are no.RAWfiles to inspect, and an empty/persistent/biometricsis expected. - Confirm
RAW_DATA_DIRmatches the tmpfs hot dir (/persistent/biometrics, per ADR-0018). A mismatch makes readers see an empty directory while frank writes fine — the classic silent failure (docs/sleep-detector.md§9). ls -la /persistent/biometrics/*.RAW— are files fresh (mtime moving)?journalctl -u frank— is the firmware writing at all?
Deep detail: docs/adr/0012-biometrics-module-system.md, docs/adr/0018-tmpfs-raw-frames.md.
Past nights can only be replayed (e.g. against the piezo-processor beat
detector) if frames reached /persistent/biometrics-archive/. The firmware
rotates *.RAW every ~15 min and unlinks the finished frame within a second, so
the archive depends on the linker pinning each frame with a hard link first
(ADR-0018).
sp-status—biometrics-archiver unitsmust showsleepypod-biometrics-linker.timeractive, and.RAW pinned (.pending)should be 1 while the firmware is writing.journalctl -u sleepypod-biometrics-linker -n 50 --no-pager— the linker is quiet on a no-op run;linked=lines appear once per rotation.dropped=means pinned frames were discarded because the archiver is not draining.journalctl -u sleepypod-biometrics-archiver -n 50 --no-pager— expectarchived=1roughly once per rotation.failed=means gzip could not write to eMMC (checkdf -P /persistent); a stuckpending=witharchived=0means frames are pinned but not being released.ls -la /persistent/biometrics /persistent/biometrics/.pending— one live frame with a moving mtime, plus its pin.
Deep detail: docs/adr/0018-tmpfs-raw-frames.md ("Rotation deletes the finished
frame").
The Thermal page now plots per-side target / bed / water trends. A stalled
pump shows as water + bed flatlining away from target while the side still
reports powered. The guard logic lives in src/hardware/pumpStallGuard.ts
(surfaced as the stalled verdict via health.thermal).
The HomeKit Pod pump left/right accessories expose pump-stall notices through
LeakSensor; the Home app's leak wording does not identify a water leak.
Check journalctl -u sleepypod.service for [pumpStallGuard] and compare the
firmware's pump duty / on-off commands in journalctl -u frank.
On startup, saved device_state.isPowered and a retained non-neutral target
with a zero countdown are insufficient to establish a running pump. The guard
waits for per-side live evidence: a positive session countdown with a non-neutral
target, positive pump duty or observed RPM >= 50 outside priming/spin-down
with a fresh non-neutral firmware status, or a successfully completed
non-neutral temperature command with a positive duration. Unknown-state frames
do not accumulate stall dwell. Confirmation is captured when each frame arrives,
so queued frames cannot inherit later evidence and wall-clock corrections cannot
revoke confirmation. A once-per-side “waiting for live pump/session
evidence” log explains this state; missing evidence is not reported as a stall.
Unresolved incidents remain blocked, and pending hardware cutoffs still retry.
If sleepypod.service has failed and journalctl -u sleepypod.service ends
with No production build at .next/standalone/server.js (older installs
restart in a loop with a pnpm permission error instead), the installed build is
gone, almost always because
next build / pnpm build was run on the pod. Next empties .next before
compiling, and the pod's 2GB of RAM can't finish the build. Recover by
reinstalling a pre-built bundle:
sp-update # on the pod: latest release
./scripts/deploy <pod-ip> # from a computer: your local checkoutNever build on the pod; next.config.mjs refuses to unless
SP_ALLOW_POD_BUILD=1 is set. See docs/DEPLOYMENT.md → Why Build Off-Device.
The systemd unit hardcodes /usr/local/bin/node — an ad-hoc node on
PATH is often the wrong ABI for better-sqlite3 and will fail to load. The
installer symlinks the correct binary (scripts/install); if you run scripts
by hand, use /usr/local/bin/node explicitly.
Service units, null-routed firmware domains, and the frank/Capybara split are
documented in docs/DEPLOYMENT.md.