Feature: UI bugs (panel hint, crest, wordmark, overdue clock, complete, meta) (#256)

Co-authored-by: dev_agent <17+dev_agent@code.cubecraftcreations.com>
Co-committed-by: dev_agent <17+dev_agent@code.cubecraftcreations.com>
This commit was merged in pull request #256.
This commit is contained in:
2026-09-26 10:22:01 -04:00
committed by approver_agent
parent bdf6d0aca1
commit d961d2e64f
18 changed files with 1477 additions and 668 deletions
+57 -13
View File
@@ -8,7 +8,8 @@ configured ``day_rollover_time`` — read fresh from the settings store
at the top of EVERY cycle, so an admin's settings change applies on
the next scheduling decision without a restart — then runs the
HA-free core's :func:`nestquest_core.sweep.run_missed_sweep` for the
API host's local date and publishes each returned
household-local date (the stored ``timezone`` setting; the API host's
local date when unset) and publishes each returned
``nestquest_quest_missed`` event on the app's ONE transition publisher
(the same SSE stream every route publishes through).
@@ -43,19 +44,56 @@ LOGGER = logging.getLogger(__name__)
_RETRY_BACKOFF_SECONDS = 60.0
def _delay_seconds(target: datetime.time, now: datetime.datetime) -> float:
"""Seconds from ``now`` until the next occurrence of ``target``.
#: Upper bound on the minute-by-minute search past a nonexistent wall
#: time; real DST gaps are an hour, the largest ever skipped a day.
_MAX_GAP_MINUTES = 2 * 24 * 60
``target`` is a plain ``HH:MM`` time of day: the next wall-clock
occurrence strictly AFTER ``now`` (an equal time schedules for
tomorrow, so the just-run sweep is never instantly re-run).
def _resolve_wall_time(
day: datetime.date, target: datetime.time, zone: datetime.tzinfo
) -> datetime.datetime:
"""The aware instant ``target`` (``HH:MM``) names on ``day`` in ``zone``.
A nonexistent wall time (inside a spring-forward gap) resolves to
the first valid wall minute at/after it — the gap's end, e.g. 02:30
on a 02:00→03:00 day becomes 03:00. An ambiguous wall time (the
repeated fall-back hour) resolves to its FIRST occurrence
(``fold=0``).
"""
scheduled = now.replace(
hour=target.hour, minute=target.minute, second=0, microsecond=0
wall = datetime.datetime.combine(
day, datetime.time(target.hour, target.minute)
)
if scheduled <= now:
scheduled += datetime.timedelta(days=1)
return (scheduled - now).total_seconds()
for _ in range(_MAX_GAP_MINUTES):
candidate = wall.replace(tzinfo=zone)
round_trip = candidate.astimezone(datetime.timezone.utc).astimezone(
zone
)
if round_trip.replace(tzinfo=None) == wall:
return candidate
wall += datetime.timedelta(minutes=1)
raise ValueError(f"no valid wall time at/after {target} on {day}")
def _delay_seconds(target: datetime.time, now: datetime.datetime) -> float:
"""Real seconds from ``now`` until the next occurrence of ``target``.
``target`` is a plain ``HH:MM`` household time of day, resolved in
``now``'s zone by :func:`_resolve_wall_time` (nonexistent → first
valid instant after the gap; ambiguous → first occurrence). The
result is the next such instant strictly AFTER ``now`` (an equal
time schedules for tomorrow, so the just-run sweep is never
instantly re-run). Both instants are compared and subtracted in
UTC: same-``tzinfo`` aware arithmetic is wall-clock arithmetic and
would be an hour off across a DST transition.
"""
now_utc = now.astimezone(datetime.timezone.utc)
day = now.date()
while True:
scheduled = _resolve_wall_time(day, target, now.tzinfo)
scheduled_utc = scheduled.astimezone(datetime.timezone.utc)
if scheduled_utc > now_utc:
return (scheduled_utc - now_utc).total_seconds()
day += datetime.timedelta(days=1)
class MissedSweepScheduler:
@@ -130,7 +168,11 @@ class MissedSweepScheduler:
)
await self._sleep(_RETRY_BACKOFF_SECONDS)
continue
delay = _delay_seconds(rollover, self._clock())
# The host may run UTC: the rollover is a household wall-clock
# time, so the clock read is re-expressed in the stored zone.
delay = _delay_seconds(
rollover, settings.household_now(self._clock())
)
LOGGER.info(
"NestQuest missed sweep scheduled for %s (%.0fs from now)",
settings.day_rollover_time,
@@ -138,7 +180,9 @@ class MissedSweepScheduler:
)
await self._sleep(delay)
try:
await self._run_once(self._clock().date())
await self._run_once(
settings.household_now(self._clock()).date()
)
except Exception:
# Not silent either: the sweep error is logged and the
# loop backs off before the next cycle, whose watermark