Frontend: let users stop a running preview carousel (#48) #88

Merged
claude-bot merged 1 commit from feat/carousel-stop-and-regenerate-confirm into main 2026-08-31 17:20:55 +00:00
Contributor

First item of batch C (#48). Closes the gap the 2026-07-15 review recorded as "no stop-carousel control (API exists, unused)".

The problem

stopCarousel existed in api/index.ts:141 with no hook and no caller. SidePanel imported useStartCarousel only. A user could start a carousel driving the real controller and had no way to end it short of waiting it out.

Why it isn't a pure frontend change

A client-side "running" flag would be lost on reload — reproducing the exact bug. The carousel lives in preview_manager's module state (_carousel_task), which nothing exposed. So the state is surfaced instead:

  • preview_manager.carousel_active() reports whether the cycle task is live
  • /wled/status carries it — on both the configured and unconfigured paths, so the field is never absent. An undefined value would read as "not running" and silently hide the control, which is the failure mode we're fixing.
  • the frontend already polls /wled/status for the connection indicator, so this adds no new polling. Both carousel mutations invalidate that query, so the control flips immediately rather than after the next 30s poll.

Two surfaces, deliberately

  • SidePanel — the Carousel button becomes a toggle, right where it's started.
  • Header — a global stop control whenever one runs. A carousel outlives the day panel that started it, so if the only stop lived in the panel you'd have to find and reopen the right day. This makes it reachable from any page and after a reload.

Verification

Run locally, not deferred to CI:

ruff check .            All checks passed!
ruff format --check .   91 files already formatted
mypy app/               Success: no issues found in 61 source files
pytest -q               253 passed

Three new tests cover carousel_active() before start, while running, and after stop — there were no carousel tests at all previously.

What I could not verify: there's no node toolchain on this machine, so tsc, eslint and vitest are left to CI. The frontend changes are small and mechanical, but I'd rather say so than imply I checked them.

Note on scope

While starting this I found item 6 of my batch-C audit was wrong — the regenerate confirmation dialog already exists (SidePanel.tsx:307, with an explicit "approval is discarded … cannot be undone" warning). Corrected on #48; batch C is seven items, not eight. The branch name mentions it because it was created before I checked.

First item of batch C (#48). Closes the gap the 2026-07-15 review recorded as *"no stop-carousel control (API exists, unused)"*. ## The problem `stopCarousel` existed in `api/index.ts:141` with **no hook and no caller**. `SidePanel` imported `useStartCarousel` only. A user could start a carousel driving the real controller and had no way to end it short of waiting it out. ## Why it isn't a pure frontend change A client-side "running" flag would be lost on reload — **reproducing the exact bug**. The carousel lives in `preview_manager`'s module state (`_carousel_task`), which nothing exposed. So the state is surfaced instead: - `preview_manager.carousel_active()` reports whether the cycle task is live - `/wled/status` carries it — **on both the configured and unconfigured paths**, so the field is never absent. An undefined value would read as "not running" and silently hide the control, which is the failure mode we're fixing. - the frontend **already polls `/wled/status`** for the connection indicator, so this adds no new polling. Both carousel mutations invalidate that query, so the control flips immediately rather than after the next 30s poll. ## Two surfaces, deliberately - **`SidePanel`** — the Carousel button becomes a toggle, right where it's started. - **`Header`** — a global stop control whenever one runs. A carousel outlives the day panel that started it, so if the only stop lived in the panel you'd have to find and reopen the right day. This makes it reachable from any page and after a reload. ## Verification Run locally, not deferred to CI: ``` ruff check . All checks passed! ruff format --check . 91 files already formatted mypy app/ Success: no issues found in 61 source files pytest -q 253 passed ``` Three new tests cover `carousel_active()` before start, while running, and after stop — there were **no carousel tests at all** previously. **What I could not verify:** there's no node toolchain on this machine, so `tsc`, `eslint` and `vitest` are left to CI. The frontend changes are small and mechanical, but I'd rather say so than imply I checked them. ## Note on scope While starting this I found **item 6 of my batch-C audit was wrong** — the regenerate confirmation dialog already exists (`SidePanel.tsx:307`, with an explicit "approval is discarded … cannot be undone" warning). Corrected on #48; batch C is seven items, not eight. The branch name mentions it because it was created before I checked.
Frontend: let users stop a running preview carousel (#48)
All checks were successful
CI / Alembic migration check (pull_request) Successful in 1m14s
CI / Python lint & type-check (pull_request) Successful in 1m45s
CI / Frontend lint, test & build (pull_request) Successful in 1m59s
CI / Python tests (pull_request) Successful in 2m43s
CI / Docker build, health smoke & E2E (pull_request) Successful in 1m47s
b4ff7ddfaf
stopCarousel existed in api/index.ts with no hook and no caller. A user could
start a carousel driving the real controller and had no way to end it short of
waiting it out -- spec 10.1 lists the carousel control, and the 2026-07-15
review flagged the missing stop as "API exists, unused".

The fix could not be purely client-side. The carousel lives in
preview_manager's module state, so a local "running" flag would be lost on
reload -- reproducing the exact bug. Instead the state is exposed:

  - preview_manager.carousel_active() reports whether the cycle task is live
  - /wled/status carries it, on both the configured and unconfigured paths so
    the field is never absent (an undefined value would read as "not running"
    and hide the control)
  - the frontend already polls /wled/status for the connection indicator, so
    this needs no new polling; both carousel mutations invalidate that query so
    the control flips immediately rather than after the next 30s poll

Two surfaces, deliberately:

  - SidePanel's Carousel button becomes a toggle, next to where it is started
  - Header shows a global stop control whenever one runs, because a carousel
    outlives the day panel that started it and must be reachable from any page
    and after a reload

Verified locally: ruff, ruff format, mypy (61 files) and the full 253-test
backend suite all pass, including three new tests covering carousel_active
before start, while running, and after stop. The frontend could not be
typechecked locally -- no node toolchain on this machine -- so tsc, eslint and
vitest are left to CI.

Refs #48.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-bot deleted branch feat/carousel-stop-and-regenerate-confirm 2026-08-31 17:20:55 +00:00
Sign in to join this conversation.
No description provided.