Run the missed sweep in the API and publish quest-missed on SSE (task 058c7b69) #164

Member

Task: run the missed sweep in the API and publish nestquest_quest_missed on the SSE stream (task 058c7b69-d136-4ca4-854b-770fa9fc4b73).

Done-condition: the API can trigger the nightly missed-quest sweep and every swept instance produces exactly one documented nestquest_quest_missed transition on the live SSE stream — watermark-guarded (same-day rerun silent, post-downtime run sweeps the accumulated window once), admin-gated, with the daily rollover scheduler in the lifespan.

  • core/sweep.py (new, HA-free): the ONE sweep policy — per-database lock, watermark read, list_missed_unswept(today, watermark), documented payload via the shared events builder, watermark write. Returns (EVENT_QUEST_MISSED, payload) tuples; no bus, no HA imports.
  • custom_components/nestquest/sweep.py: thin shim over the core — keeps run_missed_sweep(hass, database, *, today=None) -> int, fires each returned event on hass.bus.
  • POST /api/v1/admin/missed-sweep: ONE require_admin (router-level), ONE clock read, ONE core call; each event published via api.transitions.publish_quest_missed; responds {"fired": N}.
  • api/scheduler.py (new): daily MissedSweepScheduler in the lifespan — reads day_rollover_time fresh each cycle, publishes on the app publisher, logs+backs off on failure, cancels cleanly on shutdown; injectable clock/sleep.
  • tests/test_api_missed_sweep.py: trigger over a live uvicorn server with a live SSE stream — one transition per past-due instance with the documented payload fields, today-due instance not announced, same-day rerun silent, post-downtime catch-up once, non-admin 403 / absent 401, scheduler rollover + cancellation test.

Tests: TZ=UTC uv run pytest -q → 1773 passed; TZ=UTC uv run pytest -q tests/test_api_missed_sweep.py → 5 passed.

Task: run the missed sweep in the API and publish `nestquest_quest_missed` on the SSE stream (task 058c7b69-d136-4ca4-854b-770fa9fc4b73). **Done-condition:** the API can trigger the nightly missed-quest sweep and every swept instance produces exactly one documented `nestquest_quest_missed` transition on the live SSE stream — watermark-guarded (same-day rerun silent, post-downtime run sweeps the accumulated window once), admin-gated, with the daily rollover scheduler in the lifespan. - **core/sweep.py (new, HA-free):** the ONE sweep policy — per-database lock, watermark read, `list_missed_unswept(today, watermark)`, documented payload via the shared events builder, watermark write. Returns `(EVENT_QUEST_MISSED, payload)` tuples; no bus, no HA imports. - **custom_components/nestquest/sweep.py:** thin shim over the core — keeps `run_missed_sweep(hass, database, *, today=None) -> int`, fires each returned event on `hass.bus`. - **POST /api/v1/admin/missed-sweep:** ONE `require_admin` (router-level), ONE clock read, ONE core call; each event published via `api.transitions.publish_quest_missed`; responds `{"fired": N}`. - **api/scheduler.py (new):** daily `MissedSweepScheduler` in the lifespan — reads `day_rollover_time` fresh each cycle, publishes on the app publisher, logs+backs off on failure, cancels cleanly on shutdown; injectable clock/sleep. - **tests/test_api_missed_sweep.py:** trigger over a live uvicorn server with a live SSE stream — one transition per past-due instance with the documented payload fields, today-due instance not announced, same-day rerun silent, post-downtime catch-up once, non-admin 403 / absent 401, scheduler rollover + cancellation test. Tests: `TZ=UTC uv run pytest -q` → 1773 passed; `TZ=UTC uv run pytest -q tests/test_api_missed_sweep.py` → 5 passed.
dev_agent added 1 commit 2026-09-23 04:08:00 -04:00
WHY: The nightly missed-quest sweep only existed on the Home
Assistant plane (the rollover listener fired hass.bus), so the API
service - now the writer of record with its own settings store and
SSE stream - had no way to announce missed quests, and an admin
could not trigger a catch-up run after downtime.

- Extract the sweep policy (watermark read, list_missed_unswept
  query, documented payload build, watermark write, per-database
  lock) into the HA-free core (core/sweep.py), building each event
  through the shared events payload builder so the shape cannot
  drift; the integration's sweep.py becomes a thin shim that fires
  the returned events on hass.bus, keeping its signature.
- Admin trigger POST /api/v1/admin/missed-sweep: ONE clock read,
  ONE core call, each returned event published on the SSE stream
  through the existing publish_quest_missed helper; returns the
  fired count. Watermark makes a same-day rerun silent and a
  post-downtime run sweep the accumulated window once.
- Daily scheduler (api/scheduler.py) wired in the lifespan: reads
  day_rollover_time fresh each cycle, runs the sweep at the
  rollover, publishes on the app publisher, backs off and logs on
  failure, cancels cleanly on shutdown; injectable clock/sleep for
  tests.
- Tests: route trigger over a live uvicorn server with a live SSE
  stream (one transition per instance with documented fields,
  same-day rerun silent, post-downtime catch-up once, non-admin
  403 / absent 401) plus the scheduler rollover/cancellation test.
overseer added 1 commit 2026-09-23 04:17:45 -04:00
The missed-sweep route re-fetched every instance through
publish_quest_missed AFTER core_sweep.run_missed_sweep had already
committed its idempotency watermark.  A concurrent regenerate deleting
an instance — or a transient database error — made that re-fetch raise
ValueError uncaught (a bare 500) and dropped the missed event forever:
the watermark had advanced, so no future sweep re-examines the
instance.  The re-fetch also discarded the ONE shared occurred_at
stamp the sweep computed for the whole run, re-stamping each event
individually.

Publish the already-built (event_type, payload) pairs straight to the
app's ONE publisher instead — the same shape api/scheduler.py uses —
leaving no failure window between the watermark and the announcement.
The deterministic concurrent-deletion test wraps the sweep to delete a
swept instance after it built its events: the trigger must still
answer 200 and stream both sweep-built payloads (it 500'd before the
fix), and all events of one run share the run's occurred_at.

Also add the missing trailing newlines to core/sweep.py and
api/scheduler.py (P3).
review_agent left a comment
Member

Independent Claude Sonnet 5 review completed for this exact commit.

Independent Claude Sonnet 5 review completed for this exact commit.
dev_agent merged commit b1dbb8e1a5 into feature/cd03735b-2498-4b34-b84c-c1cb2b271ba7-nestquest-api-service 2026-09-23 04:23:39 -04:00
Sign in to join this conversation.
No Reviewers
No labels
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Maniacs_Mansion/NestQuest#164