Skip to content

feat: on-device history + MINMAX/DAYCOMP display modes - #3

Merged
varffs merged 10 commits into
masterfrom
feat/history-display-modes
Jul 27, 2026
Merged

feat: on-device history + MINMAX/DAYCOMP display modes#3
varffs merged 10 commits into
masterfrom
feat/history-display-modes

Conversation

@varffs

@varffs varffs commented Jul 27, 2026

Copy link
Copy Markdown
Owner

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 temperature
  • DAYCOMP — today's internal min/max above yesterday's
  • Button cycle is now DEFAULT → MINMAX → DAYCOMP → DIAG

Backed 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/record action is dispatched per poll cycle after both sensors settle; the reducer composes the sample from state.data and 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:

  • No persistence of any kind. No database, no JSON snapshot, no append log. 288 samples/day doesn't justify SQLite, and a native module would add an armv6 build step to every deploy. History resets on restart.
  • No dashboard, no query API, no iotplotter backfill.
  • The ring lives in the app, not in piteknix — promotion waits for a real second consumer (the bathroom device).

Accepted consequences: DAYCOMP shows yst 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.4 and 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, and tdy L-12.4 H-10.1 is 17 characters on a 16-column display. Whole degrees fit with margin and keep the L/H labels. DEFAULT still shows the live reading to one decimal.

A pre-existing sensor bug this feature would have amplified. pollInternal caught 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 answering MINMAX/DAYCOMP for 24–48 h after the sensor recovers — silently flattening an outage into a plausible-looking night. Fixed here: the catch now nulls both fields, and formatDataLines guards line 0 so a null internal renders int: -- -- rather than int: 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:

DEFAULT   |int: 23.0c 66.8%|
MINMAX    |i24 L23 H23     |
DAYCOMP   |tdy L23 H23     |   yst L-- H-- (no midnight crossed)
DIAG      |ext: ok         |

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.md
Plan: docs/superpowers/plans/2026-07-26-history-display-modes.md

Follow-ups, not in this PR

  • Stamp v2.0.0 on release (package.json is still 0.1.0)
  • Deploy to the Pi — remember git checkout -- package-lock.json before pulling on-device
  • formatDataLines still emits ext: -- (unknown) at 17 chars — pre-existing, untouched

🤖 Generated with Claude Code

varffs and others added 9 commits July 26, 2026 23:47
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.samples ring 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.

Comment thread src/render.js
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment thread src/history.js
@varffs
varffs merged commit cde5dc6 into master Jul 27, 2026
1 check passed
@varffs
varffs deleted the feat/history-display-modes branch July 27, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants