Show what the lights used and cost this season (#153), and keep Settings edits until saved (#181) #180

Merged
claude-bot merged 3 commits from feat/153-energy-report into main 2026-09-26 15:41:10 +00:00
Collaborator

Closes #153 (except the power-meter calibration check, which is in #179). Closes #181.

This PR grew two fixes while its CI failures were being chased down. Both are real bugs that already existed on main, and both were found by root cause, not by raising timeouts.

1. Energy and cost report (#153), commit dd4722f

  • Sampling reuses the watchdog's read (#148), so there's no second poll; a test asserts one get_info and one get_state per tick. With the watchdog off, an energy_sampler job takes its place at the same interval. Sampling can never break the watchdog.
  • Only lit readings count. The first off reading after a lit one closes the night at 0 mA. A lit reading with no estimate is skipped.
  • Integration: trapezoid, per controller and night, with gaps over 3× the interval not integrated across. Each night is attributed through the scheduler's cycle date, and its event and scheme come from plan_for_date.
  • Storage: power_samples (30 days) and energy_daily (kept forever, in mAh, so nights recorded before the voltage is set become usable once it is). Migration 0012, with a single head.
  • The report refuses to invent a voltage. kWh and cost stay null until led_voltage is set. Everything is labelled as an estimate.
  • Also: /api/v1/energy/summary, /export.csv, an /energy page (a header link and the fourth app shortcut), an Energy settings card, and the iris_wled_current_milliamps and iris_energy_wh_total metrics (tz-aware, unlike #176).

2. The calendar's scheme list stalled every other request, commit 96bd1f2

list_schemes re-read and re-validated the settings for each of about 108 schemes (an N+1). py-spy during a parallel E2E run put it at 77.8% of server CPU, and a settings PUT that takes 0.03 s when idle took 5–22 s. Settings are now read once per request, with a regression test that counts the reads. This is also the probable cause of the #177 flake.

3. Settings edits were silently lost (#181), commit 6574b4c

The Settings page renders its cards before GET /settings returns. When the response landed, a useEffect copied every server value over the form, including fields the person had just edited.

Instrumented under load:

4017 TEST edited; clicking save
4169 REQ PUT settings
10605 RESP GET settings
10611 settings-effect srv=reboot_only/5 form=enforce/10   <- the late load overwrites the edit

So Save sent the old values, which is exactly CI run 11017's diff.

  • No remount, no service-worker involvement, no stray settings keys. It reproduces with the PWA project excluded, and in a vitest with no service worker.

Fix:

  • New useDrafts: an edited field is never overwritten by a load, and Save sends only the edited settings. One card's Save no longer rewrites the other cards with stale values, and it no longer pins every key into the database, which is the #181 ask.
  • The same guard for the segment list and webhook drafts.
  • Fixed on the way: const { data: webhooks = [] } created a new array on every render, so the drafts effect re-rendered forever for a viewer. It's now a module constant.
  • Tests: a new settingsDrafts.test.tsx (6 tests), shown to fail on the old page. The watchdog E2E now asserts the exact PUT body and that the form keeps its values after the save's refetch.

Verification

  • Backend: pytest 1284 passed; ruff, strict mypy and uv lock --check are clean.
  • Frontend: lint, tsc, vitest 292 passed, and build.
  • Image import check: ok.
  • E2E: 16 post-fix runs under a CPU limit showed zero settings reverts. The remaining failures in those runs were 30-second timeouts on the PWA and happy-path specs while another project's E2E stack was using about 200% of the host's CPU.

Still open

  • The one-off review-nudges.spec.ts:93 failure in run 11017 ("Unapproved" pressed) couldn't be reproduced in 13 instrumented runs. It's probably the CPU-starved 5-second default timeout. It's tracked on #177, and the next recurrence's trace would settle it.
  • #182: the next hot spots under load.
  • #179: the power-meter calibration.

🤖 Generated with Claude Code

Closes #153 (except the power-meter calibration check, which is in #179). Closes #181. This PR grew two fixes while its CI failures were being chased down. Both are real bugs that already existed on `main`, and both were found by root cause, not by raising timeouts. ## 1. Energy and cost report (#153), commit `dd4722f` - **Sampling reuses the watchdog's read** (#148), so there's no second poll; a test asserts one `get_info` and one `get_state` per tick. With the watchdog `off`, an `energy_sampler` job takes its place at the same interval. Sampling can never break the watchdog. - **Only lit readings count.** The first off reading after a lit one closes the night at 0 mA. A lit reading with no estimate is skipped. - **Integration:** trapezoid, per controller and night, with gaps over 3× the interval not integrated across. Each night is attributed through the scheduler's cycle date, and its event and scheme come from `plan_for_date`. - **Storage:** `power_samples` (30 days) and `energy_daily` (kept forever, in **mAh**, so nights recorded before the voltage is set become usable once it is). Migration `0012`, with a single head. - **The report refuses to invent a voltage.** kWh and cost stay null until `led_voltage` is set. Everything is labelled as an estimate. - **Also:** `/api/v1/energy/summary`, `/export.csv`, an `/energy` page (a header link and the fourth app shortcut), an Energy settings card, and the `iris_wled_current_milliamps` and `iris_energy_wh_total` metrics (tz-aware, unlike #176). ## 2. The calendar's scheme list stalled every other request, commit `96bd1f2` `list_schemes` re-read and re-validated the settings for **each of about 108 schemes** (an N+1). py-spy during a parallel E2E run put it at **77.8% of server CPU**, and a settings PUT that takes 0.03 s when idle took 5–22 s. Settings are now read once per request, with a regression test that counts the reads. This is also the probable cause of the #177 flake. ## 3. Settings edits were silently lost (#181), commit `6574b4c` The Settings page renders its cards before `GET /settings` returns. When the response landed, a `useEffect` copied every server value over the form, including fields the person had just edited. Instrumented under load: ``` 4017 TEST edited; clicking save 4169 REQ PUT settings 10605 RESP GET settings 10611 settings-effect srv=reboot_only/5 form=enforce/10 <- the late load overwrites the edit ``` So Save sent the old values, which is exactly CI run 11017's diff. - **No remount, no service-worker involvement, no stray settings keys.** It reproduces with the PWA project excluded, and in a vitest with no service worker. **Fix:** - **New `useDrafts`:** an edited field is never overwritten by a load, and **Save sends only the edited settings.** One card's Save no longer rewrites the other cards with stale values, and it no longer pins every key into the database, which is the #181 ask. - **The same guard for the segment list and webhook drafts.** - **Fixed on the way:** `const { data: webhooks = [] }` created a new array on every render, so the drafts effect **re-rendered forever for a viewer**. It's now a module constant. - **Tests:** a new `settingsDrafts.test.tsx` (6 tests), shown to fail on the old page. The watchdog E2E now asserts the exact PUT body and that the form keeps its values after the save's refetch. ## Verification - **Backend:** pytest 1284 passed; ruff, strict mypy and `uv lock --check` are clean. - **Frontend:** lint, `tsc`, vitest 292 passed, and build. - **Image import check:** ok. - **E2E:** 16 post-fix runs under a CPU limit showed **zero settings reverts**. The remaining failures in those runs were 30-second timeouts on the PWA and happy-path specs while another project's E2E stack was using about 200% of the host's CPU. ## Still open - **The one-off `review-nudges.spec.ts:93` failure in run 11017** ("Unapproved" pressed) couldn't be reproduced in 13 instrumented runs. It's probably the CPU-starved 5-second default timeout. It's tracked on #177, and the next recurrence's trace would settle it. - **#182:** the next hot spots under load. - **#179:** the power-meter calibration. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Show what the lights used and cost this season (#153)
Some checks failed
CI / Pre-commit hooks (pull_request) Successful in 51s
CI / Dockerfile lint (pull_request) Successful in 10s
CI / Alembic migration check (pull_request) Successful in 37s
CI / Python lint & type-check (pull_request) Successful in 1m10s
CI / Frontend lint, test & build (pull_request) Successful in 2m18s
CI / Python tests (pull_request) Successful in 6m25s
CI / Docker build, health smoke & E2E (pull_request) Failing after 4m46s
dd4722f523
A new Energy page reports each season's estimated kWh and cost, a bar per
night, cost per event and the most expensive scheme, with a CSV export.
Every figure is labelled as an estimate from WLED's power limiter.

The watchdog's existing /json/info read supplies leds.pwr while the lights
are on, so there is no second poll. With watchdog_policy off, a sampler job
takes over at the same interval. Readings are integrated per night by the
trapezoid rule, and a gap over three intervals is not integrated across.
Nights after midnight belong to the evening's cycle date, event and scheme.

Nights are stored as milliamp-hours and converted with led_voltage and
psu_efficiency when read. The report refuses to invent a voltage, so the
page asks for one until it is set. Raw samples are pruned after 30 days;
nightly rollups are kept for good. Adds iris_wled_current_milliamps and
iris_energy_wh_total, and a fourth PWA shortcut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stop the calendar's scheme list from stalling every other request (#153)
Some checks failed
CI / Alembic migration check (pull_request) Successful in 41s
CI / Dockerfile lint (pull_request) Successful in 8s
CI / Python lint & type-check (pull_request) Successful in 1m13s
CI / Pre-commit hooks (pull_request) Successful in 2m1s
CI / Frontend lint, test & build (pull_request) Successful in 1m41s
CI / Python tests (pull_request) Successful in 6m33s
CI / Docker build, health smoke & E2E (pull_request) Failing after 4m57s
96bd1f2c7e
GET /schemes?year= read and validated the settings table once per scheme,
over a hundred times per calendar or Settings page load. In a parallel E2E
run that was three-quarters of the server's CPU, and a Settings save waited
5 to 20 seconds behind it. That is why the watchdog spec's 5-second poll
failed in CI. The list now reads the settings once, and bulk approve does
the same.

The watchdog spec now waits for the save's own response and checks what was
sent, instead of polling on a timer. It opens /settings directly, and the
energy spec reaches its header link from a light page, so neither loads the
calendar just to navigate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep Settings edits until they are saved (#181)
All checks were successful
CI / Pre-commit hooks (pull_request) Successful in 55s
CI / Python lint & type-check (pull_request) Successful in 1m11s
CI / Alembic migration check (pull_request) Successful in 43s
CI / Dockerfile lint (pull_request) Successful in 9s
CI / Frontend lint, test & build (pull_request) Successful in 2m33s
CI / Python tests (pull_request) Successful in 7m57s
CI / Docker build, health smoke & E2E (pull_request) Successful in 7m38s
6574b4cb04
The Settings page shows its cards before the saved settings arrive, and it
copied those settings over the form whenever they did. On a slow load, a
watchdog policy picked from the dropdown flipped back before Save, and Save
sent the old one. That is what failed CI's watchdog spec. A refetch after
another tab saved did the same.

Each field now tracks whether it has been edited. Loads skip edited fields
until they are saved, and Save sends only what was edited, so a Save made
before the page loaded can't overwrite settings with placeholders. The
segment list and webhook drafts keep unsaved edits the same way.

This also fixes a render loop. The webhooks query's default was a fresh []
on every render, so its effect re-ran and set state endlessly. That lasted
until the list loaded for an admin, and forever for a viewer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot changed title from Show what the lights used and cost this season (#153) to Show what the lights used and cost this season (#153), and keep Settings edits until saved (#181) 2026-09-26 15:14:48 +00:00
claude-bot deleted branch feat/153-energy-report 2026-09-26 15:41:12 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Iris-WLED!180
No description provided.