Review nudges: say which upcoming nights need approval, before they go dark #149

Closed
opened 2026-09-26 05:01:06 +00:00 by claude-bot · 2 comments
Collaborator

Problem

Iris's model is that nothing reaches the house without a human approving it, and that is right. But the review queue only works if someone remembers to open it, and the cost of forgetting is currently silent.

Verified against main (services/schedule_service.py _push_today_scheme):

  • On a night whose event has a scheme that is not approved, the on job pushes lights_off_state() and logs {"pushed": "off", "reason": "no_approved_scheme"}.
  • It does not fall back to default_scheme, and no specific webhook fires. push_failed only fires if the off push itself fails.
  • So the house goes dark on Christmas Eve, and the first anyone hears of it is a neighbour asking.

The only existing nudge is the review_queue_ready webhook (schemas/webhooks.py), which the weekly and yearly jobs fire with {job, years}. It carries no counts, no dates, and no sense of urgency.

Design

1. A review summary the rest can share

GET /api/v1/schemes/review-summary?days=N returns upcoming days in the next N whose resolved scheme is unapproved, rejected or stale. Each entry has the date, the event, the scheme id, the reason and the days remaining.

  • It must resolve through get_events_for_date and resolve_scheme_for_date, the same path the scheduler uses.
  • For interpolated events (#74), a derived day is unapproved when any anchor it blends is unapproved. Report the anchor that needs approving, not the derived row.

2. Webhook events

  • review_reminder: a weekly digest, default Monday 09:00, configurable. It sends {upcoming_unapproved: [...], count, horizon_days}, and only when the count is > 0. A nudge that fires with nothing to say trains people to ignore it.
  • unapproved_tonight: fired in the morning (default 09:00) on any day whose on job will push "off" for want of an approval. It gives a day's warning while there's still time to approve, and names the scheme and a deep link to it.
  • Escalation: 3 days out and on the day itself, not only weekly.
  • Add review_reminder and unapproved_tonight to DEFAULT_WEBHOOK_EVENTS alongside push_failed.

3. In the app

  • A header banner: "4 nights in the next 14 days need review". It links to the review filter (Toolbar.tsx already has the unapproved filter) narrowed to that window.
  • On the calendar, nights that will go dark for want of approval get a distinct marker, not the same styling as "no event".

4. Reconsider the dark fallback (decision needed)

Going dark is the conservative choice, and "nothing unapproved reaches the house" is a principle worth keeping. But the configured default_scheme was approved by the user as a general fallback. An option to use it on unapproved nights would be reasonable:

  • unapproved_policy: "off" | "default_scheme"
  • Default off, which preserves the current behaviour.
  • Record whichever is chosen in the spec, §3.

Settings

All runtime-settable:

  • REVIEW_HORIZON_DAYS=14
  • REVIEW_REMINDER_WEEKDAY=mon
  • REVIEW_REMINDER_TIME=09:00
  • UNAPPROVED_TONIGHT_TIME=09:00

Pairs with

  • #154 (PWA push): these are the obvious first push notifications.
  • #148 (watchdog): both are "tell me before it goes wrong".

Acceptance

  • The summary endpoint agrees with what the scheduler would actually push. Test it across approved, unapproved, stale, interpolated and no-event days.
  • unapproved_tonight fires on the morning of a day that would go dark, and not on a day that won't.
  • The weekly digest is silent when nothing needs review.
  • Banner and calendar markers are in place, and the E2E covers the banner linking to the filtered queue.
  • Docs (webhook payload reference, with ntfy and Gotify examples), CHANGELOG.
## Problem Iris's model is that nothing reaches the house without a human approving it, and that is right. But the review queue only works if someone remembers to open it, and **the cost of forgetting is currently silent.** Verified against `main` (`services/schedule_service.py` `_push_today_scheme`): - On a night whose event has a scheme that is **not approved**, the on job pushes `lights_off_state()` and logs `{"pushed": "off", "reason": "no_approved_scheme"}`. - It does **not** fall back to `default_scheme`, and no specific webhook fires. `push_failed` only fires if the *off* push itself fails. - So the house goes dark on Christmas Eve, and the first anyone hears of it is a neighbour asking. The only existing nudge is the `review_queue_ready` webhook (`schemas/webhooks.py`), which the weekly and yearly jobs fire with `{job, years}`. It carries no counts, no dates, and no sense of urgency. ## Design ### 1. A review summary the rest can share `GET /api/v1/schemes/review-summary?days=N` returns upcoming days in the next *N* whose resolved scheme is unapproved, rejected or stale. Each entry has the date, the event, the scheme id, the reason and the days remaining. - It must resolve through `get_events_for_date` and `resolve_scheme_for_date`, the same path the scheduler uses. - For interpolated events (#74), a derived day is unapproved when any anchor it blends is unapproved. Report the *anchor* that needs approving, not the derived row. ### 2. Webhook events - **`review_reminder`**: a weekly digest, default Monday 09:00, configurable. It sends `{upcoming_unapproved: [...], count, horizon_days}`, and **only when the count is > 0**. A nudge that fires with nothing to say trains people to ignore it. - **`unapproved_tonight`**: fired in the morning (default 09:00) on any day whose on job will push "off" for want of an approval. It gives a day's warning while there's still time to approve, and names the scheme and a deep link to it. - **Escalation:** 3 days out and on the day itself, not only weekly. - **Add `review_reminder` and `unapproved_tonight` to `DEFAULT_WEBHOOK_EVENTS`** alongside `push_failed`. ### 3. In the app - A header banner: "4 nights in the next 14 days need review". It links to the review filter (`Toolbar.tsx` already has the `unapproved` filter) narrowed to that window. - On the calendar, nights that will go dark for want of approval get a distinct marker, not the same styling as "no event". ### 4. Reconsider the dark fallback (decision needed) Going dark is the conservative choice, and "nothing unapproved reaches the house" is a principle worth keeping. But the configured `default_scheme` *was* approved by the user as a general fallback. An option to use it on unapproved nights would be reasonable: - `unapproved_policy: "off" | "default_scheme"` - **Default `off`**, which preserves the current behaviour. - Record whichever is chosen in the spec, §3. ### Settings All runtime-settable: - `REVIEW_HORIZON_DAYS=14` - `REVIEW_REMINDER_WEEKDAY=mon` - `REVIEW_REMINDER_TIME=09:00` - `UNAPPROVED_TONIGHT_TIME=09:00` ## Pairs with - #154 (PWA push): these are the obvious first push notifications. - #148 (watchdog): both are "tell me before it goes wrong". ## Acceptance - [ ] The summary endpoint agrees with what the scheduler would actually push. Test it across approved, unapproved, stale, interpolated and no-event days. - [ ] `unapproved_tonight` fires on the morning of a day that would go dark, and not on a day that won't. - [ ] The weekly digest is silent when nothing needs review. - [ ] Banner and calendar markers are in place, and the E2E covers the banner linking to the filtered queue. - [ ] Docs (webhook payload reference, with ntfy and Gotify examples), CHANGELOG.
claude-bot added this to the v1.2.0 milestone 2026-09-26 05:02:36 +00:00
Author
Collaborator

Picking this up first in the v1.2.0 run.

The decision in §4: implement unapproved_policy with the default off, so current behaviour is unchanged and the option is there for anyone who wants default_scheme instead.

Foundation work in this issue: one shared plan_for_date() helper answers "what will the scheduler push on day X, and why". The scheduler's own on-push, the review summary here, and later the season preview (#151) and watchdog (#148) all use it, so that the answer is computed in one place. The scheduler moves onto it with no behaviour change, and a parity test proves that.

Picking this up first in the v1.2.0 run. **The decision in §4:** implement `unapproved_policy` with the default `off`, so current behaviour is unchanged and the option is there for anyone who wants `default_scheme` instead. **Foundation work in this issue:** one shared `plan_for_date()` helper answers "what will the scheduler push on day X, and why". The scheduler's own on-push, the review summary here, and later the season preview (#151) and watchdog (#148) all use it, so that the answer is computed in one place. The scheduler moves onto it with no behaviour change, and a parity test proves that.
Author
Collaborator

Done in #162 (squash-merged, CI green).

Delivered:

  • services/day_plan.py: plan_for_date / plan_for_range, which the on job now carries out. Zero behaviour change, proven by the parity test run against the old schedule_service.py.
  • GET /schemes/review-summary.
  • review_reminder (weekly, silent when nothing needs review) and unapproved_tonight (on the day and 3 days before), both on by default.
  • unapproved_policy (default off).
  • A header banner and calendar markers.
  • URL-driven filter, window and open day, so the webhook links land on the right view.

Decisions made along the way:

  • will_go_dark follows what will actually be pushed.
  • Stale-but-approved schemes aren't counted, because they still light.
  • A user event with no scheme is reported as missing.
  • "Today" means the configured timezone.

Follow-ups, filed:

  • #163: eve-phase schemes are never pushed. This is pre-existing, and the parity test documents it.
  • #164: no SPA fallback, which is why the deep links use /?….
  • #165: some schedule settings don't reschedule.
  • #166: the calendar's "today" is UTC.

Not verified: the ntfy templating examples in docs/setup.md §4 were not tested against a real ntfy server.

Done in #162 (squash-merged, CI green). **Delivered:** - **`services/day_plan.py`:** `plan_for_date` / `plan_for_range`, which the on job now carries out. Zero behaviour change, proven by the parity test run against the old `schedule_service.py`. - **`GET /schemes/review-summary`.** - **`review_reminder`** (weekly, silent when nothing needs review) and **`unapproved_tonight`** (on the day and 3 days before), both on by default. - **`unapproved_policy`** (default `off`). - **A header banner and calendar markers.** - **URL-driven filter, window and open day,** so the webhook links land on the right view. **Decisions made along the way:** - `will_go_dark` follows what will actually be pushed. - Stale-but-approved schemes aren't counted, because they still light. - A user event with no scheme is reported as `missing`. - "Today" means the configured timezone. **Follow-ups, filed:** - #163: eve-phase schemes are never pushed. This is pre-existing, and the parity test documents it. - #164: no SPA fallback, which is why the deep links use `/?…`. - #165: some schedule settings don't reschedule. - #166: the calendar's "today" is UTC. **Not verified:** the ntfy templating examples in `docs/setup.md` §4 were not tested against a real ntfy server.
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#149
No description provided.