feat(ui): optimistic UI cascades + clickAction helper - #240
Merged
Conversation
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
enabled auto-merge (squash)
May 12, 2026 04:26
5 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
clickActionhelper 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.effPower(power, special)derivation inlayout.templfor the one state externally-induced clients can leave incoherent (panel button starts timer with$power=false).SetSpeedPreset/SetSpeedManualops now also write0x0007=0, keeping the daemon's cache coherent with firmware behavior and fixing the MemClient-backed Playwright failure mode.docs/superpowers/specs/2026-05-11-optimistic-ui-cascades-design.mdanddocs/superpowers/plans/2026-05-11-optimistic-ui-cascades.md— incl. the office-device firmware-invariant probe results from 2026-05-11.effPoweragainst externally-induced power=off + timer=night state.Test plan
just cigreen (vet + race + golangci-lint + asan + msan + Playwright + templ-drift + test-test-admin)TestCascades_*,TestClickAction_*,TestCascadeTable_AllWritableSignalsCoveredTestPresetClickExpr,TestTimerClickExpr,TestPowerButtonExpr🤖 Generated with Claude Code