Skip to content

feat(ui): optimistic UI cascades + clickAction helper - #240

Merged
hughobrien merged 11 commits into
mainfrom
spec/optimistic-ui-cascades
May 12, 2026
Merged

feat(ui): optimistic UI cascades + clickAction helper#240
hughobrien merged 11 commits into
mainfrom
spec/optimistic-ui-cascades

Conversation

@hughobrien

Copy link
Copy Markdown
Owner

Summary

  • Replaces six ad-hoc speculative-UI fixes with a single clickAction helper plus a cascade table (cmd/breezyd/ui/templates/cascades.go). Each click handler is now a one-liner naming its primary signal write; implied cross-signal updates come from the cascade table.
  • Adds effPower(power, special) derivation in layout.templ for the one state externally-induced clients can leave incoherent (panel button starts timer with $power=false).
  • Adds server-side mirror: SetSpeedPreset/SetSpeedManual ops now also write 0x0007=0, keeping the daemon's cache coherent with firmware behavior and fixing the MemClient-backed Playwright failure mode.
  • Spec + plan committed alongside: docs/superpowers/specs/2026-05-11-optimistic-ui-cascades-design.md and docs/superpowers/plans/2026-05-11-optimistic-ui-cascades.md — incl. the office-device firmware-invariant probe results from 2026-05-11.
  • New Playwright test pins the headline behavior: click a preset chip with night-mode active → night chip aria-pressed flips to false within 100ms (only passes if the optimistic cascade fires client-side). Second new test pins effPower against externally-induced power=off + timer=night state.

Test plan

  • just ci green (vet + race + golangci-lint + asan + msan + Playwright + templ-drift + test-test-admin)
  • 28/28 Playwright tests pass, including the two new ones
  • Foundation unit tests: TestCascades_*, TestClickAction_*, TestCascadeTable_AllWritableSignalsCovered
  • Per-handler tests updated: TestPresetClickExpr, TestTimerClickExpr, TestPowerButtonExpr

🤖 Generated with Claude Code

hughobrien and others added 11 commits May 11, 2026 21:25
Spec for replacing ad-hoc speculative-update fixes with a single
clickAction helper + cascade table. Includes the office-device probe
results (2026-05-11) that established which cross-signal effects are
firmware-driven vs. handler-driven.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Plan implements the design in 8 sequential tasks (Task 0 foundation, 1
red Playwright test, 2-6 migrations, 7 effPower + Layer A wrap-up).
Refines the spec to use a __next intermediate in clickAction so toggle
expressions evaluate once — the cascade reads the just-mutated signal
directly, and the POST payload references __next so wire value matches
locally-written value.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Foundation for centralized optimistic UI updates. No callers yet —
that lands per-handler in subsequent commits. Spec:
docs/superpowers/specs/2026-05-11-optimistic-ui-cascades-design.md
Currently failing (night chip stays pressed for the SSE roundtrip).
Task 2 of the cascades plan makes this pass.
Preset clicks now optimistically clear $specialMode via the speedMode
cascade. Closes the SSE-roundtrip window where the night/turbo chip
stayed visually pressed after a preset selection.

Also updates the preset-editor open/close UX: the editor toggles only
when re-clicking the already-active preset. Clicking a non-active
preset now selects it (delegated to clickAction) without expanding the
editor — the wasActive flag in presetClickExpr captures pre-click
signal state so the editor decision sees the device's true prior
preset, not the just-optimistically-written one. The four existing
Playwright tests that previously opened the editor via a non-active
preset click are updated to seed preset2 as the active speed and
re-click 48/49.

NOTE: the Task 1 regression test (preset chip click optimistically
de-lights active timer chip) still fails on the MemClient backend used
by Playwright. The cascade fires client-side as designed, but the
daemon's notifyAfterWrite immediately ships a fresh card whose cached
$specialMode still reads 'night' because MemClient doesn't simulate
firmware's auto-clear-on-speed-write behavior. The SSE push lands
within ms and overwrites the optimistic cascade. On real hardware the
same race exists for a poll-tick window. Follow-up task needed:
either daemon-side handler cascade (mirror firmware in /speed by
explicitly writing timer=0 too) or MemClient firmware-invariant
simulation. Out of scope for Task 2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The firmware clears 0x0007 on any 0x0002 write — encode that
invariant at the ops layer so the daemon's cache stays coherent
without waiting for the next poll, and the MemClient backend
(Playwright tests) sees the same effect. Closes the gap where
the post-write SSE push overwrote the client-side cascade with
stale specialMode state.

Completes Task 2 of docs/superpowers/plans/2026-05-11-optimistic-ui-cascades.md.
External actors (panel button, IR remote) can leave the device in
timer-active + power-off state. Dashboard's power button now reads
effPower($power, $specialMode) so it shows pressed regardless.
@hughobrien
hughobrien enabled auto-merge (squash) May 12, 2026 04:26
@hughobrien
hughobrien merged commit 8b7e433 into main May 12, 2026
6 checks passed
@hughobrien
hughobrien deleted the spec/optimistic-ui-cascades branch May 12, 2026 04:28
hughobrien added a commit that referenced this pull request May 12, 2026
…slider race) (#241)

Four targeted follow-ups to the optimistic-UI cascades PR (#240):

1. **Power-off → timer-clear at the ops layer.** Probed the office device
   2026-05-12 (with night-timer to avoid the inconclusive turbo-self-clear
   from the original Test 4): firmware DOES clear 0x0007 when 0x0001 goes
   1→0. Encode that in pkg/breezy/ops.go::Power so the daemon cache stays
   coherent for ALL callers (UI handler, /v1 JSON, HomeKit, scheduler) —
   not just the /power UI handler, which used to do its own explicit
   SetTimer(off) and is now simplified.

2. **AST-walking coverage test.** TestCascadeTable_AllWritableSignalsCovered
   used to enumerate signal names by hand. Now it parses the .go files in
   the templates package, walks every clickAction(...) call, extracts the
   2nd argument when literal, and asserts each signal appears in cascades.
   Verified by mutation: changing "heater" to "unregisteredSig" in the
   .templ produces a precise file:line error.

3. **Manual slider phantom-POST race.** Under heavy load, the
   "manual slider drag" Playwright test flaked 40-60%. Stack-trace
   probing (2026-05-12) showed datastar's data-bind plugin fires the
   change handler synchronously during its signal→input syncs — both
   initial and on every SSE-pushed signal update. The synthetic events
   had isTrusted=false and the input's current value, producing no-op
   {manual:50} POSTs that pre-empted the test's deliberate drag-to-75
   assertion. Replaced data-bind with: data-effect (signal → el.value,
   no event dispatch), data-on:input (input → signal, live drag mirror),
   data-on:change without debounce (POST on release). Removing the 200ms
   debounce also closes a second race where SSE pushes could reset
   el.value via data-effect between event dispatch and debounced
   handler-fire, causing the handler to read the server value instead
   of the dragged value. 15 full-suite runs under heavy load: all green.

4. **Spec updated** with the settled Test 4 result and reorganized
   Layer B server-side mirrors table around ops-layer placement.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
hughobrien added a commit that referenced this pull request Jun 29, 2026
* docs: optimistic UI cascades design

Spec for replacing ad-hoc speculative-update fixes with a single
clickAction helper + cascade table. Includes the office-device probe
results (2026-05-11) that established which cross-signal effects are
firmware-driven vs. handler-driven.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: optimistic UI cascades plan + spec refinements

Plan implements the design in 8 sequential tasks (Task 0 foundation, 1
red Playwright test, 2-6 migrations, 7 effPower + Layer A wrap-up).
Refines the spec to use a __next intermediate in clickAction so toggle
expressions evaluate once — the cascade reads the just-mutated signal
directly, and the POST payload references __next so wire value matches
locally-written value.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(ui): cascade table + clickAction helper

Foundation for centralized optimistic UI updates. No callers yet —
that lands per-handler in subsequent commits. Spec:
docs/superpowers/specs/2026-05-11-optimistic-ui-cascades-design.md

* test(ui): pin optimistic timer-chip de-light on preset click

Currently failing (night chip stays pressed for the SSE roundtrip).
Task 2 of the cascades plan makes this pass.

* feat(ui): migrate preset chip to clickAction cascade

Preset clicks now optimistically clear $specialMode via the speedMode
cascade. Closes the SSE-roundtrip window where the night/turbo chip
stayed visually pressed after a preset selection.

Also updates the preset-editor open/close UX: the editor toggles only
when re-clicking the already-active preset. Clicking a non-active
preset now selects it (delegated to clickAction) without expanding the
editor — the wasActive flag in presetClickExpr captures pre-click
signal state so the editor decision sees the device's true prior
preset, not the just-optimistically-written one. The four existing
Playwright tests that previously opened the editor via a non-active
preset click are updated to seed preset2 as the active speed and
re-click 48/49.

NOTE: the Task 1 regression test (preset chip click optimistically
de-lights active timer chip) still fails on the MemClient backend used
by Playwright. The cascade fires client-side as designed, but the
daemon's notifyAfterWrite immediately ships a fresh card whose cached
$specialMode still reads 'night' because MemClient doesn't simulate
firmware's auto-clear-on-speed-write behavior. The SSE push lands
within ms and overwrites the optimistic cascade. On real hardware the
same race exists for a poll-tick window. Follow-up task needed:
either daemon-side handler cascade (mirror firmware in /speed by
explicitly writing timer=0 too) or MemClient firmware-invariant
simulation. Out of scope for Task 2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(ops): speed-mode writes also clear timer (server-side mirror)

The firmware clears 0x0007 on any 0x0002 write — encode that
invariant at the ops layer so the daemon's cache stays coherent
without waiting for the next poll, and the MemClient backend
(Playwright tests) sees the same effect. Closes the gap where
the post-write SSE push overwrote the client-side cascade with
stale specialMode state.

Completes Task 2 of docs/superpowers/plans/2026-05-11-optimistic-ui-cascades.md.

* feat(ui): migrate manual + mode buttons to clickAction

* feat(ui): migrate timer button to clickAction

* feat(ui): migrate heater button to clickAction

* feat(ui): migrate power button to clickAction

* feat(ui): add effPower derivation for desynced power flag

External actors (panel button, IR remote) can leave the device in
timer-active + power-off state. Dashboard's power button now reads
effPower($power, $specialMode) so it shows pressed regardless.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
hughobrien added a commit that referenced this pull request Jun 29, 2026
…slider race) (#241)

Four targeted follow-ups to the optimistic-UI cascades PR (#240):

1. **Power-off → timer-clear at the ops layer.** Probed the office device
   2026-05-12 (with night-timer to avoid the inconclusive turbo-self-clear
   from the original Test 4): firmware DOES clear 0x0007 when 0x0001 goes
   1→0. Encode that in pkg/breezy/ops.go::Power so the daemon cache stays
   coherent for ALL callers (UI handler, /v1 JSON, HomeKit, scheduler) —
   not just the /power UI handler, which used to do its own explicit
   SetTimer(off) and is now simplified.

2. **AST-walking coverage test.** TestCascadeTable_AllWritableSignalsCovered
   used to enumerate signal names by hand. Now it parses the .go files in
   the templates package, walks every clickAction(...) call, extracts the
   2nd argument when literal, and asserts each signal appears in cascades.
   Verified by mutation: changing "heater" to "unregisteredSig" in the
   .templ produces a precise file:line error.

3. **Manual slider phantom-POST race.** Under heavy load, the
   "manual slider drag" Playwright test flaked 40-60%. Stack-trace
   probing (2026-05-12) showed datastar's data-bind plugin fires the
   change handler synchronously during its signal→input syncs — both
   initial and on every SSE-pushed signal update. The synthetic events
   had isTrusted=false and the input's current value, producing no-op
   {manual:50} POSTs that pre-empted the test's deliberate drag-to-75
   assertion. Replaced data-bind with: data-effect (signal → el.value,
   no event dispatch), data-on:input (input → signal, live drag mirror),
   data-on:change without debounce (POST on release). Removing the 200ms
   debounce also closes a second race where SSE pushes could reset
   el.value via data-effect between event dispatch and debounced
   handler-fire, causing the handler to read the server value instead
   of the dragged value. 15 full-suite runs under heavy load: all green.

4. **Spec updated** with the settled Test 4 result and reorganized
   Layer B server-side mirrors table around ops-layer placement.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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