SSE: publish uncompleted, child-day-complete and missed transitions

The panel stream so far carried only quest-completed. This adds the
three remaining documented transitions (§3) so the panel UI can react
to reversals, full days, and nightly misses through the same ONE
publisher:

- core/events.py gains build_quest_missed_event, mirroring the
  sweep's hand-built payload exactly (shared _instance_payload shape),
  so the API plane publishes the same shape the integration sweep fires.
- api/transitions.py adds the reusable publish helpers the later
  uncomplete route (da0226b3) and the API-side missed sweep will call;
  payloads are always built by the core builders, never hand-built.
- The panel complete route now evaluates the day-complete rule on an
  actual transition, off the completed event's payload and the route's
  single clock read; the core builder still decides, so a zero-quest
  day and a non-today instance never fire it and a no-op re-complete
  publishes nothing.
- Tests drive all three transitions through the real SSE stream
  (server-loop scheduling for the not-yet-routed helpers) and assert
  every documented payload field.
This commit is contained in:
2026-09-22 23:20:17 -04:00
parent a07ce1b2f2
commit 8078ca1661
5 changed files with 311 additions and 6 deletions
+21 -1
View File
@@ -14,7 +14,9 @@ fires nothing), and ``nestquest_child_day_complete`` fires when a
completion clears the child's whole day (quests were owed and none
remain — a zero-quest day can never fire it, matching the all-done
binary sensor's rule). ``nestquest_quest_missed`` is the documented
contract Feature 11's nightly sweep fires; nothing here builds it.
contract Feature 11's nightly sweep fires; :func:`build_quest_missed_event`
builds it with exactly the payload the sweep hand-builds (§3), so the
API plane can publish the same shape without re-deriving it.
The completed-event and day-complete-event builders are split so the
shim can fire ``nestquest_quest_completed`` BEFORE it evaluates the
@@ -50,6 +52,7 @@ from .completion import derive_state
from .const import (
EVENT_CHILD_DAY_COMPLETE,
EVENT_QUEST_COMPLETED,
EVENT_QUEST_MISSED,
EVENT_QUEST_UNCOMPLETED,
)
from .dao_children import ChildrenDao
@@ -188,3 +191,20 @@ async def build_quest_uncompleted_events(
"""
payload = await _instance_payload(database, instance_id, _now_stamp())
return [(EVENT_QUEST_UNCOMPLETED, payload)]
async def build_quest_missed_event(
database: NestQuestDatabase, instance_id: int
) -> tuple[str, dict]:
"""Build the ``nestquest_quest_missed`` event for one instance.
The payload mirrors the nightly sweep's hand-built payload exactly
(the shared :func:`_instance_payload` shape: ``child_id``,
``child_name``, ``instance_id``, ``quest_title``, ``window``,
``due_date``, ``due_time``, ``occurred_at``), so the API plane's
missed path publishes the same event shape the integration's sweep
fires. Like the sweep, this builder never mutates the instance or
writes a completion event — it reads only.
"""
payload = await _instance_payload(database, instance_id, _now_stamp())
return (EVENT_QUEST_MISSED, payload)