Final QA: end-to-end operational dry run #7

Closed
opened 2026-07-07 16:51:37 +00:00 by claude-bot · 6 comments
Contributor

Goal

Exercise the full operational loop end-to-end against a real (or mock) WLED device before tagging v1.0.0.

Flow to validate

  • Configure → review → approve a scheme
  • Push today's scheme to the controller
  • Scheduler turns lights on at sunset, dims at late-night, off at sunrise
  • Failure paths surface (connection loss → fallback policy)

Acceptance criteria

  • The configure→approve→push→schedule→sunrise-off loop runs unattended
  • Fallback behavior observed and correct

Part of the M10 final QA pass.

## Goal Exercise the full operational loop end-to-end against a real (or mock) WLED device before tagging v1.0.0. ## Flow to validate - [ ] Configure → review → approve a scheme - [ ] Push today's scheme to the controller - [ ] Scheduler turns lights on at sunset, dims at late-night, off at sunrise - [ ] Failure paths surface (connection loss → fallback policy) ## Acceptance criteria - [ ] The configure→approve→push→schedule→sunrise-off loop runs unattended - [ ] Fallback behavior observed and correct Part of the M10 final QA pass.
claude-bot added this to the v1.0.0 milestone 2026-07-15 15:33:03 +00:00
Author
Contributor

Dev host is deployed and ready — but needs configuring before this dry run can start

Carried over from #65, which delivered the deploy mechanism but stopped at configuration.

Current state of claude@10.1.1.14

Container iris   git.rhoving.com/rbrooks/iris-wled:main   Up (healthy)
{"status":"ok"}

Running the CI-published image (sha256:f4d5c6cb…), checkout current with main, database migrated forward from May (0006 → 0008, no errors) with 97 events and 169 schemes intact. Pre-redeploy backup at data/iris.db.pre-redeploy-20260831-053637, md5-verified identical.

Deploy/redeploy from here is:

cd ~/projects/Iris-WLED
export COMPOSE_FILE=docker-compose.yml:docker-compose.registry.yml
git pull && docker compose pull && docker compose up -d

What must be set before the dry run is meaningful

The instance is up but inert — no scheduler jobs are registered and no controller is configured:

LATITUDE  = EMPTY
LONGITUDE = EMPTY
WLED_HOST = EMPTY

WARNING  Cannot schedule today's jobs:
         LATITUDE and LONGITUDE must be set for SCHEDULE_MODE=solar

Row counts confirm it was never configured past event/scheme generation:

table rows
events 97
schemes 169
segments 0
settings 0
audit_log 0

So before this issue's steps can run:

  • LATITUDE / LONGITUDE in .env (required for SCHEDULE_MODE=solar; without them no on/dim/off jobs are ever scheduled)
  • WLED_HOST pointing at the controller
  • Confirm TZ is correct (currently set)
  • Configure segments in the UI — 0 rows today, and the whole push path depends on them

SESSION_SECRET was generated during the #65 deploy, so that no longer blocks startup.

These are values only you can supply — I would be guessing at coordinates and a controller address. Once they are in, the deploy command above picks them up on restart and this dry run can proceed against real hardware.

Note for whoever runs it

This is also the first time the priority resolver, default-scheme, fallback-policy and quick-push-revert fixes (#43/#44/#45/#47) will be exercised against a real controller — they were unit-tested but never run end to end on hardware. Worth watching those paths specifically rather than only the happy path.

## Dev host is deployed and ready — but needs configuring before this dry run can start Carried over from #65, which delivered the deploy mechanism but stopped at configuration. ### Current state of `claude@10.1.1.14` ``` Container iris git.rhoving.com/rbrooks/iris-wled:main Up (healthy) {"status":"ok"} ``` Running the CI-published image (`sha256:f4d5c6cb…`), checkout current with `main`, database migrated forward from May (`0006 → 0008`, no errors) with **97 events and 169 schemes** intact. Pre-redeploy backup at `data/iris.db.pre-redeploy-20260831-053637`, `md5`-verified identical. Deploy/redeploy from here is: ```sh cd ~/projects/Iris-WLED export COMPOSE_FILE=docker-compose.yml:docker-compose.registry.yml git pull && docker compose pull && docker compose up -d ``` ### What must be set before the dry run is meaningful The instance is up but **inert** — no scheduler jobs are registered and no controller is configured: ``` LATITUDE = EMPTY LONGITUDE = EMPTY WLED_HOST = EMPTY WARNING Cannot schedule today's jobs: LATITUDE and LONGITUDE must be set for SCHEDULE_MODE=solar ``` Row counts confirm it was never configured past event/scheme generation: | table | rows | |---|---| | events | 97 | | schemes | 169 | | **segments** | **0** | | **settings** | **0** | | **audit_log** | **0** | So before this issue's steps can run: - [ ] `LATITUDE` / `LONGITUDE` in `.env` (required for `SCHEDULE_MODE=solar`; without them no on/dim/off jobs are ever scheduled) - [ ] `WLED_HOST` pointing at the controller - [ ] Confirm `TZ` is correct (currently set) - [ ] Configure segments in the UI — 0 rows today, and the whole push path depends on them `SESSION_SECRET` was generated during the #65 deploy, so that no longer blocks startup. These are values only you can supply — I would be guessing at coordinates and a controller address. Once they are in, the deploy command above picks them up on restart and this dry run can proceed against real hardware. ### Note for whoever runs it This is also the first time the **priority resolver, default-scheme, fallback-policy and quick-push-revert** fixes (#43/#44/#45/#47) will be exercised against a real controller — they were unit-tested but never run end to end on hardware. Worth watching those paths specifically rather than only the happy path.
Author
Contributor

Dev deployment is now configured against real hardware.

Values were supplied privately and written to the dev host's .env (gitignored); they are deliberately not repeated here, since this repo is public and the coordinates are a home location. A timestamped .env backup was taken first.

ControllerGET /api/v1/wled/status:

{"configured": true, "reachable": true, "firmware_version": "16.0.0", "segment_count": 1, "name": "WLED"}

The device ("Niji", ESP32, WLED 16.0.0) is a 36-LED RGBW strip presenting a single segment 0–36. It's a bench unit rather than a full install, which is fine for exercising the loop.

Segment config — there were none, so schemes had nothing to target. Created one themed segment stripwled_index 0. The name is arbitrary (the generator uses whatever the configured segments are called), so rename it when the real layout exists — note that renaming triggers scheme regeneration for the affected segments.

Solar scheduling now resolvesGET /api/v1/schedule/status:

{"on_time": "2026-08-31T19:03:13-05:00", "dim_time": "2026-08-31T23:00:00-05:00",
 "off_time": "2026-09-01T06:31:39-05:00", "mode": "solar"}

That checks out for the configured latitude/longitude: sunset ≈ 19:33 CDT with the -30 offset, sunrise ≈ 06:32 CDT.


One thing worth flagging: the dev host was running a :main image built 2026-08-31T05:29 — roughly 21 hours behind, predating #93. /events/resolved returned {"detail": "Event not found"}, i.e. the route-shadowing bug #93 fixed. That was a stale deploy, not a regression: docs/setup.md §6 already prescribes pull before up -d, and my first recreate simply skipped the pull. After pulling (image now built 01:06 today, post-#97) the endpoint returns resolved days correctly.

Remaining before the acceptance criteria can be ticked:

  • 83 schemes exist for 2026, 0 approved — the configure→review→approve leg hasn't been walked yet.
  • Today (2026-09-01) resolves to no event, so nothing event-driven would push; the no-event default applies.
  • The push, sunset/dim/sunrise and fallback legs all drive physical lights, so they need a deliberate run rather than being done incidentally.
**Dev deployment is now configured against real hardware.** Values were supplied privately and written to the dev host's `.env` (gitignored); they are deliberately not repeated here, since this repo is public and the coordinates are a home location. A timestamped `.env` backup was taken first. **Controller** — `GET /api/v1/wled/status`: ```json {"configured": true, "reachable": true, "firmware_version": "16.0.0", "segment_count": 1, "name": "WLED"} ``` The device ("Niji", ESP32, WLED 16.0.0) is a 36-LED RGBW strip presenting a single segment `0–36`. It's a bench unit rather than a full install, which is fine for exercising the loop. **Segment config** — there were none, so schemes had nothing to target. Created one themed segment `strip` → `wled_index 0`. The name is arbitrary (the generator uses whatever the configured segments are called), so rename it when the real layout exists — note that renaming triggers scheme regeneration for the affected segments. **Solar scheduling now resolves** — `GET /api/v1/schedule/status`: ```json {"on_time": "2026-08-31T19:03:13-05:00", "dim_time": "2026-08-31T23:00:00-05:00", "off_time": "2026-09-01T06:31:39-05:00", "mode": "solar"} ``` That checks out for the configured latitude/longitude: sunset ≈ 19:33 CDT with the `-30` offset, sunrise ≈ 06:32 CDT. --- **One thing worth flagging:** the dev host was running a `:main` image built 2026-08-31T05:29 — roughly 21 hours behind, predating #93. `/events/resolved` returned `{"detail": "Event not found"}`, i.e. the route-shadowing bug #93 fixed. That was a stale deploy, **not** a regression: `docs/setup.md` §6 already prescribes `pull` before `up -d`, and my first recreate simply skipped the pull. After pulling (image now built 01:06 today, post-#97) the endpoint returns resolved days correctly. **Remaining before the acceptance criteria can be ticked:** - 83 schemes exist for 2026, **0 approved** — the configure→review→approve leg hasn't been walked yet. - Today (2026-09-01) resolves to no event, so nothing event-driven would push; the no-event default applies. - The push, sunset/dim/sunrise and fallback legs all drive physical lights, so they need a deliberate run rather than being done incidentally.
Author
Contributor

Correction to my previous comment: I described the controller as "a bench unit rather than a full install". That's wrong — it is the live roofline. I inferred "bench" from the 36-LED count, which was an assumption, not an observation. The dry run below was run against live lights and treated as such: state was snapshotted first and restored at the end.


Dry run results

Leg 1 — configure → review → approve

Approved labor_day_2026_mid. The scheme carries the strip segment created earlier, confirming the generator picks up configured segments.

One observation: source stayed builtin after approval rather than flipping to user. Promotion to user appears to apply to community/AI-sourced schemes only. Flagging in case §3.3's "promoted to user-saved on approval" was meant to cover builtins too — not treating it as a defect here.

Leg 2 — push

Action Result on the controller
POST /schedule/push-today {"pushed": "off", "reason": "no_event"} → lights off. Correct: 2026-09-01 has no event and default_scheme was unset.
POST /wled/quick-push {scheme_id} fx 9, col [[210,30,40],[255,255,255],[25,70,160]] — the approved Labor Day scheme on the roof.
POST /wled/quick-push {color:[0,255,0]} fx 0, pal 0, col [[0,255,0],[0,0,0],[0,0,0]]#97's arbitrary-colour path, verified against real hardware.
DELETE /wled/quick-push Reverted to the pre-push state.

Worth noting: labor_day_2026_mid carries four colours (…,[255,180,40] gold) but WLED takes three slots, and scheme_to_wled_state does colors[:3], so the gold is silently dropped. Correct per the WLED protocol, but the generator can emit schemes whose 4th colour can never render. Possibly worth either capping generation at 3 or surfacing it in the UI.

Leg 3 — scheduler on / dim / off

Driven directly rather than waiting for real sunset, with default_scheme temporarily set to dim_white so the transitions were observable (restored to None afterwards):

Job Controller state
run_on_job on, bri 102, warm white [255,180,107] — 40% per late_night_brightness, matching dim_white's documented behaviour
run_dim_job bri 40 — 40% of 102, correct
run_off_job on: false

Methodology note: my first attempt called these via docker exec … python -c and got WLED controller not configured. That was my harness, not a defect — _controller is a process-global set by the app's lifespan, and a fresh process has none. Re-running with init_controller() first gave the results above.

Leg 4 — failure paths

Against unroutable 192.0.2.1 (TEST-NET-1):

Policy Result
silent success=False attempts=1, logged, no follow-up
last_known_good success=False attempts=1, reconnect watch requested
retry success=False attempts=1, retry chain requested

Then the same failure driven through the live app (so a real APScheduler was present): POST /schedule/push-today returned timed out, and wled_push_retry appeared in the persistent apscheduler_jobs table scheduled for +5 minutes — the spaced-retry leg confirmed end to end, surviving in the jobstore as §5.3 describes. Host restored immediately; reachable: true.


Acceptance criteria

  • Fallback behaviour observed and correct
  • The configure→approve→push→schedule→sunrise-off loop runs unattended — every leg is verified individually, but they were driven manually. This still needs one real unattended overnight cycle before it can be ticked.

Open items

  1. Iris now drives these lights on a schedule. With default_scheme = None (restored to its original value), every no-event evening pushes off at sunset−30. 2026 has 83 schemes but 0 approved besides Labor Day, so most nights currently resolve to "off". That's a real behavioural decision — set default_scheme, or approve schemes for current dates.
  2. The 4th-colour truncation above.
  3. Builtin schemes not being promoted to user on approval.
**Correction to my previous comment:** I described the controller as "a bench unit rather than a full install". That's wrong — it is the live roofline. I inferred "bench" from the 36-LED count, which was an assumption, not an observation. The dry run below was run against live lights and treated as such: state was snapshotted first and restored at the end. --- ## Dry run results ### Leg 1 — configure → review → approve ✅ Approved `labor_day_2026_mid`. The scheme carries the `strip` segment created earlier, confirming the generator picks up configured segments. One observation: `source` stayed `builtin` after approval rather than flipping to `user`. Promotion to `user` appears to apply to community/AI-sourced schemes only. Flagging in case §3.3's "promoted to user-saved on approval" was meant to cover builtins too — not treating it as a defect here. ### Leg 2 — push ✅ | Action | Result on the controller | |---|---| | `POST /schedule/push-today` | `{"pushed": "off", "reason": "no_event"}` → lights off. Correct: 2026-09-01 has no event and `default_scheme` was unset. | | `POST /wled/quick-push {scheme_id}` | `fx 9`, `col [[210,30,40],[255,255,255],[25,70,160]]` — the approved Labor Day scheme on the roof. | | `POST /wled/quick-push {color:[0,255,0]}` | `fx 0`, `pal 0`, `col [[0,255,0],[0,0,0],[0,0,0]]` — #97's arbitrary-colour path, verified against real hardware. | | `DELETE /wled/quick-push` | Reverted to the pre-push state. | **Worth noting:** `labor_day_2026_mid` carries **four** colours (`…,[255,180,40]` gold) but WLED takes three slots, and `scheme_to_wled_state` does `colors[:3]`, so the gold is silently dropped. Correct per the WLED protocol, but the generator can emit schemes whose 4th colour can never render. Possibly worth either capping generation at 3 or surfacing it in the UI. ### Leg 3 — scheduler on / dim / off ✅ Driven directly rather than waiting for real sunset, with `default_scheme` temporarily set to `dim_white` so the transitions were observable (restored to `None` afterwards): | Job | Controller state | |---|---| | `run_on_job` | `on`, `bri 102`, warm white `[255,180,107]` — 40% per `late_night_brightness`, matching `dim_white`'s documented behaviour | | `run_dim_job` | `bri 40` — 40% of 102, correct | | `run_off_job` | `on: false` | *Methodology note:* my first attempt called these via `docker exec … python -c` and got `WLED controller not configured`. That was my harness, not a defect — `_controller` is a process-global set by the app's lifespan, and a fresh process has none. Re-running with `init_controller()` first gave the results above. ### Leg 4 — failure paths ✅ Against unroutable `192.0.2.1` (TEST-NET-1): | Policy | Result | |---|---| | `silent` | `success=False attempts=1`, logged, no follow-up | | `last_known_good` | `success=False attempts=1`, reconnect watch requested | | `retry` | `success=False attempts=1`, retry chain requested | Then the same failure driven **through the live app** (so a real APScheduler was present): `POST /schedule/push-today` returned `timed out`, and `wled_push_retry` appeared in the persistent `apscheduler_jobs` table scheduled for +5 minutes — the spaced-retry leg confirmed end to end, surviving in the jobstore as §5.3 describes. Host restored immediately; `reachable: true`. --- ## Acceptance criteria - [x] Fallback behaviour observed and correct - [ ] **The configure→approve→push→schedule→sunrise-off loop runs unattended** — every leg is verified individually, but they were driven manually. This still needs one real unattended overnight cycle before it can be ticked. ## Open items 1. **Iris now drives these lights on a schedule.** With `default_scheme = None` (restored to its original value), every no-event evening pushes **off** at sunset−30. 2026 has 83 schemes but **0 approved** besides Labor Day, so most nights currently resolve to "off". That's a real behavioural decision — set `default_scheme`, or approve schemes for current dates. 2. The 4th-colour truncation above. 3. Builtin schemes not being promoted to `user` on approval.
Author
Contributor

The unattended overnight cycle ran last night and did not pass. Found a real bug: #106.

What happened on the dev deployment between 2026-08-31 evening and 2026-09-01 morning:

Job Result
on_job (push today's scheme) Late Summer Moon pushed manually before the window; the scheme was live
dim_job 23:00 CDT fired unattended, dimmed 128 → 51
reschedule_daily 00:01 CDT ran
daily_backup 04:00 CDT created iris_daily_20260901T090000Z.db
off_job 06:31 CDT never fired — no log line, no schedule-log row

The lights were off when I checked, which nearly read as success. They had been switched off by a Home Assistant automation the user had forgotten was still enabled. Without that coincidence the roofline would have stayed lit all day, and the schedule log would still have looked healthy — the missing job leaves no row, and every job that did run reported success.

Root cause in #106: reschedule_daily runs at 00:01 local and rewrites the single off_job slot to the next morning's sunrise, cancelling the one pending ~6.5 hours away. off_job is the only one of the three whose correct time falls on a different calendar day, which is why only it collides.

Acceptance criteria

  • Fallback behaviour observed and correct — unchanged from the earlier run
  • The configure→approve→push→schedule→sunrise-off loop runs unattended blocked by #106. Every leg works individually and every leg except sunrise-off works unattended; the off never fires.

This issue should stay open until #106 lands and one real night has been observed end to end with the Home Assistant automation disabled, so nothing external can mask the result again.

Worth recording why this was nearly missed: I had already confirmed run_off_job() works when invoked directly, so the leg looked proven. Driving a job by hand tests the job; it does not test that the scheduler will ever call it.

**The unattended overnight cycle ran last night and did not pass. Found a real bug: #106.** What happened on the dev deployment between 2026-08-31 evening and 2026-09-01 morning: | Job | Result | |---|---| | `on_job` (push today's scheme) | ✅ Late Summer Moon pushed manually before the window; the scheme was live | | `dim_job` 23:00 CDT | ✅ fired unattended, dimmed 128 → 51 | | `reschedule_daily` 00:01 CDT | ✅ ran | | `daily_backup` 04:00 CDT | ✅ created `iris_daily_20260901T090000Z.db` | | **`off_job` 06:31 CDT** | ❌ **never fired** — no log line, no `schedule-log` row | **The lights were off when I checked, which nearly read as success.** They had been switched off by a Home Assistant automation the user had forgotten was still enabled. Without that coincidence the roofline would have stayed lit all day, and the schedule log would still have looked healthy — the missing job leaves no row, and every job that *did* run reported success. Root cause in #106: `reschedule_daily` runs at 00:01 local and rewrites the single `off_job` slot to the *next* morning's sunrise, cancelling the one pending ~6.5 hours away. `off_job` is the only one of the three whose correct time falls on a different calendar day, which is why only it collides. ### Acceptance criteria - [x] **Fallback behaviour observed and correct** — unchanged from the earlier run - [ ] **The configure→approve→push→schedule→sunrise-off loop runs unattended** — ❌ blocked by #106. Every leg works individually and every leg except sunrise-off works unattended; the off never fires. This issue should stay open until #106 lands and one real night has been observed end to end **with the Home Assistant automation disabled**, so nothing external can mask the result again. Worth recording why this was nearly missed: I had already confirmed `run_off_job()` works when invoked directly, so the leg looked proven. Driving a job by hand tests the job; it does not test that the scheduler will ever call it.
claude-bot modified the milestone from v1.0.0 to v1.1.0 2026-09-01 19:02:46 +00:00
Author
Contributor

Moved out of v1.0.0 to v1.1.0 — this is an observation to run tomorrow morning, not a blocker on cutting the release.

That is a reasonable call because the mechanism is already verified; what remains is confirmation under ordinary conditions:

Leg State
Configure → review → approve verified against the live controller
Push today's scheme scheme push, no-event default, and arbitrary-colour push all verified
Scheduler on / dim dim_job fired unattended on the night of 2026-08-31
Scheduler sunrise-off fixed in #106 and verified firing unattended on real hardwareoff_job logged a schedule_log row and switched the controller off, with the pending-off condition genuinely reproduced rather than simulated
Failure paths / fallback all three policies, plus the spaced-retry chain confirmed in the persistent jobstore

So the one unticked criterion — "the loop runs unattended" — is now about watching an ordinary night end to end, not about discovering whether it works. The residual risk that a full 19:01 → 23:00 → 00:01 → 06:32 cycle behaves differently from the reproduced one is small.

What to check tomorrow

curl -s "$IRIS/api/v1/schedule-log?limit=6"     # expect on_job, dim_job, off_job rows
curl -s http://192.168.1.140/json/state          # expect on=false after 06:32 CDT

Tonight's setup is already in place: late_summer_moon_2026 extended through 2026-09-01, its approved scheme resolving for tonight, on_job 19:01 → dim_job 23:00 → reschedule_daily 00:01 → off_job 06:32. The Home Assistant automation that masked the original failure is disabled, so an off_job row tomorrow is attributable to Iris alone.

One caveat if this slips. The confirming observation is only meaningful while the dev deployment keeps running an image with #106 in it and the HA automation stays off. If either changes before the check happens, the result stops being evidence — worth re-establishing both rather than trusting a stale setup.

**Moved out of v1.0.0 to v1.1.0** — this is an observation to run tomorrow morning, not a blocker on cutting the release. That is a reasonable call because the *mechanism* is already verified; what remains is confirmation under ordinary conditions: | Leg | State | |---|---| | Configure → review → approve | ✅ verified against the live controller | | Push today's scheme | ✅ scheme push, no-event default, and arbitrary-colour push all verified | | Scheduler on / dim | ✅ `dim_job` fired unattended on the night of 2026-08-31 | | Scheduler sunrise-off | ✅ fixed in #106 and **verified firing unattended on real hardware** — `off_job` logged a `schedule_log` row and switched the controller off, with the pending-off condition genuinely reproduced rather than simulated | | Failure paths / fallback | ✅ all three policies, plus the spaced-retry chain confirmed in the persistent jobstore | So the one unticked criterion — *"the loop runs unattended"* — is now about watching an ordinary night end to end, not about discovering whether it works. The residual risk that a full 19:01 → 23:00 → 00:01 → 06:32 cycle behaves differently from the reproduced one is small. ### What to check tomorrow ```sh curl -s "$IRIS/api/v1/schedule-log?limit=6" # expect on_job, dim_job, off_job rows curl -s http://192.168.1.140/json/state # expect on=false after 06:32 CDT ``` Tonight's setup is already in place: `late_summer_moon_2026` extended through 2026-09-01, its approved scheme resolving for tonight, `on_job` 19:01 → `dim_job` 23:00 → `reschedule_daily` 00:01 → `off_job` 06:32. The Home Assistant automation that masked the original failure is disabled, so an `off_job` row tomorrow is attributable to Iris alone. **One caveat if this slips.** The confirming observation is only meaningful while the dev deployment keeps running an image with #106 in it and the HA automation stays off. If either changes before the check happens, the result stops being evidence — worth re-establishing both rather than trusting a stale setup.
Author
Contributor

Confirmed — three consecutive unattended cycles

Checked on 2026-09-04. The dev container has been up since the #106 deploy (started 2026-09-01T17:07Z, image built 16:51Z, get_active_times present), so all three nights ran on the fixed build with no intervention.

local time           job       status
Tue 09-01 19:01:42   on_job    success
Tue 09-01 23:00:00   dim_job   success
Wed 09-02 06:32:31   off_job   success
Wed 09-02 19:00:11   on_job    success
Wed 09-02 23:00:00   dim_job   success
Thu 09-03 06:33:24   off_job   success
Thu 09-03 18:58:39   on_job    success
Thu 09-03 23:00:00   dim_job   success
Fri 09-04 06:34:17   off_job   success

Every off_job crossed a 00:01 reschedule_daily and survived — the failure in #106 reproduced three more times over, and didn't.

The solar drift is right too, which is a useful independent check that the times are being recomputed rather than cached: sunset−30 moves earlier each evening (19:01 → 19:00 → 18:58) and sunrise later each morning (06:32 → 06:33 → 06:34).

Which night actually satisfies the criterion

Being precise, because it matters: only the first cycle had the lights genuinely lit. 2026-09-01 was covered by late_summer_moon_2026, so on_job pushed the approved scheme (global_brightness: 128), dim_job took it to 51, and off_job switched it off at sunrise. That is the full configure→approve→push→schedule→sunrise-off loop, unattended, end to end.

2026-09-02 and 09-03 are no-event days with default_scheme unset, so on_job correctly pushed off and the lights never came on. Those two nights confirm the scheduler keeps firing and rescheduling correctly across days, but they are not lit cycles.

The Home Assistant automation stayed disabled throughout, so nothing external produced these results.

Acceptance criteria

  • The configure→approve→push→schedule→sunrise-off loop runs unattended — 2026-09-01 → 09-02, with two further nights confirming the scheduler's day-to-day behaviour
  • Fallback behaviour observed and correct — all three policies, plus the spaced-retry chain in the persistent jobstore

Flow

  • Configure → review → approve a scheme
  • Push today's scheme to the controller
  • Scheduler turns lights on at sunset, dims at late-night, off at sunrise
  • Failure paths surface (connection loss → fallback policy)

One defect found while checking

Brightness has been decaying across the quiet nights: 128 → 51 → 20 → 8. run_dim_job dims relative to the live value with no check that the lights are on, so on consecutive no-event days each night's dim compounds on the last. Filed as #110 — not a scheduler fault, and invisible while the lights are off, but it would surprise anyone who turned them on by hand mid-week.

Closing.

## Confirmed — three consecutive unattended cycles Checked on 2026-09-04. The dev container has been up since the #106 deploy (`started 2026-09-01T17:07Z`, image built `16:51Z`, `get_active_times` present), so all three nights ran on the fixed build with no intervention. ``` local time job status Tue 09-01 19:01:42 on_job success Tue 09-01 23:00:00 dim_job success Wed 09-02 06:32:31 off_job success Wed 09-02 19:00:11 on_job success Wed 09-02 23:00:00 dim_job success Thu 09-03 06:33:24 off_job success Thu 09-03 18:58:39 on_job success Thu 09-03 23:00:00 dim_job success Fri 09-04 06:34:17 off_job success ``` **Every `off_job` crossed a 00:01 `reschedule_daily` and survived** — the failure in #106 reproduced three more times over, and didn't. The solar drift is right too, which is a useful independent check that the times are being recomputed rather than cached: sunset−30 moves earlier each evening (19:01 → 19:00 → 18:58) and sunrise later each morning (06:32 → 06:33 → 06:34). ### Which night actually satisfies the criterion Being precise, because it matters: **only the first cycle had the lights genuinely lit.** 2026-09-01 was covered by `late_summer_moon_2026`, so `on_job` pushed the approved scheme (`global_brightness: 128`), `dim_job` took it to 51, and `off_job` switched it off at sunrise. That is the full configure→approve→push→schedule→sunrise-off loop, unattended, end to end. 2026-09-02 and 09-03 are no-event days with `default_scheme` unset, so `on_job` correctly pushed *off* and the lights never came on. Those two nights confirm the scheduler keeps firing and rescheduling correctly across days, but they are not lit cycles. The Home Assistant automation stayed disabled throughout, so nothing external produced these results. ### Acceptance criteria - [x] **The configure→approve→push→schedule→sunrise-off loop runs unattended** — 2026-09-01 → 09-02, with two further nights confirming the scheduler's day-to-day behaviour - [x] **Fallback behaviour observed and correct** — all three policies, plus the spaced-retry chain in the persistent jobstore ### Flow - [x] Configure → review → approve a scheme - [x] Push today's scheme to the controller - [x] Scheduler turns lights on at sunset, dims at late-night, off at sunrise - [x] Failure paths surface (connection loss → fallback policy) ### One defect found while checking Brightness has been decaying across the quiet nights: **128 → 51 → 20 → 8**. `run_dim_job` dims relative to the *live* value with no check that the lights are on, so on consecutive no-event days each night's dim compounds on the last. Filed as **#110** — not a scheduler fault, and invisible while the lights are off, but it would surprise anyone who turned them on by hand mid-week. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#7
No description provided.