Push-path correctness cluster: resolver wiring, default_scheme, quick-push revert, fallback policies (#43 #44 #47 #45) #68

Merged
claude-bot merged 4 commits from fix/43-push-paths into main 2026-07-16 06:42:29 +00:00
Contributor

Wave-2 backend correctness cluster. These four issues all touch schedule_service.py / wled_push.py / routers/wled.py, so per the #43 note they're done sequentially on one branch (4 commits, one per issue) to avoid conflicts.

#43 — Wire push paths to priority_resolver.get_events_for_date

Both push_today (API) and the scheduler daily push used naive max(priority) queries, so date pins and Promote-Secondary had no runtime effect. Both now call get_events_for_date (honors date-level pins + user-event shadowing). The scheduler now resolves against the DB (pins/shadow rows don't exist in the in-memory cache), capturing primary.id before the session closes.

#44 — Honor the no-event default_scheme (§5.4)

New shared resolve_no_event_state helper used by both push paths: off → lights-off; dim_white → warm-white (~2700K) at the configured late-night brightness; scheme reference → validate exists+approved, else off + logged WARNING. 5 tests across all modes. (Frontend Settings control is #48; the no-event response action field now carries the mode instead of the literal "lights_off".)

#47 — Revert active quick push at the on event (§6.1)

trigger_scheduler_revert was called on dim/off but not on, so a quick push spanning the on boundary survived it. Now reverted in the shared _push_today_scheme (before pushing) — also covers the manual push-today/push-now endpoints and avoids a stale-saved-state clobber.

#45 — Finish fallback policies (§5.3)

retry no longer blocks in an immediate loop — it makes one attempt then schedules 3 spaced retries (5-min DateTrigger jobs) on the existing APScheduler + persistent jobstore (picklable args → survive restart; wled_push_retry id for dedup). last_known_good gains a 1-min reconnect watcher that pushes the expected state once on reconnect then removes its own job. push_failed webhook is now owned by wled_push and fires only after the final retry; callers updated (_raise_on_failed_push is raise-only). PushResult gains retry_scheduled.

Verification

Backend ruff check / ruff format --check / mypy app/ clean; pytest 217 passed locally (the only 2 errors are pre-existing Windows-only test_backup.py file-locking PermissionErrors — they pass on the Linux CI runner). New tests: test_push_resolution.py, test_no_event_default.py, test_scheduler.py (on-revert), test_wled_fallback.py.

Closes #43, closes #44, closes #47, closes #45.

🤖 Generated with Claude Code

Wave-2 backend correctness cluster. These four issues all touch `schedule_service.py` / `wled_push.py` / `routers/wled.py`, so per the #43 note they're done **sequentially on one branch** (4 commits, one per issue) to avoid conflicts. ### #43 — Wire push paths to `priority_resolver.get_events_for_date` Both `push_today` (API) and the scheduler daily push used naive `max(priority)` queries, so date pins and Promote-Secondary had **no runtime effect**. Both now call `get_events_for_date` (honors date-level pins + user-event shadowing). The scheduler now resolves against the **DB** (pins/shadow rows don't exist in the in-memory cache), capturing `primary.id` before the session closes. ### #44 — Honor the no-event `default_scheme` (§5.4) New shared `resolve_no_event_state` helper used by both push paths: `off` → lights-off; `dim_white` → warm-white (~2700K) at the configured late-night brightness; **scheme reference** → validate exists+approved, else off + logged WARNING. 5 tests across all modes. (Frontend Settings control is #48; the no-event response `action` field now carries the mode instead of the literal `"lights_off"`.) ### #47 — Revert active quick push at the `on` event (§6.1) `trigger_scheduler_revert` was called on dim/off but not `on`, so a quick push spanning the `on` boundary survived it. Now reverted in the shared `_push_today_scheme` (before pushing) — also covers the manual push-today/push-now endpoints and avoids a stale-saved-state clobber. ### #45 — Finish fallback policies (§5.3) `retry` no longer blocks in an immediate loop — it makes one attempt then schedules 3 spaced retries (5-min `DateTrigger` jobs) on the **existing** APScheduler + persistent jobstore (picklable args → survive restart; `wled_push_retry` id for dedup). `last_known_good` gains a 1-min reconnect watcher that pushes the expected state once on reconnect then removes its own job. `push_failed` webhook is now owned by `wled_push` and fires only after the **final** retry; callers updated (`_raise_on_failed_push` is raise-only). `PushResult` gains `retry_scheduled`. ## Verification Backend `ruff check` / `ruff format --check` / `mypy app/` clean; `pytest` **217 passed** locally (the only 2 errors are pre-existing Windows-only `test_backup.py` file-locking `PermissionError`s — they pass on the Linux CI runner). New tests: `test_push_resolution.py`, `test_no_event_default.py`, `test_scheduler.py` (on-revert), `test_wled_fallback.py`. Closes #43, closes #44, closes #47, closes #45. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The API push (routers/wled.py::push_today) and scheduler daily push
(services/schedule_service.py::_push_today_scheme) resolved today's event
with naive max(priority) queries, so date-level pins ("Promote Secondary")
and user-event shadowing had no runtime effect on the controller.

Both paths now call priority_resolver.get_events_for_date, which honors
date_overrides pins and parent-event shadowing. Behavior around scheme
selection, phase resolution, logging, audit, and webhooks is preserved.
The scheduler now resolves against the DB (inside its existing session)
instead of the in-memory event cache, since pins/shadowing live in the DB.

Adds tests proving the wiring: a pin via PUT /event-date-overrides/{date}
changes what POST /wled/push sends, and a user-event override shadows its
parent in the scheduler push path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both push paths previously pushed unconditional lights-off on days with no
matching event, ignoring the configured default_scheme runtime setting.

Adds a shared helper, no_event_resolver.resolve_no_event_state(db, runtime)
-> (wled_state, reason), that maps default_scheme to a WLED state:
  - None / "off"  -> lights_off_state()            (reason "no_event")
  - "dim_white"   -> warm-white ambient at         (reason "default_dim_white")
                     late-night brightness
  - "<scheme id>" -> that saved scheme, if it       (reason "default_scheme")
                     exists and is approved
A missing/unapproved scheme reference logs a WARNING and falls back to
lights-off, mirroring how the normal push path guards scheme availability.

The no-event branch of routers/wled.py::push_today and
services/schedule_service.py::_push_today_scheme now use this helper; the
resolved reason flows into the return dict / audit / webhook context.
Adds scheme_translator.dim_white_state + WARM_WHITE_RGB for the ambient state.

Tests cover all three modes plus the invalid- and unapproved-reference
fallbacks end-to-end through POST /wled/push.

Frontend Settings control for default_scheme is tracked separately in #48
and is intentionally untouched here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Quick push should revert at the next scheduler event (on/dim/off, whichever
comes first). run_dim_job and run_off_job already call
trigger_scheduler_revert, but the on path did not, so a quick push active
when the lights come on survived the on event and the scheduled scheme was
never applied.

Add trigger_scheduler_revert(ctrl) in _push_today_scheme, right after the
controller is fetched and before pushing today's scheme -- mirroring the
"get controller -> revert -> push" order in the dim/off jobs. Placing it in
_push_today_scheme (rather than only run_on_job) also covers the manual
/schedule/push-today and /schedule/push-now endpoints, which run the on-job,
and avoids leaving a stale quick-push tracker that a later dim/off event
could use to clobber the scheduled scheme.

Test: a quick push active across the on boundary is cleared when run_on_job
runs, and today's scheduled scheme is the final state pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix #45: spaced retries + last_known_good reconnect watcher
All checks were successful
CI / Python lint & type-check (pull_request) Successful in 5m49s
CI / Python tests (pull_request) Successful in 4m55s
CI / Frontend lint, test & build (pull_request) Successful in 7m46s
CI / Alembic migration check (pull_request) Successful in 9m12s
CI / Dependency audit (pull_request) Successful in 12m37s
CI / Docker build, health smoke & E2E (pull_request) Successful in 46m33s
bddb30c71d
wled_push previously "retried" in an immediate blocking loop and stored
last_known_good but never re-pushed it. Both fallback policies now use the
shared APScheduler + persistent jobstore instead of blocking.

push_with_fallback now makes a single immediate attempt and, on failure,
arranges follow-up by policy:
- retry: schedule spaced retries (3 more attempts, 5-min DateTrigger jobs,
  fixed id wled_push_retry + replace_existing for dedup; picklable kwargs so
  they survive a restart). The chain reschedules itself until the final
  attempt.
- last_known_good: start a single IntervalTrigger reconnect-watch job
  (id wled_reconnect_watch + replace_existing) that polls reachability and,
  once the controller answers, pushes the expected state exactly once then
  removes its own job.
- silent: unchanged (one attempt, log).

push_with_fallback now also owns the push_failed webhook, firing it only after
the FINAL retry (or immediately for silent / last_known_good / no-scheduler).
Callers pass a context dict and no longer fire push_failed themselves;
routers/wled.py and schedule_service (_raise_on_failed_push is now raise-only)
were updated accordingly, keeping the 502 / job-failure and scheme_pushed
behavior intact. PushResult gains retry_scheduled. Scheduler jobstore reused;
scheduler is imported lazily inside wled_push to avoid an import cycle.

Tests (scheduler-mocked, clock-independent): retries are scheduled ~5 min
apart and stop after the final attempt; reconnect triggers exactly one
catch-up push then stops polling; push_failed fires only after the final
failed attempt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude-bot deleted branch fix/43-push-paths 2026-07-16 06:42:29 +00:00
Sign in to join this conversation.
No description provided.