Skip to content

Web UI: merge the mount-time status seed into chart history (#124) - #247

Merged
BenSeverson merged 2 commits into
mainfrom
claude/fix-124-chart-history
Jul 29, 2026
Merged

Web UI: merge the mount-time status seed into chart history (#124)#247
BenSeverson merged 2 commits into
mainfrom
claude/fix-124-chart-history

Conversation

@BenSeverson

Copy link
Copy Markdown
Owner

Fixes #124.

The bug

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 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. firingProgress self-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):

  • Staleness guard. dispatchedAt is 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.
  • Merge, never replace. Otherwise the reading is folded into the existing series through the same dedupe-by-minute path the WebSocket handler uses. A new tempDataSeeded flag 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 forceMount

The issue offered either fix. Keeping the tab mounted would preserve the store, but it mounts Recharts' ResponsiveContainer into 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 in kilnStore.test.ts cover 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:check clean.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 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".

Comment thread web_ui/src/app/stores/kilnStore.ts Outdated
Comment on lines +101 to +102
const series = state.tempDataSeeded ? state.currentTempData : [];
const currentTempData = appendPoint(series, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Both findings were right and are fixed in 47d9bce.

P1 — reset history when REST observes a new firing. Confirmed, including the follow-on the comment identified: because the seed also adopts the new profileId/isActive, the WebSocket handler stops seeing a discontinuity too, so points keep being dropped until the new firing's elapsed time passes the old tail.

The shared signals now live in isDifferentFiring() and are applied on both paths. Rewind is deliberately not in the helper, because the two callers measure it against different baselines: consecutive frames come from one monotonic source, so any decrease in raw seconds is a restart; the snapshot is a second source, and a /status computed a few hundred ms either side of the last frame can legitimately report a second less elapsed. Comparing raw seconds there would have made an ordinary clock skew wipe the chart, so the seed compares against the plotted tail at the chart's one-minute resolution — skew collapses onto the same bin, a whole minute behind cannot belong to the firing being plotted. There's a test pinning that.

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 seedFromStatus does it.

Verified red first: disabling the reset fails the new-firing test; restoring the placeholder append fails four. make test-web — 198 passed, 1 skipped.


Generated by Claude Code

@BenSeverson
BenSeverson force-pushed the claude/fix-124-chart-history branch from 47d9bce to 29e24e1 Compare July 29, 2026 16:21

Copy link
Copy Markdown
Owner Author

Rebased onto main (2937b8a) — #244 landed delayRemaining in the exact useKilnStore.setState block this PR replaces, so the branch went un-mergeable.

Resolved by keeping both sides rather than either: the component keeps lastUpdateAt (which #244 needs to anchor the "starts at" wall-clock label) alongside seedFromStatus, and delayRemaining: s.delayRemaining ?? 0 moves into seedFromStatus in the store, where the rest of the snapshot mapping now lives. Dropping it would have made the countdown blank on every tab revisit — a silent regression of #204, not a merge artifact anyone would have spotted in review.

make test-web — 214 passed, 1 skipped, against regenerated fixtures. Typecheck, eslint and prettier clean.


Generated by Claude Code

claude added 2 commits July 29, 2026 16:26
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
@BenSeverson
BenSeverson force-pushed the claude/fix-124-chart-history branch from 29e24e1 to 1f5cefb Compare July 29, 2026 16:28

Copy link
Copy Markdown
Owner Author

Rebased again onto main (28965c4) after #241 merged. That one is more than a textual conflict, so worth reading before merging.

tempDataSeeded is gone. It existed for exactly one job: telling real history apart from the synthetic {time: 0, temp: 20} placeholder, so a first seed would replace rather than draw a fictitious ramp up from 20 °C. #241 landed #192, which removes that placeholder — initialTempData is now []. With an empty starting series, appendPoint([], point) and the flag's replace branch produce identical results in every case, including after resetTempData(). Keeping it would have been a flag guarding a condition that can no longer occur, and the diff would have invited exactly that question in review.

So keepHistory = tempDataSeeded && !restarted collapses to restarted ? [point] : appendPoint(...), and the WebSocket path's placeholder branch collapses back to the plain appendPoint.

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:

FAIL  kilnStore.test.ts > seedFromStatus (#124) > starts a new series when the snapshot is the first sight of a new firing

make test-web — 21 files, 227 passed, 1 skipped, against regenerated fixtures. Typecheck, eslint and prettier clean.


Generated by Claude Code

@BenSeverson
BenSeverson merged commit 8ecb4c4 into main Jul 29, 2026
12 checks passed
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.

[Medium] Returning to the Dashboard tab wipes accumulated live chart history

2 participants