Skip to content

Make control surfaces reflect server truth (no panel-local optimism) - #29

Merged
adborbas merged 3 commits into
mainfrom
fix/control-surface-server-truth
Jul 16, 2026
Merged

Make control surfaces reflect server truth (no panel-local optimism)#29
adborbas merged 3 commits into
mainfrom
fix/control-surface-server-truth

Conversation

@adborbas

Copy link
Copy Markdown
Owner

Changes

  • Switch panel: dropped the local @State isOn mirror + onChange reconcile; the power button now binds directly to viewModel.isOn. Kept isPressed local.
  • CommitCooldown: replaced the pure time-computed isSuppressed with a stored, observed flag driven by a cancellable expiry timer, so window expiry is observable and re-commits debounce (window measured from the last commit). duration is injectable for tests; expiryTask is exposed read-only so tests await expiry deterministically.
  • Light/Climate sliders: now reconcile to the model on failure — once the observable window expires, brightness/targetTemp re-read server truth and the panel's onChange reverts the slider. Each commit clears the other pending values so a stale pending can't resurface under the shared cooldown.
  • Cover: position stays server truth (card, subtitle, iconTapped); a dedicated pending-aware sliderPosition drives the panel slider so it reconciles on failure without ever showing unconfirmed state on the card.

Why

Control panels kept panel-local optimistic state that reconciled only via onChange keyed on the model — which fires on success but not on failure. A rejected/failed service call left a toggle or slider showing a state the server never accepted, disagreeing with the card behind it. Per the recorded decision (control-surface-state-pattern.md, Option A), control surfaces reflect the @Model (server truth); the UI never shows unconfirmed state. Toggles bind directly; sliders keep local drag state but reconcile on completion including failure.

Notes

  • Product effect: toggling in a panel now shows a visible round-trip before the control moves (already how every card behaves) — consistent, not a regression.
  • No changes to controlling-protocol signatures, controllers, or demo/mock conformers.
  • Tests: new CommitCooldownTests (immediate suppress, expiry, re-commit debounce) plus VM-level tests that brightness/targetTemp/sliderPosition return the pending value while suppressed and revert to the model on expiry; a cover test asserts position stays server truth during a pending commit; a light test locks in the no-stale-pending behaviour. HemeraTests: 376 tests pass.

adborbas and others added 3 commits July 16, 2026 18:34
Per the control-surface-state-pattern decision (Option A):

- Switch panel (Rule 1): drop the local @State isOn mirror + onChange
  reconcile; the power button now binds directly to viewModel.isOn, so a
  failed service call can no longer leave it stuck disagreeing with the card.

- CommitCooldown (Rule 2): replace the pure time-computed isSuppressed with
  a stored, observed flag driven by a cancellable expiry timer. Window
  expiry is now observable, so after a failed light/climate slider commit the
  VM re-reads the model and onChange reverts the slider (~1s). Re-commits
  debounce by cancelling the prior timer. Duration stays injectable for tests.

- Cover: adopt the shared CommitCooldown with a pendingPosition so its
  position slider reconciles on failure like Light/Climate.

Tests: new CommitCooldownTests (immediate suppress, expiry, recommit
debounce) plus VM-level tests that brightness/targetTemp/position return the
pending value while suppressed and revert to the model after expiry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert the multi-line comments introduced in the previous commit to the
project's /** ... */ block convention. Single-line // comments untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two fixes from the review of the control-surface-server-truth change:

- Cover (finding 2): position reverted to server truth (cover.currentPosition)
  so the card tile, subtitle, and iconTapped never reflect an unconfirmed
  value. The slider's optimistic-with-failure-reconcile behaviour moves to a
  dedicated pending-aware sliderPosition, which CoverControlPanel now observes.

- Light/Climate (finding 1): one CommitCooldown gates several pending values,
  so each commit now clears the others first (resetPending). Otherwise a stale
  pending from an earlier commit resurfaced and masked server truth when a
  different property was committed within the shared window.

Tests: cover asserts position stays server truth during a pending commit and
sliderPosition reconciles on expiry; a new light test locks in the no-stale-
pending behaviour. Timing-dependent tests now await CommitCooldown.expiryTask
instead of racing a fixed sleep against the scheduler (removes flakiness).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@adborbas
adborbas merged commit dadca8a into main Jul 16, 2026
1 check passed
@adborbas
adborbas deleted the fix/control-surface-server-truth branch July 16, 2026 20:37
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.

1 participant