feat: on-device history + MINMAX/DAYCOMP display modes - #3
Merged
Conversation
Two new 16x2 modes fed by a 48h in-memory ring of sensor snapshots: rolling 24h min/max (internal + external temp) and today-vs-yesterday internal min/max. Records non-goals settled during brainstorming so they don't get re-litigated: no database, no persistence of any kind, no dashboard or iotplotter backfill. 288 samples/day doesn't justify SQLite, and a native module would add an armv6 build step to every deploy. Ring holds 48h because DAYCOMP's "yesterday" needs data from up to 48h back just after midnight. Recording is a separate history/record action dispatched once both polls settle, since the existing data/* dispatches aren't atomic and either sensor can fail alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four TDD tasks: history slice (record/prune/cap/clock-sanity), pure selectors, the two formatters plus mode registration, then app wiring. Also amends the spec's line layout. The mockup fitted 16 chars only for single-digit negatives -- "i24h L-13.2 H41.5" is 17. Labels shortened to three chars (i24/e24), which holds at the worst realistic values (-19.9 / 49.9). Missing data now renders "L-- H--" on all four lines instead of a second "no data" wording. Notes that adding two modes breaks the existing mode-cycling test in store.test.js, and that the pre-existing 17-char "ext: -- (unknown)" line in formatDataLines is out of scope. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ndow to 49h pollInternal's catch left the last-good temperature/humidity in state on a failed read, which then got written into the history ring every poll — MINMAX and DAYCOMP kept answering with stale data for up to 49h after the sensor recovered. Null both fields in the catch, matching pollExternal's null-before-diagnose behaviour, and guard formatDataLines' internal line so a null renders as "int: -- --" instead of the misleading "int: 0c 0%". Also corrects the pollAll comment that inaccurately described both sensors' failure behaviour as the same, and widens HISTORY_WINDOW_MS from 48h to 49h so DAYCOMP's "yesterday" doesn't lose its first hour on the evening a DST fall-back lengthens a local day. The prune test now derives its offsets from the constant instead of hardcoding a number, so it actually pins the retention boundary. Adds a regression test for renderDisplay's now default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… 49h fixes Keeps the whole-degree precision edits already in the working tree and adds targeted corrections on top: the sample shape was missing pressure, the retention section still said 48h with the wrong "two days back" reasoning instead of the DST fall-back one, and the display section didn't record that whole-degree precision was a build-time amendment rather than the original design. Plan gets the same 48h to 49h correction wherever it describes current behaviour; historical TDD snippets and the already-merged commit-message quote for Task 1 are left as accurate history. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an in-memory, on-device history ring to the polytunnel monitor and introduces two new LCD display modes (MINMAX, DAYCOMP) driven by pure selectors/formatters, plus fixes an internal-sensor failure case to avoid silently reusing stale readings.
Changes:
- Add
history.samplesring buffer recorded once per poll cycle, pruned to a 49h window and hard-capped for clock anomalies. - Introduce pure selectors (
src/history.js) and new render modes/formatters (MINMAX,DAYCOMP) with whole-degree summaries. - Change sensor failure behavior so a failed internal read explicitly dispatches nulls (and update tests/docs accordingly).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/store.js | Adds history slice, ring retention/cap constants, record action, and expands display mode cycle to 4 modes. |
| src/store.test.js | Updates mode-cycle test and adds reducer-level history recording/pruning/capping tests plus a MINMAX integration-style assertion. |
| src/sensors.js | On internal sensor read failure, dispatches null temperature + humidity instead of dispatching nothing. |
| src/sensors.test.js | Updates internal-sensor fault test to assert null dispatches. |
| src/history.js | Adds pure time-window, min/max, and local-day bucketing selectors. |
| src/history.test.js | Adds unit tests for selector windowing, null-skipping, and local-midnight day separation. |
| src/render.js | Adds MINMAX/DAYCOMP formatters and threads an injectable now through renderDisplay; adjusts DEFAULT formatting for null internal temp. |
| src/render.test.js | Adds tests for new formatters/modes and updates DEFAULT placeholder expectation; verifies 16-char max for new lines. |
| src/backlight.test.js | Updates expectations to match the new mode order after button presses while lit. |
| app.js | Records one history sample per poll cycle and triggers re-render on history/* actions. |
| CHANGELOG.md | Documents new history display modes and internal sensor failure behavior fix. |
| docs/superpowers/specs/2026-07-26-history-display-modes-design.md | Adds design doc describing data model, retention rationale, selectors, and display behavior. |
| docs/superpowers/plans/2026-07-26-history-display-modes.md | Adds implementation plan for the history ring and new display modes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
pollInternal dispatches temperature and humidity as separate data/* actions and the render listener fires on each, so on sensor recovery the frame between them showed a fabricated 'int: 21.4c 0%' — formatFloat renders null as '0'. Humidity now renders '--' when absent, keeping a valid temperature visible rather than blanking the whole line. Addresses Copilot review on PR #3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds on-device history to the polytunnel monitor and two new 16x2 screens driven by it. The device can now answer "how cold did it get last night, and was that worse than the night before?" without reaching for a phone.
What's here
MINMAX— rolling 24 h min/max, internal and external temperatureDAYCOMP— today's internal min/max above yesterday'sDEFAULT → MINMAX → DAYCOMP → DIAGBacked by an in-memory ring of sensor snapshots pruned to 49 h, with all display values derived by pure selectors at render time. One
history/recordaction is dispatched per poll cycle after both sensors settle; the reducer composes the sample fromstate.dataand the payload carries only the timestamp, so tests drive time without mocking the clock.Deliberate non-goals
Settled during design, recorded so they don't get re-litigated:
piteknix— promotion waits for a real second consumer (the bathroom device).Accepted consequences:
DAYCOMPshowsyst L-- H--until the device crosses a midnight, and for the first hour after a restart the "24 h" window holds only samples since boot.Two things worth a reviewer's attention
Whole degrees, not one decimal. The original spec drew
i24 L-3.2 H31.4and reasoned about a worst case of "negative min, positive max". That's wrong: the real worst case is an all-day freeze where both values are two-digit negative, andtdy L-12.4 H-10.1is 17 characters on a 16-column display. Whole degrees fit with margin and keep the L/H labels.DEFAULTstill shows the live reading to one decimal.A pre-existing sensor bug this feature would have amplified.
pollInternalcaught a failed read and dispatched nothing, so state kept the last good value. That was a momentary wrong reading on one screen; with a history ring it becomes 288 fabricated samples a day that keep answeringMINMAX/DAYCOMPfor 24–48 h after the sensor recovers — silently flattening an outage into a plausible-looking night. Fixed here: the catch now nulls both fields, andformatDataLinesguards line 0 so a null internal rendersint: -- --rather thanint: 0c 0%.Also fixed: the retention window is 49 h rather than 48 h, because on the evening a DST fall-back lengthens a local day,
DAYCOMP's "yesterday" starts up to 48 h 59 m before now.Verification
56/56 tests passing. Everything added is pure — no hardware, no filesystem, no clock mocking.
Full mode cycle exercised in virtual mode end to end:
Not verified: the dark→wake button transition (boot leaves the display lit, so the driven run never started dark) — covered by unit tests rather than by that run. Nothing has run on the real Pi yet.
Design:
docs/superpowers/specs/2026-07-26-history-display-modes-design.mdPlan:
docs/superpowers/plans/2026-07-26-history-display-modes.mdFollow-ups, not in this PR
v2.0.0on release (package.jsonis still 0.1.0)git checkout -- package-lock.jsonbefore pulling on-deviceformatDataLinesstill emitsext: -- (unknown)at 17 chars — pre-existing, untouched🤖 Generated with Claude Code