Run the missed sweep in the API and publish quest-missed on SSE
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.
This commit is contained in:
+12
-1
@@ -48,6 +48,7 @@ from api.database import DatabaseState, make_database
|
||||
from api.events import TransitionPublisher
|
||||
from api.routes_admin import router as admin_router
|
||||
from api.routes_panel import router as panel_router
|
||||
from api.scheduler import MissedSweepScheduler
|
||||
|
||||
|
||||
def _build_lifespan(db_path: str):
|
||||
@@ -57,7 +58,11 @@ def _build_lifespan(db_path: str):
|
||||
path in without threading it through FastAPI's lifespan signature.
|
||||
The lifespan also installs the app's ONE in-process transition
|
||||
publisher on ``app.state.publisher`` (see :mod:`api.events`) so
|
||||
every route and SSE subscriber shares that single instance.
|
||||
every route and SSE subscriber shares that single instance, and
|
||||
starts the daily missed-sweep scheduler
|
||||
(:class:`api.scheduler.MissedSweepScheduler`) against the same
|
||||
database and publisher — cancelled before the connection closes on
|
||||
shutdown.
|
||||
"""
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -65,10 +70,16 @@ def _build_lifespan(db_path: str):
|
||||
state = make_database(db_path)
|
||||
app.state.db = state
|
||||
app.state.publisher = TransitionPublisher()
|
||||
scheduler = MissedSweepScheduler(
|
||||
state.database, publisher=app.state.publisher
|
||||
)
|
||||
app.state.sweep_scheduler = scheduler
|
||||
try:
|
||||
await state.open_and_migrate()
|
||||
scheduler.start()
|
||||
yield
|
||||
finally:
|
||||
await scheduler.stop()
|
||||
await state.close()
|
||||
|
||||
return lifespan
|
||||
|
||||
Reference in New Issue
Block a user