Web UI: merge the mount-time status seed into chart history (#124) - #247
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c68f8c1a2c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const series = state.tempDataSeeded ? state.currentTempData : []; | ||
| const currentTempData = appendPoint(series, { |
There was a problem hiding this comment.
Reset history when REST observes a new firing
When the WebSocket misses a firing transition—for example, the device is offline while a new firing starts—and /status is the first fresh observation, tempDataSeeded remains true and this reuses the previous firing's series. The new firing's lower elapsed-time point is then rejected by appendPoint; because the same seed also overwrites firingProgress with the new profile and active state, subsequent WebSocket frames no longer recognize the transition and keep being rejected until their elapsed time reaches the old series tail. Detect the same profile/elapsed/active discontinuities used by the WebSocket handler and reset the series before merging.
Useful? React with 👍 / 👎.
| newData = [...series, newPoint]; | ||
| if (newData.length > MAX_TEMP_POINTS) newData.shift(); | ||
| } | ||
| const newData = isNewFiring ? [newPoint] : appendPoint(state.currentTempData, newPoint); |
There was a problem hiding this comment.
Discard the placeholder when WebSocket supplies the first point
When a WebSocket frame is the first real reading after a fresh mid-firing load—such as when the 1 Hz stream beats GET /status—or after resetTempData(), tempDataSeeded is false but this branch still passes the synthetic {time: 0, temp: 20} point to appendPoint. For elapsed times beyond minute zero that placeholder remains and draws a fictitious ramp to the live temperature; on initial load the REST response that would otherwise remove it is then rejected by the lastUpdateAt guard. Start from an empty series here whenever tempDataSeeded is false, as seedFromStatus already does.
Useful? React with 👍 / 👎.
|
Both findings were right and are fixed in P1 — reset history when REST observes a new firing. Confirmed, including the follow-on the comment identified: because the seed also adopts the new The shared signals now live in P2 — discard the placeholder on the WebSocket path. Also right, and the reasoning about the REST snapshot then being rejected by the staleness guard is what makes it stick rather than self-correct. Fixed the same way Verified red first: disabling the reset fails the new-firing test; restoring the placeholder append fails four. Generated by Claude Code |
47d9bce to
29e24e1
Compare
|
Rebased onto Resolved by keeping both sides rather than either: the component keeps
Generated by Claude Code |
The Dashboard tab is not forceMount'ed, so it remounts on every visit and
re-runs its mount-time GET /status seed. That seed replaced currentTempData
outright with a single "now" point — but the store and the WebSocket both
live at the app level and keep accumulating readings while the user is on
another tab. Visiting Settings mid-firing and coming back collapsed hours of
chart history into one point, and the curve visibly restarted.
The same code was also the losing side of a REST/WS race: frames arriving
between fetch dispatch and resolution were overwritten by the older snapshot.
firingProgress self-corrected on the next frame; the deleted chart did not.
Move the seed into the store as seedFromStatus(status, dispatchedAt), which:
- drops the snapshot entirely when a frame is stamped at or after the
moment the request went out, since that frame describes a later state;
- otherwise folds the reading into the existing series through the same
dedupe-by-minute path the WebSocket handler uses, rather than replacing
it. A new tempDataSeeded flag distinguishes real history from the
synthetic 20 C placeholder, so a first seed still replaces.
appendPoint() is now shared by both entry points, and drops a point that
lands before the series tail instead of appending it — out-of-order inserts
draw the chart's X axis running backwards.
Keeping the tab mounted would also have preserved the store, but it would
mount Recharts' ResponsiveContainer into a hidden, zero-width container; the
seed was wrong on a full page reload too, which forceMount would not fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyJ47o5sWvpb9vMk1RNKkb
Two gaps in the seed rework, both found in review of #124. The seed adopted the snapshot's profile and active state while merging its reading into the existing series. When the WebSocket missed a firing transition — the device offline while the next firing began — /status is the first observation of it, so the new firing's low-elapsed point was dropped by appendPoint as out-of-order, and because firingProgress had already moved on, the WebSocket handler no longer saw a discontinuity either. The chart stayed on the previous firing's curve until the new one's elapsed time passed the old tail. The same discontinuity signals the WebSocket handler uses now live in isDifferentFiring() and are applied on both paths. Rewind is left to the callers because they measure it against different baselines: consecutive frames come from one monotonic source, so any decrease is a restart, while the snapshot is a second source and is compared against the plotted tail at the chart's one-minute resolution — sub-minute skew between the two collapses onto the same bin instead of reading as a restart. The WebSocket path also kept the synthetic 20 C placeholder when a frame was the first real reading (the 1Hz stream beating GET /status on a mid-firing load, or the first frame after resetTempData), drawing the cold-start ramp #192 removed — and on that path the REST snapshot that would have replaced it is rejected by the staleness guard. It now starts from an empty series whenever tempDataSeeded is false, as the seed does. Both fixes verified red first: disabling the reset fails the new new-firing test, and restoring the placeholder append fails four. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PyJ47o5sWvpb9vMk1RNKkb
29e24e1 to
1f5cefb
Compare
|
Rebased again onto
So One test dropped, deliberately. "discards the placeholder when a frame is the first real reading" no longer tests this PR — on top of #241 it passes trivially because the series already starts empty. The two other conflicts were comment wording; I took #241's, which is more accurate now. The P1 fix is unaffected and still has teeth — re-verified red by neutralising the reset:
Generated by Claude Code |
Fixes #124.
The bug
The Dashboard tab is not
forceMount'ed, so it remounts on every visit and re-runs its mount-timeGET /statusseed. That seed replacedcurrentTempDataoutright with a single "now" point — but the zustand store and the WebSocket both live at the app level and keep accumulating readings while the user is on another tab. Visiting Settings mid-firing and coming back collapsed hours of chart history into one point, and the curve visibly restarted.The same code was also the losing side of a REST/WS race: frames arriving between fetch dispatch and resolution were overwritten by the older snapshot.
firingProgressself-corrected on the next frame; the deleted chart history did not come back.The fix
The seed moves into the store as
seedFromStatus(status, dispatchedAt):dispatchedAtis stamped before the request goes out, so any WebSocket frame stamped at or after it describes a later state than the snapshot in hand. When that's the case the snapshot is dropped entirely.tempDataSeededflag distinguishes real history from the synthetic 20 °C placeholder, so a first seed (fresh page load) still replaces — no fictitious curve from 20 °C up to the live reading.appendPoint()is now shared by both entry points, and drops a point that lands before the series tail rather than appending it — the chart's X axis is point order, so an out-of-order insert draws the curve running backwards.Why not
forceMountThe issue offered either fix. Keeping the tab mounted would preserve the store, but it mounts Recharts'
ResponsiveContainerinto a hidden, zero-width container, and it wouldn't fix the seed being wrong on a full page reload — which the race guard does.Testing
make test-web— 196 passed, 1 skipped. Seven new cases inkilnStore.test.tscover the first seed, the tab revisit, same-minute collapse, the in-flight-frame race, an out-of-order snapshot, and both profile-selection paths.npm run typecheck/lint/format:checkclean.Generated by Claude Code