Stop the late-night dim compounding across nights (#110) #111

Merged
claude-bot merged 1 commit from fix/dim-compounding into main 2026-09-04 16:04:41 +00:00
Contributor

Fixes #110. Observed on the dev deployment: brightness decayed 128 → 51 → 20 → 8 over three consecutive no-event nights.

Cause

run_dim_job dimmed relative to the live controller value and never checked whether the lights were on. On a no-event evening the on job pushes {"on": false}, which leaves bri untouched — so nothing reset the baseline and each night's dim multiplied the previous night's result.

Two changes

Skip when the controller is off. There is nothing lit to dim, and the job now logs skipped rather than reporting success for work with no effect. This alone stops the observed decay.

Dim from the scheme, not the live value. dim_state now takes the baseline as an argument instead of reading it out of controller state, which forces the caller to say what it is dimming from:

def dim_state(brightness_pct: int, baseline_bri: int) -> dict[str, Any]:

The baseline is the active scheme's global_brightness, resolved exactly the way the on job resolves it so the two cannot disagree about which scheme is live. That makes the operation idempotent.

The spec already had the answer

§5.2: "applies globally and overrides per-scheme brightness values proportionally." So proportional was right all along — the baseline was the part that was wrong.

Why the ambiguity survived

LATE_NIGHT_BRIGHTNESS was being read two ways in the same codebase: dim_white_state treats it as a percentage of 255, while dim_state treated it as a percentage of current. Both are defensible readings of "brightness 40", which is how they coexisted unnoticed. It is now documented as a percentage of the scheme's brightness in .env.example and the spec.

A consequence worth noting: nights with no approved scheme fall back to a 255 baseline, so the dim_white ambient default now lands on the level it was already showing rather than being dimmed a second time — it previously came on at 40% and was then dimmed to 16% within the same night.

Verification

  • Backend: 294 passed (3 new), ruff, ruff format, mypy clean; all seven pre-commit hooks pass.
  • New tests cover the two halves separately: dim_state applied twice gives the same answer, and run_dim_job against an off controller pushes nothing and logs skipped.
  • The five existing dim_state tests moved to the new signature.

🤖 Generated with Claude Code

Fixes #110. Observed on the dev deployment: brightness decayed **128 → 51 → 20 → 8** over three consecutive no-event nights. ## Cause `run_dim_job` dimmed relative to the *live* controller value and never checked whether the lights were on. On a no-event evening the on job pushes `{"on": false}`, which leaves `bri` untouched — so nothing reset the baseline and each night's dim multiplied the previous night's result. ## Two changes **Skip when the controller is off.** There is nothing lit to dim, and the job now logs `skipped` rather than reporting `success` for work with no effect. This alone stops the observed decay. **Dim from the scheme, not the live value.** `dim_state` now takes the baseline as an argument instead of reading it out of controller state, which forces the caller to say what it is dimming *from*: ```python def dim_state(brightness_pct: int, baseline_bri: int) -> dict[str, Any]: ``` The baseline is the active scheme's `global_brightness`, resolved exactly the way the on job resolves it so the two cannot disagree about which scheme is live. That makes the operation idempotent. ## The spec already had the answer §5.2: *"applies globally and overrides per-scheme brightness values **proportionally**."* So proportional was right all along — the **baseline** was the part that was wrong. ## Why the ambiguity survived `LATE_NIGHT_BRIGHTNESS` was being read two ways in the same codebase: `dim_white_state` treats it as a percentage of **255**, while `dim_state` treated it as a percentage of **current**. Both are defensible readings of "brightness 40", which is how they coexisted unnoticed. It is now documented as a percentage of the scheme's brightness in `.env.example` and the spec. A consequence worth noting: nights with no approved scheme fall back to a 255 baseline, so the `dim_white` ambient default now lands on the level it was *already showing* rather than being dimmed a second time — it previously came on at 40% and was then dimmed to 16% within the same night. ## Verification - Backend: **294 passed** (3 new), `ruff`, `ruff format`, `mypy` clean; all seven pre-commit hooks pass. - New tests cover the two halves separately: `dim_state` applied twice gives the same answer, and `run_dim_job` against an off controller pushes nothing and logs `skipped`. - The five existing `dim_state` tests moved to the new signature. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Stop the late-night dim compounding across nights (#110)
All checks were successful
CI / Alembic migration check (pull_request) Successful in 1m15s
CI / Pre-commit hooks (pull_request) Successful in 1m31s
CI / Frontend lint, test & build (pull_request) Successful in 1m38s
CI / Python lint & type-check (pull_request) Successful in 1m51s
CI / Python tests (pull_request) Successful in 2m43s
CI / Docker build, health smoke & E2E (pull_request) Successful in 2m50s
2678860486
Observed on the dev deployment: brightness decayed 128 -> 51 -> 20 -> 8 over
three consecutive no-event nights.

`run_dim_job` dimmed relative to the live controller value and never checked
whether the lights were on. On a no-event evening the on job pushes
`{"on": false}`, which leaves `bri` untouched, so nothing reset the baseline
and each night's dim multiplied the previous night's result.

Two changes:

- Skip when the controller is off. There is nothing lit to dim, and the job
  now logs `skipped` rather than reporting success for work that had no
  effect.
- Dim relative to the active scheme's `global_brightness` instead of the live
  value, so the operation is idempotent. `dim_state` takes the baseline as an
  argument rather than reading it from controller state, which forces the
  caller to be explicit about what is being dimmed from.

Spec §5.2 already specified the reduction as proportional -- "overrides
per-scheme brightness values proportionally" -- so proportional was right and
the baseline was what was wrong.

`LATE_NIGHT_BRIGHTNESS` now says in `.env.example` and the spec that it is a
percentage of the scheme's brightness, not of full scale. The setting was
being read both ways: `dim_white_state` treats it as a percentage of 255
while `dim_state` treated it as a percentage of current, which is how the two
interpretations coexisted unnoticed.

Nights with no approved scheme fall back to 255 as the baseline, so the
`dim_white` ambient default lands on the same level it was already showing
instead of being dimmed a second time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-bot deleted branch fix/dim-compounding 2026-09-04 16:04:42 +00:00
Sign in to join this conversation.
No description provided.