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).
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.
HA event triggers expose the payload as trigger.event.data, not
trigger.payload; fix the blueprint, its tests, and the README
examples, with a regression guard.
README gains the blueprint section: all four blueprints, their
inputs, the wiring table to the options-flow notification settings,
the celebration helper setup, event/sensor payload reference, and a
copy-pasteable example package. Tests assert every event trigger
references a real NestQuest event and no reminder is produced for an
absent child.
Child ids sharing a prefix no longer suppress each other (split and
compare entries for equality), and queued mode keeps a second child's
day-complete event from being dropped while the first notify runs.
Triggers on nestquest_child_day_complete (never a zero-quest day),
notifies with the payload's child name, and dedupes per child per
day via an input_text helper holding <child_id>:<today> entries
pruned to today — a re-cleared day does not re-fire, but a second
child clearing the same day still celebrates.