Fix Progress tab highlighting the wrong step on reconnect/Save

Reconnecting to (or any client-side Save fetch of) an already-running
Sud showed the wrong step active and bogus remaining times, traced to
two independent bugs:

- recv()'s 'Save' handler always called send_forecast(doc), which
  resimulates the *entire* schedule cold from step 0 - fine for a
  not-yet-started Sud, but for one already running it silently
  overwrote the forecast/forecast_step_starts that _reanchor_forecast()
  had been accurately, continuously maintaining with a context-free
  "starting now" guess. Now skipped whenever a run is already in
  progress, re-sending what's already there instead.

- _reanchor_forecast() recomputes each step's real start time
  asynchronously, so that recomputation can itself be (and routinely
  is) superseded and discarded by a later transition's reanchor before
  ever committing - and the next *successful* commit's "everything
  before my index is real" filter would then preserve whatever stale
  prediction was sitting there before, sometimes for a step that
  hadn't even happened yet by the schedule's real position.
  on_step_changed() now also records each step's start synchronously,
  immune to that race.

- Client-side, update_sud_forecast() unconditionally reset sud_step_
  index/forecast_step_starts/etc. on every 'Json' push - correct for
  an actual Load, but connect()'s unconditional Save request produces
  a standalone 'Json' push (no Step/State alongside it, unlike the
  subscribe-triggered replay) for the *same* already-running schedule,
  wiping the correct state with nothing left to restore it. Now only
  resets when the incoming doc actually differs from the last one
  processed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qvu5giu7gvRCyEWzf2Vpx
This commit is contained in:
2026-06-24 18:15:45 +02:00
co-authored by Claude Sonnet 4.6
parent 6faf1a1f12
commit 8896f216d1
2 changed files with 82 additions and 17 deletions
+43 -1
View File
@@ -137,6 +137,31 @@ class SudTask(ATask):
self.tc.set_theta_soll(step['temperature'])
self.tc.set_heatrate_soll(ramp['rate'])
self.apply_stirrer(phase)
# Records the moment this step actually began *synchronously* -
# only on the genuine first entry (ramping, per Sud._advance()
# unconditionally setting state to RAMPING first - see this
# method's own comment above), not the same step's later ramp->
# hold switch, so this can't itself get overwritten by that
# switch's slightly later timestamp. Unconditional, not
# setdefault: an *earlier* reanchor's own forward-looking
# simulation may already have written a prediction for this
# same index (it simulates every remaining step from its own
# anchor point, real ones included) - that guess must be
# replaced now that the real thing has actually happened,
# never left to linger as if it still were one.
#
# _reanchor_forecast() recomputes this same entry too, but
# asynchronously, so it can be (and routinely is, e.g. on the
# very next step boundary arriving before its own simulation
# finishes) superseded and discarded before ever committing -
# see its own comment. Without this synchronous copy, a later
# reanchor's "everything before my own index is real and
# immutable" filter would then preserve whatever *that*
# discarded call's predecessor had left behind instead - a
# stale prediction, sometimes even one that hasn't happened
# yet by the schedule's real current position.
if ramping:
self.forecast_step_starts[self.sud.index] = self.sud.elapsed
# Every real step boundary (full step change or ramp->hold
# within one) is a trustworthy checkpoint to correct the
# forecast against - see _reanchor_forecast(). Catches drift
@@ -370,7 +395,24 @@ class SudTask(ATask):
elif 'Save' in pair[0]:
doc = self.sud.save()
await self.send({'Json': doc})
await self.send_forecast(doc)
# A run already in progress (e.g. a client connecting mid-
# brew, which always fires this on connect - see client/
# brewpi_gui.py's Window.connect()) must NOT get send_
# forecast()'s cold, from-step-0 simulation here: it knows
# nothing of the real current step/elapsed/temperature, so
# it would silently overwrite the forecast/forecast_step_
# starts that's been accurately, continuously maintained
# by _reanchor_forecast() all along with a context-free
# "starting fresh right now" guess - the exact bug that
# made a freshly-connected client highlight/countdown the
# wrong step. Just re-send what's already there instead;
# only a genuinely not-yet-started schedule (IDLE/DONE)
# has nothing accurate yet to preserve, so it alone still
# gets the real, full computation.
if self.sud.state in (SudState.IDLE, SudState.DONE):
await self.send_forecast(doc)
else:
await self._send_forecast()
elif 'Load' in pair[0]:
if self.sud.load(pair[1]):
await self.send({'Name': self.sud.name, 'Description': self.sud.description})