Releases: LTimothy/nightstand
Releases · LTimothy/nightstand
Release list
v3.4.0
Presence-detection accuracy and honest status reporting.
Fixed
- Correctness and UI fixes from a frontend audit.
controlTempStoremerged
incoming device-status into the existing state object in place instead of
building a fresh one, so the optimistic and server-derived state ended up
sharing the same reference; it now merges into a new object. The Status page
could briefly show "Updated" at the Unix epoch before the first fetch
resolved (dataUpdatedAtstarts at 0); it now waits for real data. The
Clock component rebuilt both its alignment and tick timers on every render
because the effect was keyed on the clock value itself, occasionally firing
two ticks at the minute boundary; it now aligns once and ticks on a stable
interval. The water-level notification showed the raw internal
waterLeveldebug value to users, had a spelling error in "included", and
linked its report button to the wrong repo's issues; all three are fixed.
ControlTempPageno longer throws if a loaded settings object is missing a
side'sawayModekey, and the device settings reboot/wifi row's alignment
and stray margin are corrected. - An explicit 0°F target temperature was silently dropped.
targetTemperatureFis a number, andupdateDeviceStatus.tschecked it
with a plain truthiness test, so setting a side's target to exactly 0
never reached the hardware. The check now tests forundefinedinstead. - Turning biometrics off in Settings never actually stopped the stream
service. The toggle only wrotebiometrics.enabled: falseto the
database;scripts/disable_biometrics.shalready did the right
systemctl stop/disablebut was never wired up.POST /servicesnow
triggers it whenever biometrics is explicitly turned off, with a sudoers
rule for fresh installs and a self-heal path for pods that installed
before this change. - Status page could get stuck showing "needs attention" for two biometrics
jobs that weren't actually broken. The daily calibration job reported a
correct, intentional skip (bed occupied at the scheduled time) as a
failure, which could hold "Calibration job" on "needs attention" for up to
24 hours even though the previous calibration was still valid; it no
longer touches job status on a skip. Separately,update_pump_health
could latch a pump-stall warning and then freeze in that state once a side
powered off and stopped sending live pump readings, since the code
skipped frames with missing data instead of letting the warning clear;
missing readings are now treated the same as "not actively heating or
cooling," so the warning clears once a side goes quiet. - The calibration job's occupancy guard could see "occupied" when nobody
was in bed, silently skipping calibration. The live presence detector's
exit timer reset to zero on any single frame where a side read above the
noise threshold. A piezo permanently loaded by pillows or a mattress
topper idles close to that threshold and spikes above it every few
minutes, so an empty-but-loaded side could stay latched "present" for
hours and repeatedly block the nightly calibration run. A single spike no
longer resets the exit timer; only a short sustained run of dominant
frames counts as a real return. A separate, deeper fix (per-side baseline
learning) is tracked for later. - A departed or empty side of the bed could keep having heart rate, HRV,
and breathing rate written to the database.BiometricProcessor.next()
inserted vitals purely on a tick counter, with no check of whether that
side was actually occupied. On one incident night, the left side kept
inserting vitals for about 20 minutes after the left occupant got up: its
stored heart rate first converged on the right occupant's real heart rate
(picked up mechanically through the shared mattress frame), then, once
both occupants had left, settled into logging noise on a genuinely empty
bed. Vitals insertion is now gated on the existing per-sideself.present
flag. This closes the empty-bed case completely. It does not fully close
the cross-mattress case: if live presence detection is itself still
(incorrectly) reporting a side as present because it is picking up the
other side's signal, this gate has nothing to catch, since the flag it
reads is wrong in that moment. That crosstalk problem is a separate,
already-tracked presence-detection issue and needs its own fix. - The Logs viewer's pause buffer had no cap. The visible
logslist was
already trimmed to the last 1000 lines, butpendingLogs, which buffers
incoming lines while the viewer is paused, was not. Left paused against a
busy log file (the biometrics stream writes several times a second) with
the tab open, that buffer could grow without limit. This is a defensive
fix, not a confirmed cause of any specific crash:pendingLogsis now
capped the same waylogsalready was. - Offline sleep detection: capacitance-sensor presence never fired, making
the piezo+cap fusion piezo-only in practice.create_cap_baseline_from_cap_df
floors each sensor's calibration std at a minimum of 5 before it is used as
a z-score denominator indetect_presence_cap. That floor was sized for
older capSense hardware, whose raw values run in the hundreds to low
thousands; Pod 5's capSense2 records get pair-averaged down to a much
smaller scale (roughly 9-25), where the real empty-bed std measures under 1.
The floor of 5 dominated every sensor's z-score by two to three orders of
magnitude and suppressed real occupied-vs-empty signal enough that cap
presence fired on 0.0-0.1% of confirmed-occupied samples on a known-occupied
night, on both sides. The default floor is now 1, comfortably above the
measured empty-bed noise ceiling on this hardware; replayed against the same
incident night, cap presence now fires on 90.8-99.6% of confirmed-occupied
samples with no false positives across three independent confirmed-empty
stretches. - A single garbage sensor reading could blow up presence detection's signal
range and latch a false "present" state for hours._range_p98_p2had no
sanity check on raw piezo values, so an occasional sensor glitch (values
around 2^31, consistent with signed 32-bit overflow) flowed straight into
the percentile calculation and produced a range many times larger than any
real occupied reading. Raw samples are now masked out if their magnitude
exceeds a sane ceiling, checked symmetrically since int32 overflow wraps to
large negative values just as readily as large positive ones. The ceiling
sits well above the highest confirmed legitimate reading and well below the
known garbage magnitude, so real signal, including full-scale sensor
readings during vigorous motion, is untouched. - A fresh install, or a pod that just had biometrics turned on, could show
calibration or sleep analysis as "failed" on the Status page for no real
reason. Both jobs need archived RAW sensor data to run; on a fresh
install there isn't any yet, so they raised and that raise was reported the
same way as a genuine failure. They now report a distinct "collecting
data" status instead, shown as an informational chip rather than a
needs-attention one, and left alone by the page's unhealthy-status
checks. Also fixes a real bug found while making this change: calibration
could silently save a sensor baseline built over occupied bed time when no
clean empty-bed window had been recorded yet, which skews presence
detection toward reading the bed as occupied. That case is now treated the
same way as the missing-data case instead of being saved. - Strong crosstalk between the two sides of the bed could keep an empty
side reading as "present" for hours. When both sides read above the
noise floor and neither is clearly the louder one, live presence detection
could not tell genuine two-person occupancy apart from one side picking up
the other's movement through the bed frame, so it froze the exit timer
entirely rather than risk a wrong call. Under strong-enough crosstalk that
freeze never resolved. Each side now also tracks a rolling low-percentile
"floor" of its own recent signal and compares it against its own learned
occupied level; once that floor collapses toward what a genuinely empty,
crosstalk-only side looks like, the exit timer resumes counting instead of
staying frozen indefinitely. A capped fallback also guarantees the freeze
can never last forever even in an inconclusive case. The fix is
deliberately conservative: it is biased to keep a real, quiet sleeper
present rather than risk cutting a session short, so it narrows this
problem rather than fully closing it.
v3.3.0
[3.3.0] - 2026-07-10
Trust in the numbers, honest provenance, and a hardening pass.
Changed
- Offline sleep-presence detection now measures within-second signal
amplitude instead of a DC drift proxy. The nightly analyzer decided bed
occupancy by thresholding the second-to-second drift of each piezo second's
mean, a slowly-moving DC offset that carries no information about the
vibration amplitude a body actually produces. It false-fired on an empty bed
whenever the water pump cycled. Detection now uses the same per-second p98-p2
waveform range the live stream relies on, with the production-tuned 150k noise
floor separating "bed empty" from "someone in the bed", so pump spikes no
longer cross it. Phantom empty-bed presence disappears; on a normal night
session boundaries move by at most a few minutes, and the final exit is
detected the moment the signal drops below the floor. Known limit, documented
and not yet fixed: the piezo sensors pick up the other side of the bed through
the frame, so when two people leave at different times the earlier riser's
exit is still reported near the later one's. Correct per-side attribution is a
separate upcoming change.
Fixed
- Replaced the adjustable-base visualization's inherited bed frames with
original artwork. The 67 photorealistic per-degree PNG frames came from the
inherited tree with no attribution in their history and were renders of the
Pod's mattress. Because their provenance was unclear and this repo is public,
they were replaced rather than shipped. The visualization is now a single
parametric SVG (two hinged panels either side of a fixed centre platform)
drawn in the same rounded-stroke language as the preset icons, with the same
head and feet inputs and no external image assets. A follow-up polish pass
added a gradient mattress, quilting texture, eased angle tweens, and preset
icons generated from the same geometry the bed itself uses, so a button's icon
always matches what pressing it draws. The rest of the icon set was audited
alongside and is original work. - Security hardening across the API surface. A path-traversal arbitrary file
read onGET /api/logs/:filename(a request like/api/logs/../../etc/passwd
could read any file the server process can reach, CWE-22) is closed by
rejecting any filename whose basename doesn't match itself or that doesn't end
in.log. The CORS origin allow-list no longer trusts entire RFC1918 ranges;
it derives the pod's actual subnets from its network interfaces, correctly
handling a Tailscale interface alongside the LAN one.POST /api/executenow
range-checks numeric hardware arguments against the same bounds
/api/deviceStatusalready enforces.POST /api/settingsmerges validated
data instead of the raw body, so extra properties can't write arbitrary keys
into settings storage. The install-time clock sync moved from plaintext HTTP to
HTTPS, closing a LAN-in-the-middle clock-spoofing path. The documented
LAN-only, no-auth trust model is unchanged and deliberate; these close holes
that sit underneath it. - Patched known dependency advisories in both
server/andapp/via
npm audit fix, with no forced major bumps and bothpackage.jsonfiles
untouched. Test suites, typecheck, and lint are unchanged before and after.
v3.2.0
[3.2.0] - 2026-07-10
The in-app update system: pick a version, roll back instantly, migrate in.
Added
- A release manifest and in-app changelog.
releases.jsonat the repo root
lists every release, its channel (stableorbeta), its date, and sha256
hashes of the committed build artifacts, gated by a release-ritual test that
fails the build if the manifest andserverInfo.jsonever drift apart. The
update alert shows a "What's new" summary between the running and latest
builds, and a Changelog page shows the full history parsed from the pod's own
copy, with not-yet-installed entries prepended and marked. - Update channels, a version picker, and instant rollback. A "Software &
updates" Settings page lets you choose thestableorbetachannel, browse
every release with its notes and install any specific version including
downgrades (data is kept, migrations stay additive by standing rule), and roll
back to the previous install in seconds with no download.update.sh
understands a target-version request, resolves it against the manifest,
downloads the right tag archive, refuses a downgrade unless explicitly
allowed, and skips the migration step on a downgrade.rollback_pod.shand its
service swap the live install back to the tree the updater already keeps at
free-sleep-prev, health-check, and swap back on failure. - Fork-switch tool. A safety-obsessed migration script for anyone running
another fork who wants to move to this one. It locates and identifies the pod
read-only, reports what it found and what it would do, requires a typed
confirmation, then backs up the pod's code and data both on the pod and pulled
to your laptop (integrity-verified in both places) before touching anything. A
data-compatibility dry run loads your existing settings and schedules through
this fork's schemas and aborts on a real incompatibility. A dead-man sentinel
auto-restores your original fork within minutes if the installer is killed,
OOM'd, or the pod loses power mid-swap, with no laptop or user action needed.
It refuses Pod 1/2 outright and requires a typed acknowledgment for Pod 3/4.
v3.1.0
[3.1.0] - 2026-07-09
Reliability under real hardware, honest logs, and biometrics accuracy.
Added
- Live biometrics from the pod's own message bus. The stream service
consumes NATS JetStream directly (durable consumer, sequence deduplication)
instead of tailing RAW files, eliminating file-truncation races and lag, and
falls back to the RAW watcher automatically if NATS is unavailable. Adapted
from SFenton/free-sleep. - Multiple alarms per day, with a chip row to add, select, and remove them,
durations up to five minutes, and the legacy single-alarm field mirrored for
compatibility. Server model adapted from SFenton/free-sleep. - Pump-stall detection. When the pump stalls while the heating/cooling
element keeps drawing current, the hub water sensor reads stagnant water next
to a powered heater and reports a runaway temperature that isn't the bed's.
The pod already writes pump RPM, flow state, and element current into the RAW
stream; those are now parsed, watched per side with a dwell window in both
directions, and surfaced on the Status page. Detection and visibility only, no
automatic power-off yet. - Storage, memory, and a redesigned Status page. Indicators for
/persistent
usage (with a breakdown of the archive and logs) and for RAM headroom on a pod
with no swap. The 17 backend service checks are grouped into Schedules,
Biometrics and sensors, and a collapsible Core services section that only
expands when something needs attention, with plain-English descriptions and a
clear meaning for each status. - A clock on the main page that ticks on the browser's own time, so it stays
correct regardless of pod clock drift.
Fixed
- A wedged Franken command can no longer hang its caller forever. The
per-command timeout only covered reading the reply; time queued or blocked on
the socket write was unguarded, so one stuck command silently pinned every
later command and every/api/deviceStatuscoalesced onto it. Commands now
carry a total deadline that logs where they got stuck, tears down the
connection, and reconnects, so the server self-heals instead of hanging. /api/deviceStatusno longer blocks for 25-30s on a cold start. It returns
a fast 503 while Franken is still connecting instead of queueing behind the
handshake, so deploy and update health checks (and browsers) get a fast,
accurate answer and can retry.- Concurrent job-status writes no longer clobber each other. The biometrics
jobs POST status independently and landed within milliseconds; without
serialization the second write silently overwrote the first, leaving a job
stuck showing "Started" for hours after it had failed. Writes are serialized,
a temp-file watcher event that triggered a full reschedule on every write is
fixed, and a status still reading "started" across a restart is reconciled to
"failed". - Shutdown could crash on a concurrent teardown when the connect-retry loop
andSIGTERMtore down the Franken singletons at the same moment. They are now
captured into locals and cleared synchronously before anyawait. - Honest Python logging. The Node wrapper logged all Python stderr (including
routine DEBUG output) as errors, burying real problems; only a non-zero exit
is a failure now, and a failing script's stderr is included instead of dropped.
Unbounded log growth in the biometrics jobs (abackupCount=0that disabled
rotation entirely) and the Node server's silent truncation at 7MB are both
fixed with real rotation history. - Log viewer reads only the bytes appended since the last read instead of
re-sending the whole file on every write, with a filter box, pause/resume,
clear, download, and level-aware coloring. - Pod 5 capacitance data parses now:
capSense2records (8 values per side)
are normalized onto the legacy channels at load time, so calibration and sleep
analysis stop finding zero rows and failing. - Presence detection raised its empty-bed noise floor to 150k after an empty side
idling near the old threshold kept presence latched for hours.
Changed
- New original app icon (a crescent moon over a temperature bar); the previous
icon closely resembled the official app's. - Added
docs/EIGHT_SLEEP_PROTOCOL.md, a consolidateddac.sockcommand and RAW
telemetry reference written from our own research and cross-checked against
other reverse-engineering projects, with every entry tagged by verification
status.
v3.0.0
[3.0.0] - 2026-07-08
Initial release of Nightstand's own version stream, a hard fork descending from
jmew/free-sleep (presence detection, sleep stages, adjustable-base BLE control,
WebSocket live updates) and throwaway31265/free-sleep before it.
Added
- Own semver stream independent of upstream, with
upstreamBasetracked in
serverInfo.jsonfor deliberate, reviewed cherry-picking instead of
auto-merging. - In-app updater: the Settings Update button pulls new builds directly from this
project's GitHub, with backup, health check, and automatic rollback on failure. - RAW-file archive so overnight biometrics survive the firmware's ~75-minute
rolling-buffer truncation, and LAN deploy tooling (ops/deploy.sh,
ops/rollback.sh) with backup and rollback for development and emergencies. - Manual power-on now starts at the scheduled temperature, applying the
temperature the schedule would have reached by now. Adapted from
EpicPi/free-sleep. Temperature display respects the stored Fahrenheit or level
format instead of hardcoding one. - App and Python test runners, and a hosted mocked demo deployed to GitHub Pages
on every push so the app can be tried in a browser without a Pod.
Fixed
- Pod 5 biometrics: RAW records are no longer skipped. cbor2's C extension
read in 4096-byte chunks, so nearly every ~2700-byte Pod 5 record was silently
dropped and empty placeholder records ended the read loop early. Records are
now parsed with a byte-accurate reader, adapted from the original project's PRs
#46 and #50. - Adjustable-base page goes dormant instead of logging retry errors forever when
no base is configured, and the sleep-stage classifier requires HR and HRV
evidence for REM instead of defaulting to it. - Presence auto-off respects the user's power schedule instead of powering the
bed down mid-sleep.
Removed
- Sentry error-reporting integration; its DSNs pointed at an account this fork
can't read, so reports were write-only.