Warn before an unapproved night goes dark (#149) #162

Merged
claude-bot merged 2 commits from feat/149-review-nudges into main 2026-09-26 08:51:55 +00:00
Collaborator

Closes #149.

Why

On a night whose event has no approved scheme, the on job pushes "off" with reason no_approved_scheme, and nothing tells anyone. The only existing nudge, review_queue_ready, carries no counts or dates.

What

1. One shared day-plan resolver (services/day_plan.py). plan_for_date / plan_for_range return a frozen DayPlan saying what the on job will push on night D and why: the event, phase, scheme, the anchors a human must approve, will_push, the reason, and the exact wled_state.

  • _push_today_scheme now only carries the plan out (push, ramp, webhook).
  • It is read-only. #151 (season preview) and #148 (watchdog) will reuse it.
  • Behaviour preservation: a parity test walks 28 nights (approved, candidate, rejected, stale, missing, pinned, eve, half-approved interpolated, no-event) under 5 settings combinations. Because the push now reads the plan, that test alone would be nearly circular, so it was also run against main's schedule_service.py. Every unapproved_policy="off" case passed, so behaviour is unchanged.

2. GET /api/v1/schemes/review-summary?days=N. Upcoming nights that won't show their own scheme, grouped by the scheme that needs approving, with days_remaining and will_go_dark. Stale-but-approved schemes still light, so they aren't counted. A user event with no generated scheme is reported as missing.

3. Two webhook events, both on by default. WEBHOOK_EVENTS is now derived from the WebhookEvent Literal, so the two lists can't drift.

  • review_reminder: weekly (configurable weekday and time), only when something needs review.
  • unapproved_tonight: daily at a configurable time, for a night that won't show its scheme, on the day and 3 days before.
  • Both jobs re-register when their settings change, via the existing reload_scheduled_jobs path.
  • Deep links take their origin from OIDC_REDIRECT_URI, and are omitted when it isn't set.

4. unapproved_policy: off (the default, and today's behaviour) or default_scheme, which shows the no-event default instead of going dark. Recorded in spec §3.

5. UI:

  • A header banner, "N nights in the next D days need review", linking to the Unapproved filter narrowed to that window.
  • Calendar marker: a dashed rose border with a moon icon for nights that will go dark (amber when they'll show the default).
  • Filter, window and open day now live in the URL, so the webhook links land on the right view.

Fixed along the way: bulk approve fell back to every candidate in the year when a filter matched nothing. The new window made that reachable.

Verification

  • Backend: 912 passed. ruff is clean, and mypy is clean (run in a python:3.14-slim container, because it is blocked on the Windows host).
  • Frontend: tsc, lint (max-warnings 0), vitest (147 passed, 15 new) and build, all in node:22-slim.
  • E2E: the CI procedure (read-only image plus Playwright 1.63): 8/8 passed, 3 new.

Not verified

The ntfy examples in docs/setup.md §4 use ntfy's message templating and were not tested against a real ntfy server.

Existing bugs found, filed separately

  • Eve-phase schemes are never pushed.
  • Deep links to anything but / 404, because there is no SPA fallback.
  • Several schedule settings don't reschedule when changed.
  • The calendar's "today" is UTC.

🤖 Generated with Claude Code

Closes #149. ## Why On a night whose event has no approved scheme, the on job pushes "off" with reason `no_approved_scheme`, and nothing tells anyone. The only existing nudge, `review_queue_ready`, carries no counts or dates. ## What **1. One shared day-plan resolver** (`services/day_plan.py`). `plan_for_date` / `plan_for_range` return a frozen `DayPlan` saying what the on job will push on night D and why: the event, phase, scheme, the anchors a human must approve, `will_push`, the reason, and the exact `wled_state`. - `_push_today_scheme` now only carries the plan out (push, ramp, webhook). - It is read-only. #151 (season preview) and #148 (watchdog) will reuse it. - **Behaviour preservation:** a parity test walks 28 nights (approved, candidate, rejected, stale, missing, pinned, eve, half-approved interpolated, no-event) under 5 settings combinations. Because the push now reads the plan, that test alone would be nearly circular, so it was also run against **`main`'s** `schedule_service.py`. Every `unapproved_policy="off"` case passed, so behaviour is unchanged. **2. `GET /api/v1/schemes/review-summary?days=N`.** Upcoming nights that won't show their own scheme, grouped by the scheme that needs approving, with `days_remaining` and `will_go_dark`. Stale-but-approved schemes still light, so they aren't counted. A user event with no generated scheme is reported as `missing`. **3. Two webhook events, both on by default.** `WEBHOOK_EVENTS` is now derived from the `WebhookEvent` Literal, so the two lists can't drift. - `review_reminder`: weekly (configurable weekday and time), **only when something needs review**. - `unapproved_tonight`: daily at a configurable time, for a night that won't show its scheme, **on the day and 3 days before**. - Both jobs re-register when their settings change, via the existing `reload_scheduled_jobs` path. - Deep links take their origin from `OIDC_REDIRECT_URI`, and are omitted when it isn't set. **4. `unapproved_policy`**: `off` (the default, and today's behaviour) or `default_scheme`, which shows the no-event default instead of going dark. Recorded in spec §3. **5. UI:** - A header banner, "N nights in the next D days need review", linking to the Unapproved filter narrowed to that window. - Calendar marker: a dashed rose border with a moon icon for nights that will go dark (amber when they'll show the default). - Filter, window and open day now live in the URL, so the webhook links land on the right view. **Fixed along the way:** bulk approve fell back to *every* candidate in the year when a filter matched nothing. The new window made that reachable. ## Verification - **Backend:** 912 passed. ruff is clean, and mypy is clean (run in a `python:3.14-slim` container, because it is blocked on the Windows host). - **Frontend:** `tsc`, lint (max-warnings 0), vitest (147 passed, 15 new) and build, all in `node:22-slim`. - **E2E:** the CI procedure (read-only image plus Playwright 1.63): 8/8 passed, 3 new. ## Not verified The ntfy examples in `docs/setup.md` §4 use ntfy's message templating and were not tested against a real ntfy server. ## Existing bugs found, filed separately - Eve-phase schemes are never pushed. - Deep links to anything but `/` 404, because there is no SPA fallback. - Several schedule settings don't reschedule when changed. - The calendar's "today" is UTC. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add services/day_plan.py: plan_for_date / plan_for_range answer "what will
the on job push on date D, and why" by composing the existing resolvers
(priority, phase, interpolation, no-event default). The on job now pushes
exactly plan.wled_state, so anything that predicts a night -- the review
summary here, the season preview (#151) and watchdog (#148) later -- cannot
disagree with what is pushed.

No behaviour change under the default settings. A parity test walks a month
of approved, candidate, rejected, stale, missing, pinned, eve, interpolated
and no-event nights under every no-event default, and asserts the pushed
state and reason equal the plan's; it also passes against the pre-refactor
_push_today_scheme for every unapproved_policy="off" case.

Adds the runtime settings the review nudges use. unapproved_policy defaults
to "off" (today's behaviour); "default_scheme" makes an unapproved event
night show the no-event default, reported as reason "unapproved_default".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Warn before an unapproved night goes dark (#149)
All checks were successful
CI / Dockerfile lint (pull_request) Successful in 23s
CI / Pre-commit hooks (pull_request) Successful in 59s
CI / Python lint & type-check (pull_request) Successful in 1m29s
CI / Alembic migration check (pull_request) Successful in 1m56s
CI / Frontend lint, test & build (pull_request) Successful in 4m8s
CI / Python tests (pull_request) Successful in 5m22s
CI / Docker build, health smoke & E2E (pull_request) Successful in 7m39s
96786288d7
An event night with nothing approved is dark by design, but nothing said so
until the night. Now, all read from the day plan the on job pushes from:

- GET /schemes/review-summary: upcoming unapproved event nights (default
  window review_horizon_days, max 60), grouped by the scheme to approve,
  with days_remaining, reason (unapproved/rejected/stale/missing) and
  will_go_dark. A blended interpolated night names only its unapproved
  anchor(s).
- A header banner on every page, "N nights in the next D days need review",
  linking to the review queue filtered to unapproved and narrowed to the
  window. Filter, window and the open day now live in the URL.
- A dashed red marker on each such night in the calendar, distinct from the
  faded no-event look.
- Webhook events review_reminder (weekly digest, silent when empty) and
  unapproved_tonight (per night, 3 days out and on the day), on by default
  for new endpoints. Links use the origin of OIDC_REDIRECT_URI, and are
  omitted when it is unset. Both jobs re-register when their times change.
- Settings -> Review Nudges for the policy, window and job times.

Also: bulk approve no longer falls back to every candidate in the year when
a narrowed view matches nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot deleted branch feat/149-review-nudges 2026-09-26 08:51:57 +00:00
Sign in to join this conversation.
No reviewers
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!162
No description provided.