From d961d2e64f005444112b745775e4dfdbec9dc620 Mon Sep 17 00:00:00 2001 From: dev_agent <17+dev_agent@code.cubecraftcreations.com> Date: Sat, 26 Sep 2026 10:22:01 -0400 Subject: [PATCH] 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> --- admin/src/api/definitions.ts | 5 +- admin/src/settings/SettingsScreen.test.tsx | 2 +- api/routes_admin.py | 105 +- api/routes_panel.py | 27 +- api/scheduler.py | 70 +- custom_components/nestquest/core/const.py | 7 + custom_components/nestquest/core/settings.py | 43 + .../nestquest/core/settings_store.py | 9 +- custom_components/nestquest/core/sweep.py | 7 +- .../nestquest/www/nestquest-cards.js | 1141 +++++++++-------- design/PANEL-SPEC.md | 12 +- frontend/src/cards/party-board.ts | 104 +- frontend/src/cards/quest-log.ts | 6 +- frontend/tests/render-quest-log.mjs | 34 +- frontend/tsconfig.json | 1 + tests/test_api_admin_settings.py | 9 +- tests/test_api_household_clock.py | 309 +++++ tests/test_panel_cards.py | 254 ++++ 18 files changed, 1477 insertions(+), 668 deletions(-) create mode 100644 tests/test_api_household_clock.py diff --git a/admin/src/api/definitions.ts b/admin/src/api/definitions.ts index b38f83b..ce7a513 100644 --- a/admin/src/api/definitions.ts +++ b/admin/src/api/definitions.ts @@ -150,7 +150,10 @@ export async function updateDefinition( export const OCCURRENCES_PREVIEW_PATH = `${DEFINITIONS_PATH}/occurrences-preview`; export interface OccurrencesPreviewOptions { - /** "YYYY-MM-DD"; the API defaults to its host's today. */ + /** + * "YYYY-MM-DD"; the API defaults to the household-local today in the + * configured `timezone` (the API host's local time when that is empty). + */ startDate?: string; /** 1..50; the API defaults to 10. */ count?: number; diff --git a/admin/src/settings/SettingsScreen.test.tsx b/admin/src/settings/SettingsScreen.test.tsx index 94a0609..d687e62 100644 --- a/admin/src/settings/SettingsScreen.test.tsx +++ b/admin/src/settings/SettingsScreen.test.tsx @@ -370,7 +370,7 @@ describe("SettingsScreen — preferences", () => { fireEvent.click(within(form).getByRole("button", { name: "Save preferences" })); } - it("renders the current value of all ten fields", async () => { + it("renders the current value of the ten settings form fields (timezone is API-only)", async () => { const { form } = await renderPreferences(); expect(field(form, "Planning horizon (days)").value).toBe("14"); expect(field(form, "Day rollover").value).toBe("03:00"); diff --git a/api/routes_admin.py b/api/routes_admin.py index 23a8f1d..c67a269 100644 --- a/api/routes_admin.py +++ b/api/routes_admin.py @@ -165,10 +165,12 @@ integration's Home-Assistant config-entry options remain a temporary, SEPARATE source until Feature 18 wires the client to the API — the two are deliberately not unified here: -- ``GET /settings`` returns the current effective settings: all ten +- ``GET /settings`` returns the current effective settings: all eleven documented fields (the rolling horizon, the day rollover time, the - notify target, the three notification times and the four enable - toggles) — the defaults on a fresh database. + notify target, the three notification times, the four enable + toggles and the ``timezone`` — the IANA household zone the + household-local dates derive from, empty meaning host local) — the + defaults on a fresh database. - ``PATCH /settings`` updates ONLY the supplied fields: the body is a JSON object of settings field names to new values, passed STRAIGHT to :func:`nestquest_core.settings_store.update_settings` — the @@ -184,18 +186,19 @@ The missed-sweep trigger (task 058c7b69) follows the same pattern over :mod:`nestquest_core.sweep` — the HA-free sweep policy the integration's rollover listener shares: -- ``POST /missed-sweep`` runs the sweep for the API host's local - ``today`` (ONE :func:`_local_now` clock read threaded into the core - call) and publishes each returned ``(event_type, payload)`` pair - DIRECTLY on the app's ONE transition publisher — the sweep-built - payload, never re-fetched (the core commits its watermark before - returning, so a failed re-fetch after it would lose the missed event - forever), the same publish-what-the-core-built shape - api/scheduler.py uses. The watermark rule - (a same-day rerun announces nothing, a post-downtime run sweeps the - accumulated window once), the no-completion-event rule and the - read-only-over-the-domain-tables guarantee all live in the core; the - response reports how many transitions were published. +- ``POST /missed-sweep`` runs the sweep for the household-local + ``today`` (ONE :func:`_local_now` clock read re-expressed in the + configured household ``timezone`` — empty falls back to host local — + threaded into the core call) and publishes each returned + ``(event_type, payload)`` pair DIRECTLY on the app's ONE transition + publisher — the sweep-built payload, never re-fetched (the core + commits its watermark before returning, so a failed re-fetch after it + would lose the missed event forever), the same + publish-what-the-core-built shape api/scheduler.py uses. The + watermark rule (a same-day rerun announces nothing, a post-downtime + run sweeps the accumulated window once), the no-completion-event rule + and the read-only-over-the-domain-tables guarantee all live in the + core; the response reports how many transitions were published. The occurrence-preview route (task 5f843564) lets the admin PWA's Definitions edit sheet show the next dates a rule fires on WITHOUT @@ -205,9 +208,10 @@ reimplementing the recurrence rules client-side: ``rule`` object create/edit take (:class:`AdminRuleRequest`, built through the SAME :func:`_rule_from_request` / core ``from_dict`` validation, so a rejected rule is 422 exactly as on create), an - optional ``start_date`` (default: the API host's local ``today``, - ONE :func:`_local_now` clock read) and an optional ``count`` (1..50, - default 10). The dates come from ONE + optional ``start_date`` (default: the household-local ``today`` — + ONE :func:`_local_now` clock read re-expressed in the configured + household ``timezone``, empty falling back to host local) and an + optional ``count`` (1..50, default 10). The dates come from ONE :func:`nestquest_core.recurrence.occurrences_between` call — the same engine the materializer walks — so the preview can only agree with what would be generated. The scan is BOUNDED server-side: the @@ -532,9 +536,10 @@ class AdminOccurrencesPreviewRequest(BaseModel): ``rule`` is the SAME object create/edit take. ``start_date`` is an optional strict ``YYYY-MM-DD`` (validated by the core; default the - API host's today). ``count`` is a strict JSON integer in - 1..``_PREVIEW_MAX_COUNT`` — anything else is 422 before any handler - code runs. + household-local today in the configured ``timezone``, empty falling + back to the API host's local time). ``count`` is a strict JSON + integer in 1..``_PREVIEW_MAX_COUNT`` — anything else is 422 before + any handler code runs. """ rule: AdminRuleRequest @@ -868,7 +873,7 @@ class AdminSettingsResponse(BaseModel): """The effective settings as the admin plane serializes them. Mirrors the core :class:`~nestquest_core.settings.NestQuestSettings` - — all TEN documented fields, resolved (every absent stored field + — all ELEVEN documented fields, resolved (every absent stored field already fell back to its default in the core). The same shape both the GET and the PATCH route answer with. """ @@ -883,6 +888,9 @@ class AdminSettingsResponse(BaseModel): afternoon_reminder_enabled: bool end_of_day_report_enabled: bool celebration_enabled: bool + #: The household IANA time zone the API reads its clock in; ``""`` + #: means the API host's local time. + timezone: str class AdminMissedSweepResponse(BaseModel): @@ -1179,6 +1187,26 @@ def _local_now() -> datetime.datetime: return datetime.datetime.now().astimezone() +async def _household_clock( + database: object, +) -> tuple[object, datetime.datetime]: + """Return the stored settings and ONE household-local clock read. + + The API host may run UTC while the household does not, so the one + :func:`_local_now` read is re-expressed in the stored settings' + ``timezone`` (:meth:`NestQuestSettings.household_now`); an unset + zone keeps the host's local time. + """ + settings = await core_settings_store.load_settings(database) + return settings, settings.household_now(_local_now()) + + +async def _household_today(database: object) -> datetime.date: + """Return the household-local date of ONE :func:`_household_clock` read.""" + _settings, now = await _household_clock(database) + return now.date() + + def _rule_from_request(rule: AdminRuleRequest) -> core_recurrence.ScheduleRule: """Build the core ScheduleRule from the request's ``rule`` object. @@ -1497,7 +1525,7 @@ async def admin_create_quest_definition( response_model=AdminOccurrencesPreviewResponse, ) async def admin_preview_occurrences( - body: AdminOccurrencesPreviewRequest, + body: AdminOccurrencesPreviewRequest, request: Request ) -> AdminOccurrencesPreviewResponse: """Return the first ``count`` dates the supplied rule fires on. @@ -1510,12 +1538,15 @@ async def admin_preview_occurrences( call over the bounded window ``[start_date, start_date + 366 days]`` — ``start_date`` defaulting to ONE :func:`_local_now` clock read — truncated to ``count``. Every core ``ValueError`` is 422 - (:func:`_raise_preview_error`). No database access. + (:func:`_raise_preview_error`). The only database access is the + settings read behind the household clock. """ start_date = ( body.start_date if body.start_date is not None - else _local_now().date().isoformat() + else ( + await _household_today(request.app.state.db.database) + ).isoformat() ) try: rule = _rule_from_request(body.rule) @@ -1840,7 +1871,7 @@ async def admin_presence_override_consequence( body.start_date, body.end_date, body.is_present, - today=_local_now().date(), + today=await _household_today(state.database), ) ) except ValueError as error: @@ -1927,7 +1958,7 @@ async def admin_uncomplete_instance( """ state: DatabaseState = request.app.state.db database = state.database - now = _local_now() + _settings, now = await _household_clock(database) sub = claims.get("sub") try: result = await core_completion.uncomplete_instance( @@ -1993,7 +2024,7 @@ async def admin_regenerate( """ state: DatabaseState = request.app.state.db database = state.database - today = _local_now().date() + today = await _household_today(state.database) if body.scope == "household": end_date = today + datetime.timedelta( @@ -2078,7 +2109,7 @@ def _settings_response( """Serialize one core NestQuestSettings into the documented payload. Explicit field-by-field (not ``dataclasses.asdict``) so the payload - stays pinned to the ten documented fields even if the core + stays pinned to the eleven documented fields even if the core dataclass later grows another one. """ return AdminSettingsResponse( @@ -2092,6 +2123,7 @@ def _settings_response( afternoon_reminder_enabled=settings.afternoon_reminder_enabled, end_of_day_report_enabled=settings.end_of_day_report_enabled, celebration_enabled=settings.celebration_enabled, + timezone=settings.timezone, ) @@ -2123,7 +2155,7 @@ async def admin_history_query( ``ValueError`` is 422 (:func:`_raise_history_error`). """ state: DatabaseState = request.app.state.db - today = _local_now().date() + today = await _household_today(state.database) try: rows = await core_history.query_history( state.database, filter, start, end, today=today @@ -2166,7 +2198,7 @@ async def admin_history_csv( JSON route's (:func:`_raise_history_error`). """ state: DatabaseState = request.app.state.db - today = _local_now().date() + today = await _household_today(state.database) try: csv_text = await core_history.export_history_csv( state.database, filter, start, end, today=today @@ -2189,7 +2221,7 @@ async def admin_history_csv( response_model=AdminSettingsResponse, ) async def admin_get_settings(request: Request) -> AdminSettingsResponse: - """Return the current effective settings (all ten documented fields). + """Return the current effective settings (all eleven documented fields). Requires a valid admin JWT (the router's shared :func:`~api.auth.require_admin` dependency — the ONE check; this @@ -2254,7 +2286,9 @@ async def admin_run_missed_sweep( handler performs NO auth of its own). Thin adapter over the SAME HA-free sweep policy the integration's rollover listener runs (:mod:`nestquest_core.sweep`): ONE timezone-aware clock read - (:func:`_local_now`) pins the API host's local ``today``, and ONE + (:func:`_local_now`, re-expressed in the configured household + ``timezone``; empty falls back to host local) pins the + household-local ``today``, and ONE :func:`nestquest_core.sweep.run_missed_sweep` call does everything else — the past-due open-instance query, the documented payload build and the watermark update (a same-day rerun is an empty @@ -2281,7 +2315,7 @@ async def admin_run_missed_sweep( """ state: DatabaseState = request.app.state.db database = state.database - today = _local_now().date() + today = await _household_today(state.database) try: events = await core_sweep.run_missed_sweep(database, today=today) except ValueError as error: @@ -2310,8 +2344,9 @@ async def admin_snapshot(request: Request) -> AdminSnapshotResponse: :class:`AdminSnapshotResponse`. """ state: DatabaseState = request.app.state.db + settings, now = await _household_clock(state.database) snapshot = await core_snapshot.build_snapshot( - state.database, core_settings.NestQuestSettings(), _local_now() + state.database, settings, now ) return AdminSnapshotResponse( today_iso=snapshot.today_iso, diff --git a/api/routes_panel.py b/api/routes_panel.py index 6405384..200c431 100644 --- a/api/routes_panel.py +++ b/api/routes_panel.py @@ -8,9 +8,9 @@ this router inherits the check). The snapshot route is a thin adapter over the bundled core: it performs NO business logic. It reads the live database off ``app.state.db``, -resolves ONE timezone-aware ``now`` (the API host's local time — the -API is the household's "local" host now that the domain no longer lives -inside Home Assistant), calls the pure +resolves ONE timezone-aware ``now`` (the host clock re-expressed in the +stored household ``timezone`` setting — the API host may run UTC while +the household does not), calls the pure :func:`nestquest_core.snapshot.build_snapshot`, shapes each child's instances through :func:`nestquest_core.snapshot.instance_payload` with ``include_missed=False`` (D-009: missed instances are OMITTED from the @@ -63,7 +63,7 @@ from api.dependencies import require_panel_token from api.nestquest_core import ( core_completion, core_events, - core_settings, + core_settings_store, core_snapshot, ) from api import transitions as api_transitions @@ -178,6 +178,20 @@ def _local_now() -> datetime.datetime: return datetime.datetime.now().astimezone() +async def _household_clock( + database: object, +) -> tuple[object, datetime.datetime]: + """Return the stored settings and ONE household-local clock read. + + The API host may run UTC while the household does not, so the one + :func:`_local_now` read is re-expressed in the stored settings' + ``timezone`` (:meth:`NestQuestSettings.household_now`); an unset + zone keeps the host's local time. + """ + settings = await core_settings_store.load_settings(database) + return settings, settings.household_now(_local_now()) + + @router.get( "/snapshot", summary="Today's household snapshot", @@ -196,8 +210,9 @@ async def panel_snapshot(request: Request) -> PanelSnapshotResponse: :class:`PanelSnapshotResponse`. """ state: DatabaseState = request.app.state.db + settings, now = await _household_clock(state.database) snapshot = await core_snapshot.build_snapshot( - state.database, core_settings.NestQuestSettings(), _local_now() + state.database, settings, now ) return PanelSnapshotResponse( today_iso=snapshot.today_iso, @@ -282,7 +297,7 @@ async def panel_complete_instance( detail="Quest instance not found for this child", ) - now = _local_now() + _settings, now = await _household_clock(database) try: result = await core_completion.complete_instance( database, diff --git a/api/scheduler.py b/api/scheduler.py index 32dcc07..30498ea 100644 --- a/api/scheduler.py +++ b/api/scheduler.py @@ -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 diff --git a/custom_components/nestquest/core/const.py b/custom_components/nestquest/core/const.py index d8aba4b..5074190 100644 --- a/custom_components/nestquest/core/const.py +++ b/custom_components/nestquest/core/const.py @@ -112,6 +112,13 @@ CONF_AFTERNOON_REMINDER_ENABLED = "afternoon_reminder_enabled" CONF_END_OF_DAY_REPORT_ENABLED = "end_of_day_report_enabled" CONF_CELEBRATION_ENABLED = "celebration_enabled" DEFAULT_AUTOMATION_ENABLED = True +# The household's IANA time zone (e.g. ``America/New_York``) the API +# service reads its clock in. The empty string keeps the historical +# behaviour — the API host's own local time — so an existing install is +# unchanged until the owner sets it. The integration itself always uses +# Home Assistant's configured ``time_zone`` and ignores this field. +CONF_TIMEZONE = "timezone" +DEFAULT_TIMEZONE = "" # Panel-plane API client (Feature 18): the integration reaches the # NestQuest API service (Feature 16) over HTTP instead of the database. diff --git a/custom_components/nestquest/core/settings.py b/custom_components/nestquest/core/settings.py index c432335..1fd57a5 100644 --- a/custom_components/nestquest/core/settings.py +++ b/custom_components/nestquest/core/settings.py @@ -35,6 +35,7 @@ from collections.abc import Callable from dataclasses import dataclass from types import MappingProxyType from typing import Any, Mapping +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from .const import ( CONF_AFTERNOON_REMINDER_ENABLED, @@ -47,12 +48,14 @@ from .const import ( CONF_MORNING_SUMMARY_ENABLED, CONF_MORNING_SUMMARY_TIME, CONF_NOTIFY_TARGET, + CONF_TIMEZONE, DEFAULT_AFTERNOON_REMINDER_TIME, DEFAULT_AUTOMATION_ENABLED, DEFAULT_DAY_ROLLOVER_TIME, DEFAULT_END_OF_DAY_REPORT_TIME, DEFAULT_HORIZON_DAYS, DEFAULT_MORNING_SUMMARY_TIME, + DEFAULT_TIMEZONE, LOGGER, TIME_PATTERN, ) @@ -153,6 +156,29 @@ def _resolve_optional_str(value: Any) -> str: return value.strip() +def _resolve_timezone(value: Any) -> str: + """Validate a household time zone and return it. + + The empty string means "the API host's local time" (the historical + behaviour); anything else must be an IANA key :class:`ZoneInfo` + resolves (e.g. ``America/New_York``). A non-str (including + ``None``) is rejected; the ``None``-as-absent mapping lives ONLY in + :meth:`NestQuestSettings.from_options` (via :func:`_option_value`). + """ + if not isinstance(value, str): + raise ValueError(f"timezone must be a string, got {value!r}") + if value == "": + return value + try: + ZoneInfo(value) + except (ZoneInfoNotFoundError, ValueError) as error: + raise ValueError( + f"timezone must be an IANA time zone name such as " + f"'America/New_York', got {value!r}" + ) from error + return value + + #: The single field table the two builders share. Each row is #: ``(field_name, CONF_* key, default, validator)``. ``from_options`` #: applies every validator (raising on the first invalid value) while @@ -214,6 +240,7 @@ _FIELDS: tuple[tuple[str, str, Any, Callable[[Any], Any]], ...] = ( DEFAULT_AUTOMATION_ENABLED, _resolve_bool, ), + ("timezone", CONF_TIMEZONE, DEFAULT_TIMEZONE, _resolve_timezone), ) @@ -243,6 +270,7 @@ class NestQuestSettings: afternoon_reminder_enabled: bool = DEFAULT_AUTOMATION_ENABLED end_of_day_report_enabled: bool = DEFAULT_AUTOMATION_ENABLED celebration_enabled: bool = DEFAULT_AUTOMATION_ENABLED + timezone: str = DEFAULT_TIMEZONE def __post_init__(self) -> None: # Validate STRICTLY at construction so a hand-built instance @@ -281,6 +309,21 @@ class NestQuestSettings: raise ValueError( f"{name} must be a boolean, got {value!r}" ) + _resolve_timezone(self.timezone) + + def household_now(self, instant: datetime.datetime) -> datetime.datetime: + """Return the aware ``instant`` expressed in the household's zone. + + The API service has no Home Assistant ``time_zone``, so its one + clock read (an aware host-local ``now``) is converted here: with + :attr:`timezone` set the SAME instant is re-expressed in that + zone, so ``.date()`` and ``.time()`` are the household's wall + clock even when the host runs UTC. With :attr:`timezone` empty + the instant is returned unchanged (the host's local time). + """ + if not self.timezone: + return instant + return instant.astimezone(ZoneInfo(self.timezone)) @property def day_rollover_hour_minute(self) -> tuple[int, int]: diff --git a/custom_components/nestquest/core/settings_store.py b/custom_components/nestquest/core/settings_store.py index 9f9c91f..319b3d8 100644 --- a/custom_components/nestquest/core/settings_store.py +++ b/custom_components/nestquest/core/settings_store.py @@ -18,7 +18,7 @@ API client; the two are deliberately NOT unified here, and neither side reads the other's values. STORED SERIALIZATION — the document at ``SETTINGS_META_KEY`` is a JSON -object holding exactly the TEN settings fields, keyed by field name +object holding exactly the ELEVEN settings fields, keyed by field name (which equals the ``CONF_*`` option key for every field), complete after the first update: @@ -30,7 +30,10 @@ after the first update: core's normalizer), - ``morning_summary_enabled``, ``afternoon_reminder_enabled``, ``end_of_day_report_enabled``, ``celebration_enabled`` — real - booleans. + booleans, +- ``timezone`` — ``""`` (the API host's local time) or an IANA time + zone name such as ``America/New_York``. A document stored before + this field existed simply lacks it and resolves to ``""``. Only values whose core validators ACCEPTED are ever written, so a stored document always round-trips through the core's option builders. @@ -172,7 +175,7 @@ async def update_settings( update can never leave a half-applied document behind. The validated changes are merged onto the current effective - settings and the merged (complete, ten-field) document is persisted + settings and the merged (complete, eleven-field) document is persisted in one atomic upsert; the updated settings are returned. The whole read → merge → write span runs inside the per-database settings lock, so concurrent updates queue instead of interleaving. diff --git a/custom_components/nestquest/core/sweep.py b/custom_components/nestquest/core/sweep.py index 79f1977..b853cb8 100644 --- a/custom_components/nestquest/core/sweep.py +++ b/custom_components/nestquest/core/sweep.py @@ -40,8 +40,8 @@ announces nothing. The caller supplies ``today`` as a plain :class:`datetime.date`: the household-local calendar date, read from ``hass.config.time_zone`` by -the integration or from the API host's local clock by the API. This -module never reads a timezone database or config of its own. +the integration or from the configured household timezone by the API. +This module never reads a timezone database or config of its own. """ from __future__ import annotations @@ -98,7 +98,8 @@ async def run_missed_sweep( ``today`` is the household-local calendar date the sweep runs for (the caller owns the clock: the integration reads it from - ``hass.config.time_zone``, the API from its host's local time). + ``hass.config.time_zone``, the API from the configured household + timezone, falling back to its host's local time when unset). Returns the ``(event_type, payload)`` tuples to announce, EMPTY when the watermark already covers ``today`` (a same-night rerun) — the caller decides how to publish them and announces nothing on diff --git a/custom_components/nestquest/www/nestquest-cards.js b/custom_components/nestquest/www/nestquest-cards.js index 6f276d1..9706aa2 100644 --- a/custom_components/nestquest/www/nestquest-cards.js +++ b/custom_components/nestquest/www/nestquest-cards.js @@ -1,15 +1,15 @@ -const F = globalThis, se = F.ShadowRoot && (F.ShadyCSS === void 0 || F.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, ie = /* @__PURE__ */ Symbol(), ce = /* @__PURE__ */ new WeakMap(); -let Pe = class { +const F = globalThis, ce = F.ShadowRoot && (F.ShadyCSS === void 0 || F.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, de = /* @__PURE__ */ Symbol(), me = /* @__PURE__ */ new WeakMap(); +let Fe = class { constructor(e, t, n) { - if (this._$cssResult$ = !0, n !== ie) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); + if (this._$cssResult$ = !0, n !== de) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); this.cssText = e, this.t = t; } get styleSheet() { let e = this.o; const t = this.t; - if (se && e === void 0) { + if (ce && e === void 0) { const n = t !== void 0 && t.length === 1; - n && (e = ce.get(t)), e === void 0 && ((this.o = e = new CSSStyleSheet()).replaceSync(this.cssText), n && ce.set(t, e)); + n && (e = me.get(t)), e === void 0 && ((this.o = e = new CSSStyleSheet()).replaceSync(this.cssText), n && me.set(t, e)); } return e; } @@ -17,101 +17,101 @@ let Pe = class { return this.cssText; } }; -const v = (s) => new Pe(typeof s == "string" ? s : s + "", void 0, ie), Ie = (s, ...e) => { - const t = s.length === 1 ? s[0] : e.reduce((n, i, a) => n + ((r) => { +const x = (i) => new Fe(typeof i == "string" ? i : i + "", void 0, de), Ve = (i, ...e) => { + const t = i.length === 1 ? i[0] : e.reduce((n, s, a) => n + ((r) => { if (r._$cssResult$ === !0) return r.cssText; if (typeof r == "number") return r; throw Error("Value passed to 'css' function must be a 'css' function result: " + r + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security."); - })(i) + s[a + 1], s[0]); - return new Pe(t, s, ie); -}, We = (s, e) => { - if (se) s.adoptedStyleSheets = e.map((t) => t instanceof CSSStyleSheet ? t : t.styleSheet); + })(s) + i[a + 1], i[0]); + return new Fe(t, i, de); +}, tt = (i, e) => { + if (ce) i.adoptedStyleSheets = e.map((t) => t instanceof CSSStyleSheet ? t : t.styleSheet); else for (const t of e) { - const n = document.createElement("style"), i = F.litNonce; - i !== void 0 && n.setAttribute("nonce", i), n.textContent = t.cssText, s.appendChild(n); + const n = document.createElement("style"), s = F.litNonce; + s !== void 0 && n.setAttribute("nonce", s), n.textContent = t.cssText, i.appendChild(n); } -}, de = se ? (s) => s : (s) => s instanceof CSSStyleSheet ? ((e) => { +}, ge = ce ? (i) => i : (i) => i instanceof CSSStyleSheet ? ((e) => { let t = ""; for (const n of e.cssRules) t += n.cssText; - return v(t); -})(s) : s; -const { is: Ge, defineProperty: Ke, getOwnPropertyDescriptor: Je, getOwnPropertyNames: Xe, getOwnPropertySymbols: Ye, getPrototypeOf: et } = Object, Z = globalThis, pe = Z.trustedTypes, tt = pe ? pe.emptyScript : "", nt = Z.reactiveElementPolyfillSupport, P = (s, e) => s, ne = { toAttribute(s, e) { + return x(t); +})(i) : i; +const { is: nt, defineProperty: it, getOwnPropertyDescriptor: st, getOwnPropertyNames: at, getOwnPropertySymbols: rt, getPrototypeOf: ot } = Object, K = globalThis, _e = K.trustedTypes, lt = _e ? _e.emptyScript : "", ct = K.reactiveElementPolyfillSupport, I = (i, e) => i, ae = { toAttribute(i, e) { switch (e) { case Boolean: - s = s ? tt : null; + i = i ? lt : null; break; case Object: case Array: - s = s == null ? s : JSON.stringify(s); + i = i == null ? i : JSON.stringify(i); } - return s; -}, fromAttribute(s, e) { - let t = s; + return i; +}, fromAttribute(i, e) { + let t = i; switch (e) { case Boolean: - t = s !== null; + t = i !== null; break; case Number: - t = s === null ? null : Number(s); + t = i === null ? null : Number(i); break; case Object: case Array: try { - t = JSON.parse(s); + t = JSON.parse(i); } catch { t = null; } } return t; -} }, He = (s, e) => !Ge(s, e), he = { attribute: !0, type: String, converter: ne, reflect: !1, useDefault: !1, hasChanged: He }; -Symbol.metadata ??= /* @__PURE__ */ Symbol("metadata"), Z.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap(); -let T = class extends HTMLElement { +} }, Ze = (i, e) => !nt(i, e), ye = { attribute: !0, type: String, converter: ae, reflect: !1, useDefault: !1, hasChanged: Ze }; +Symbol.metadata ??= /* @__PURE__ */ Symbol("metadata"), K.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap(); +let z = class extends HTMLElement { static addInitializer(e) { this._$Ei(), (this.l ??= []).push(e); } static get observedAttributes() { return this.finalize(), this._$Eh && [...this._$Eh.keys()]; } - static createProperty(e, t = he) { + static createProperty(e, t = ye) { if (t.state && (t.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(e) && ((t = Object.create(t)).wrapped = !0), this.elementProperties.set(e, t), !t.noAccessor) { - const n = /* @__PURE__ */ Symbol(), i = this.getPropertyDescriptor(e, n, t); - i !== void 0 && Ke(this.prototype, e, i); + const n = /* @__PURE__ */ Symbol(), s = this.getPropertyDescriptor(e, n, t); + s !== void 0 && it(this.prototype, e, s); } } static getPropertyDescriptor(e, t, n) { - const { get: i, set: a } = Je(this.prototype, e) ?? { get() { + const { get: s, set: a } = st(this.prototype, e) ?? { get() { return this[t]; }, set(r) { this[t] = r; } }; - return { get: i, set(r) { - const l = i?.call(this); + return { get: s, set(r) { + const l = s?.call(this); a?.call(this, r), this.requestUpdate(e, l, n); }, configurable: !0, enumerable: !0 }; } static getPropertyOptions(e) { - return this.elementProperties.get(e) ?? he; + return this.elementProperties.get(e) ?? ye; } static _$Ei() { - if (this.hasOwnProperty(P("elementProperties"))) return; - const e = et(this); + if (this.hasOwnProperty(I("elementProperties"))) return; + const e = ot(this); e.finalize(), e.l !== void 0 && (this.l = [...e.l]), this.elementProperties = new Map(e.elementProperties); } static finalize() { - if (this.hasOwnProperty(P("finalized"))) return; - if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(P("properties"))) { - const t = this.properties, n = [...Xe(t), ...Ye(t)]; - for (const i of n) this.createProperty(i, t[i]); + if (this.hasOwnProperty(I("finalized"))) return; + if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(I("properties"))) { + const t = this.properties, n = [...at(t), ...rt(t)]; + for (const s of n) this.createProperty(s, t[s]); } const e = this[Symbol.metadata]; if (e !== null) { const t = litPropertyMetadata.get(e); - if (t !== void 0) for (const [n, i] of t) this.elementProperties.set(n, i); + if (t !== void 0) for (const [n, s] of t) this.elementProperties.set(n, s); } this._$Eh = /* @__PURE__ */ new Map(); for (const [t, n] of this.elementProperties) { - const i = this._$Eu(t, n); - i !== void 0 && this._$Eh.set(i, t); + const s = this._$Eu(t, n); + s !== void 0 && this._$Eh.set(s, t); } this.elementStyles = this.finalizeStyles(this.styles); } @@ -119,8 +119,8 @@ let T = class extends HTMLElement { const t = []; if (Array.isArray(e)) { const n = new Set(e.flat(1 / 0).reverse()); - for (const i of n) t.unshift(de(i)); - } else e !== void 0 && t.push(de(e)); + for (const s of n) t.unshift(ge(s)); + } else e !== void 0 && t.push(ge(e)); return t; } static _$Eu(e, t) { @@ -146,7 +146,7 @@ let T = class extends HTMLElement { } createRenderRoot() { const e = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); - return We(e, this.constructor.elementStyles), e; + return tt(e, this.constructor.elementStyles), e; } connectedCallback() { this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(!0), this._$EO?.forEach((e) => e.hostConnected?.()); @@ -160,31 +160,31 @@ let T = class extends HTMLElement { this._$AK(e, n); } _$ET(e, t) { - const n = this.constructor.elementProperties.get(e), i = this.constructor._$Eu(e, n); - if (i !== void 0 && n.reflect === !0) { - const a = (n.converter?.toAttribute !== void 0 ? n.converter : ne).toAttribute(t, n.type); - this._$Em = e, a == null ? this.removeAttribute(i) : this.setAttribute(i, a), this._$Em = null; + const n = this.constructor.elementProperties.get(e), s = this.constructor._$Eu(e, n); + if (s !== void 0 && n.reflect === !0) { + const a = (n.converter?.toAttribute !== void 0 ? n.converter : ae).toAttribute(t, n.type); + this._$Em = e, a == null ? this.removeAttribute(s) : this.setAttribute(s, a), this._$Em = null; } } _$AK(e, t) { - const n = this.constructor, i = n._$Eh.get(e); - if (i !== void 0 && this._$Em !== i) { - const a = n.getPropertyOptions(i), r = typeof a.converter == "function" ? { fromAttribute: a.converter } : a.converter?.fromAttribute !== void 0 ? a.converter : ne; - this._$Em = i; + const n = this.constructor, s = n._$Eh.get(e); + if (s !== void 0 && this._$Em !== s) { + const a = n.getPropertyOptions(s), r = typeof a.converter == "function" ? { fromAttribute: a.converter } : a.converter?.fromAttribute !== void 0 ? a.converter : ae; + this._$Em = s; const l = r.fromAttribute(t, a.type); - this[i] = l ?? this._$Ej?.get(i) ?? l, this._$Em = null; + this[s] = l ?? this._$Ej?.get(s) ?? l, this._$Em = null; } } - requestUpdate(e, t, n, i = !1, a) { + requestUpdate(e, t, n, s = !1, a) { if (e !== void 0) { const r = this.constructor; - if (i === !1 && (a = this[e]), n ??= r.getPropertyOptions(e), !((n.hasChanged ?? He)(a, t) || n.useDefault && n.reflect && a === this._$Ej?.get(e) && !this.hasAttribute(r._$Eu(e, n)))) return; + if (s === !1 && (a = this[e]), n ??= r.getPropertyOptions(e), !((n.hasChanged ?? Ze)(a, t) || n.useDefault && n.reflect && a === this._$Ej?.get(e) && !this.hasAttribute(r._$Eu(e, n)))) return; this.C(e, t, n); } this.isUpdatePending === !1 && (this._$ES = this._$EP()); } - C(e, t, { useDefault: n, reflect: i, wrapped: a }, r) { - n && !(this._$Ej ??= /* @__PURE__ */ new Map()).has(e) && (this._$Ej.set(e, r ?? t ?? this[e]), a !== !0 || r !== void 0) || (this._$AL.has(e) || (this.hasUpdated || n || (t = void 0), this._$AL.set(e, t)), i === !0 && this._$Em !== e && (this._$Eq ??= /* @__PURE__ */ new Set()).add(e)); + C(e, t, { useDefault: n, reflect: s, wrapped: a }, r) { + n && !(this._$Ej ??= /* @__PURE__ */ new Map()).has(e) && (this._$Ej.set(e, r ?? t ?? this[e]), a !== !0 || r !== void 0) || (this._$AL.has(e) || (this.hasUpdated || n || (t = void 0), this._$AL.set(e, t)), s === !0 && this._$Em !== e && (this._$Eq ??= /* @__PURE__ */ new Set()).add(e)); } async _$EP() { this.isUpdatePending = !0; @@ -203,13 +203,13 @@ let T = class extends HTMLElement { if (!this.isUpdatePending) return; if (!this.hasUpdated) { if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) { - for (const [i, a] of this._$Ep) this[i] = a; + for (const [s, a] of this._$Ep) this[s] = a; this._$Ep = void 0; } const n = this.constructor.elementProperties; - if (n.size > 0) for (const [i, a] of n) { - const { wrapped: r } = a, l = this[i]; - r !== !0 || this._$AL.has(i) || l === void 0 || this.C(i, void 0, a, l); + if (n.size > 0) for (const [s, a] of n) { + const { wrapped: r } = a, l = this[s]; + r !== !0 || this._$AL.has(s) || l === void 0 || this.C(s, void 0, a, l); } } let e = !1; @@ -246,54 +246,54 @@ let T = class extends HTMLElement { firstUpdated(e) { } }; -T.elementStyles = [], T.shadowRootOptions = { mode: "open" }, T[P("elementProperties")] = /* @__PURE__ */ new Map(), T[P("finalized")] = /* @__PURE__ */ new Map(), nt?.({ ReactiveElement: T }), (Z.reactiveElementVersions ??= []).push("2.1.2"); -const ae = globalThis, ue = (s) => s, Q = ae.trustedTypes, fe = Q ? Q.createPolicy("lit-html", { createHTML: (s) => s }) : void 0, je = "$lit$", k = `lit$${Math.random().toFixed(9).slice(2)}$`, Re = "?" + k, st = `<${Re}>`, A = document, I = () => A.createComment(""), H = (s) => s === null || typeof s != "object" && typeof s != "function", re = Array.isArray, it = (s) => re(s) || typeof s?.[Symbol.iterator] == "function", K = `[ -\f\r]`, L = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, me = /-->/g, ge = />/g, $ = RegExp(`>|${K}(?:([^\\s"'>=/]+)(${K}*=${K}*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`, "g"), _e = /'/g, ye = /"/g, Ue = /^(?:script|style|textarea|title)$/i, De = (s) => (e, ...t) => ({ _$litType$: s, strings: e, values: t }), d = De(1), be = De(2), S = /* @__PURE__ */ Symbol.for("lit-noChange"), g = /* @__PURE__ */ Symbol.for("lit-nothing"), ve = /* @__PURE__ */ new WeakMap(), C = A.createTreeWalker(A, 129); -function Fe(s, e) { - if (!re(s) || !s.hasOwnProperty("raw")) throw Error("invalid template strings array"); - return fe !== void 0 ? fe.createHTML(e) : e; +z.elementStyles = [], z.shadowRootOptions = { mode: "open" }, z[I("elementProperties")] = /* @__PURE__ */ new Map(), z[I("finalized")] = /* @__PURE__ */ new Map(), ct?.({ ReactiveElement: z }), (K.reactiveElementVersions ??= []).push("2.1.2"); +const pe = globalThis, be = (i) => i, V = pe.trustedTypes, ve = V ? V.createPolicy("lit-html", { createHTML: (i) => i }) : void 0, Qe = "$lit$", k = `lit$${Math.random().toFixed(9).slice(2)}$`, We = "?" + k, dt = `<${We}>`, A = document, P = () => A.createComment(""), H = (i) => i === null || typeof i != "object" && typeof i != "function", he = Array.isArray, pt = (i) => he(i) || typeof i?.[Symbol.iterator] == "function", Y = `[ +\f\r]`, L = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, xe = /-->/g, we = />/g, $ = RegExp(`>|${Y}(?:([^\\s"'>=/]+)(${Y}*=${Y}*(?:[^ +\f\r"'\`<>=]|("|')|))|$)`, "g"), ke = /'/g, $e = /"/g, Ge = /^(?:script|style|textarea|title)$/i, Ke = (i) => (e, ...t) => ({ _$litType$: i, strings: e, values: t }), d = Ke(1), qe = Ke(2), S = /* @__PURE__ */ Symbol.for("lit-noChange"), g = /* @__PURE__ */ Symbol.for("lit-nothing"), Me = /* @__PURE__ */ new WeakMap(), M = A.createTreeWalker(A, 129); +function Je(i, e) { + if (!he(i) || !i.hasOwnProperty("raw")) throw Error("invalid template strings array"); + return ve !== void 0 ? ve.createHTML(e) : e; } -const at = (s, e) => { - const t = s.length - 1, n = []; - let i, a = e === 2 ? "" : e === 3 ? "" : "", r = L; +const ht = (i, e) => { + const t = i.length - 1, n = []; + let s, a = e === 2 ? "" : e === 3 ? "" : "", r = L; for (let l = 0; l < t; l++) { - const o = s[l]; + const o = i[l]; let p, u, c = -1, f = 0; - for (; f < o.length && (r.lastIndex = f, u = r.exec(o), u !== null); ) f = r.lastIndex, r === L ? u[1] === "!--" ? r = me : u[1] !== void 0 ? r = ge : u[2] !== void 0 ? (Ue.test(u[2]) && (i = RegExp("" ? (r = i ?? L, c = -1) : u[1] === void 0 ? c = -2 : (c = r.lastIndex - u[2].length, p = u[1], r = u[3] === void 0 ? $ : u[3] === '"' ? ye : _e) : r === ye || r === _e ? r = $ : r === me || r === ge ? r = L : (r = $, i = void 0); - const h = r === $ && s[l + 1].startsWith("/>") ? " " : ""; - a += r === L ? o + st : c >= 0 ? (n.push(p), o.slice(0, c) + je + o.slice(c) + k + h) : o + k + (c === -2 ? l : h); + for (; f < o.length && (r.lastIndex = f, u = r.exec(o), u !== null); ) f = r.lastIndex, r === L ? u[1] === "!--" ? r = xe : u[1] !== void 0 ? r = we : u[2] !== void 0 ? (Ge.test(u[2]) && (s = RegExp("" ? (r = s ?? L, c = -1) : u[1] === void 0 ? c = -2 : (c = r.lastIndex - u[2].length, p = u[1], r = u[3] === void 0 ? $ : u[3] === '"' ? $e : ke) : r === $e || r === ke ? r = $ : r === xe || r === we ? r = L : (r = $, s = void 0); + const h = r === $ && i[l + 1].startsWith("/>") ? " " : ""; + a += r === L ? o + dt : c >= 0 ? (n.push(p), o.slice(0, c) + Qe + o.slice(c) + k + h) : o + k + (c === -2 ? l : h); } - return [Fe(s, a + (s[t] || "") + (e === 2 ? "" : e === 3 ? "" : "")), n]; + return [Je(i, a + (i[t] || "") + (e === 2 ? "" : e === 3 ? "" : "")), n]; }; -class j { +class U { constructor({ strings: e, _$litType$: t }, n) { - let i; + let s; this.parts = []; let a = 0, r = 0; - const l = e.length - 1, o = this.parts, [p, u] = at(e, t); - if (this.el = j.createElement(p, n), C.currentNode = this.el.content, t === 2 || t === 3) { + const l = e.length - 1, o = this.parts, [p, u] = ht(e, t); + if (this.el = U.createElement(p, n), M.currentNode = this.el.content, t === 2 || t === 3) { const c = this.el.content.firstChild; c.replaceWith(...c.childNodes); } - for (; (i = C.nextNode()) !== null && o.length < l; ) { - if (i.nodeType === 1) { - if (i.hasAttributes()) for (const c of i.getAttributeNames()) if (c.endsWith(je)) { - const f = u[r++], h = i.getAttribute(c).split(k), m = /([.?@])?(.*)/.exec(f); - o.push({ type: 1, index: a, name: m[2], strings: h, ctor: m[1] === "." ? ot : m[1] === "?" ? lt : m[1] === "@" ? ct : W }), i.removeAttribute(c); - } else c.startsWith(k) && (o.push({ type: 6, index: a }), i.removeAttribute(c)); - if (Ue.test(i.tagName)) { - const c = i.textContent.split(k), f = c.length - 1; + for (; (s = M.nextNode()) !== null && o.length < l; ) { + if (s.nodeType === 1) { + if (s.hasAttributes()) for (const c of s.getAttributeNames()) if (c.endsWith(Qe)) { + const f = u[r++], h = s.getAttribute(c).split(k), m = /([.?@])?(.*)/.exec(f); + o.push({ type: 1, index: a, name: m[2], strings: h, ctor: m[1] === "." ? ft : m[1] === "?" ? mt : m[1] === "@" ? gt : J }), s.removeAttribute(c); + } else c.startsWith(k) && (o.push({ type: 6, index: a }), s.removeAttribute(c)); + if (Ge.test(s.tagName)) { + const c = s.textContent.split(k), f = c.length - 1; if (f > 0) { - i.textContent = Q ? Q.emptyScript : ""; - for (let h = 0; h < f; h++) i.append(c[h], I()), C.nextNode(), o.push({ type: 2, index: ++a }); - i.append(c[f], I()); + s.textContent = V ? V.emptyScript : ""; + for (let h = 0; h < f; h++) s.append(c[h], P()), M.nextNode(), o.push({ type: 2, index: ++a }); + s.append(c[f], P()); } } - } else if (i.nodeType === 8) if (i.data === Re) o.push({ type: 2, index: a }); + } else if (s.nodeType === 8) if (s.data === We) o.push({ type: 2, index: a }); else { let c = -1; - for (; (c = i.data.indexOf(k, c + 1)) !== -1; ) o.push({ type: 7, index: a }), c += k.length - 1; + for (; (c = s.data.indexOf(k, c + 1)) !== -1; ) o.push({ type: 7, index: a }), c += k.length - 1; } a++; } @@ -303,13 +303,13 @@ class j { return n.innerHTML = e, n; } } -function z(s, e, t = s, n) { +function T(i, e, t = i, n) { if (e === S) return e; - let i = n !== void 0 ? t._$Co?.[n] : t._$Cl; + let s = n !== void 0 ? t._$Co?.[n] : t._$Cl; const a = H(e) ? void 0 : e._$litDirective$; - return i?.constructor !== a && (i?._$AO?.(!1), a === void 0 ? i = void 0 : (i = new a(s), i._$AT(s, t, n)), n !== void 0 ? (t._$Co ??= [])[n] = i : t._$Cl = i), i !== void 0 && (e = z(s, i._$AS(s, e.values), i, n)), e; + return s?.constructor !== a && (s?._$AO?.(!1), a === void 0 ? s = void 0 : (s = new a(i), s._$AT(i, t, n)), n !== void 0 ? (t._$Co ??= [])[n] = s : t._$Cl = s), s !== void 0 && (e = T(i, s._$AS(i, e.values), s, n)), e; } -class rt { +class ut { constructor(e, t) { this._$AV = [], this._$AN = void 0, this._$AD = e, this._$AM = t; } @@ -320,17 +320,17 @@ class rt { return this._$AM._$AU; } u(e) { - const { el: { content: t }, parts: n } = this._$AD, i = (e?.creationScope ?? A).importNode(t, !0); - C.currentNode = i; - let a = C.nextNode(), r = 0, l = 0, o = n[0]; + const { el: { content: t }, parts: n } = this._$AD, s = (e?.creationScope ?? A).importNode(t, !0); + M.currentNode = s; + let a = M.nextNode(), r = 0, l = 0, o = n[0]; for (; o !== void 0; ) { if (r === o.index) { let p; - o.type === 2 ? p = new N(a, a.nextSibling, this, e) : o.type === 1 ? p = new o.ctor(a, o.name, o.strings, this, e) : o.type === 6 && (p = new dt(a, this, e)), this._$AV.push(p), o = n[++l]; + o.type === 2 ? p = new N(a, a.nextSibling, this, e) : o.type === 1 ? p = new o.ctor(a, o.name, o.strings, this, e) : o.type === 6 && (p = new _t(a, this, e)), this._$AV.push(p), o = n[++l]; } - r !== o?.index && (a = C.nextNode(), r++); + r !== o?.index && (a = M.nextNode(), r++); } - return C.currentNode = A, i; + return M.currentNode = A, s; } p(e) { let t = 0; @@ -341,8 +341,8 @@ class N { get _$AU() { return this._$AM?._$AU ?? this._$Cv; } - constructor(e, t, n, i) { - this.type = 2, this._$AH = g, this._$AN = void 0, this._$AA = e, this._$AB = t, this._$AM = n, this.options = i, this._$Cv = i?.isConnected ?? !0; + constructor(e, t, n, s) { + this.type = 2, this._$AH = g, this._$AN = void 0, this._$AA = e, this._$AB = t, this._$AM = n, this.options = s, this._$Cv = s?.isConnected ?? !0; } get parentNode() { let e = this._$AA.parentNode; @@ -356,7 +356,7 @@ class N { return this._$AB; } _$AI(e, t = this) { - e = z(this, e, t), H(e) ? e === g || e == null || e === "" ? (this._$AH !== g && this._$AR(), this._$AH = g) : e !== this._$AH && e !== S && this._(e) : e._$litType$ !== void 0 ? this.$(e) : e.nodeType !== void 0 ? this.T(e) : it(e) ? this.k(e) : this._(e); + e = T(this, e, t), H(e) ? e === g || e == null || e === "" ? (this._$AH !== g && this._$AR(), this._$AH = g) : e !== this._$AH && e !== S && this._(e) : e._$litType$ !== void 0 ? this.$(e) : e.nodeType !== void 0 ? this.T(e) : pt(e) ? this.k(e) : this._(e); } O(e) { return this._$AA.parentNode.insertBefore(e, this._$AB); @@ -368,60 +368,60 @@ class N { this._$AH !== g && H(this._$AH) ? this._$AA.nextSibling.data = e : this.T(A.createTextNode(e)), this._$AH = e; } $(e) { - const { values: t, _$litType$: n } = e, i = typeof n == "number" ? this._$AC(e) : (n.el === void 0 && (n.el = j.createElement(Fe(n.h, n.h[0]), this.options)), n); - if (this._$AH?._$AD === i) this._$AH.p(t); + const { values: t, _$litType$: n } = e, s = typeof n == "number" ? this._$AC(e) : (n.el === void 0 && (n.el = U.createElement(Je(n.h, n.h[0]), this.options)), n); + if (this._$AH?._$AD === s) this._$AH.p(t); else { - const a = new rt(i, this), r = a.u(this.options); + const a = new ut(s, this), r = a.u(this.options); a.p(t), this.T(r), this._$AH = a; } } _$AC(e) { - let t = ve.get(e.strings); - return t === void 0 && ve.set(e.strings, t = new j(e)), t; + let t = Me.get(e.strings); + return t === void 0 && Me.set(e.strings, t = new U(e)), t; } k(e) { - re(this._$AH) || (this._$AH = [], this._$AR()); + he(this._$AH) || (this._$AH = [], this._$AR()); const t = this._$AH; - let n, i = 0; - for (const a of e) i === t.length ? t.push(n = new N(this.O(I()), this.O(I()), this, this.options)) : n = t[i], n._$AI(a), i++; - i < t.length && (this._$AR(n && n._$AB.nextSibling, i), t.length = i); + let n, s = 0; + for (const a of e) s === t.length ? t.push(n = new N(this.O(P()), this.O(P()), this, this.options)) : n = t[s], n._$AI(a), s++; + s < t.length && (this._$AR(n && n._$AB.nextSibling, s), t.length = s); } _$AR(e = this._$AA.nextSibling, t) { for (this._$AP?.(!1, !0, t); e !== this._$AB; ) { - const n = ue(e).nextSibling; - ue(e).remove(), e = n; + const n = be(e).nextSibling; + be(e).remove(), e = n; } } setConnected(e) { this._$AM === void 0 && (this._$Cv = e, this._$AP?.(e)); } } -class W { +class J { get tagName() { return this.element.tagName; } get _$AU() { return this._$AM._$AU; } - constructor(e, t, n, i, a) { - this.type = 1, this._$AH = g, this._$AN = void 0, this.element = e, this.name = t, this._$AM = i, this.options = a, n.length > 2 || n[0] !== "" || n[1] !== "" ? (this._$AH = Array(n.length - 1).fill(new String()), this.strings = n) : this._$AH = g; + constructor(e, t, n, s, a) { + this.type = 1, this._$AH = g, this._$AN = void 0, this.element = e, this.name = t, this._$AM = s, this.options = a, n.length > 2 || n[0] !== "" || n[1] !== "" ? (this._$AH = Array(n.length - 1).fill(new String()), this.strings = n) : this._$AH = g; } - _$AI(e, t = this, n, i) { + _$AI(e, t = this, n, s) { const a = this.strings; let r = !1; - if (a === void 0) e = z(this, e, t, 0), r = !H(e) || e !== this._$AH && e !== S, r && (this._$AH = e); + if (a === void 0) e = T(this, e, t, 0), r = !H(e) || e !== this._$AH && e !== S, r && (this._$AH = e); else { const l = e; let o, p; - for (e = a[0], o = 0; o < a.length - 1; o++) p = z(this, l[n + o], t, o), p === S && (p = this._$AH[o]), r ||= !H(p) || p !== this._$AH[o], p === g ? e = g : e !== g && (e += (p ?? "") + a[o + 1]), this._$AH[o] = p; + for (e = a[0], o = 0; o < a.length - 1; o++) p = T(this, l[n + o], t, o), p === S && (p = this._$AH[o]), r ||= !H(p) || p !== this._$AH[o], p === g ? e = g : e !== g && (e += (p ?? "") + a[o + 1]), this._$AH[o] = p; } - r && !i && this.j(e); + r && !s && this.j(e); } j(e) { e === g ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, e ?? ""); } } -class ot extends W { +class ft extends J { constructor() { super(...arguments), this.type = 3; } @@ -429,7 +429,7 @@ class ot extends W { this.element[this.name] = e === g ? void 0 : e; } } -class lt extends W { +class mt extends J { constructor() { super(...arguments), this.type = 4; } @@ -437,20 +437,20 @@ class lt extends W { this.element.toggleAttribute(this.name, !!e && e !== g); } } -class ct extends W { - constructor(e, t, n, i, a) { - super(e, t, n, i, a), this.type = 5; +class gt extends J { + constructor(e, t, n, s, a) { + super(e, t, n, s, a), this.type = 5; } _$AI(e, t = this) { - if ((e = z(this, e, t, 0) ?? g) === S) return; - const n = this._$AH, i = e === g && n !== g || e.capture !== n.capture || e.once !== n.once || e.passive !== n.passive, a = e !== g && (n === g || i); - i && this.element.removeEventListener(this.name, this, n), a && this.element.addEventListener(this.name, this, e), this._$AH = e; + if ((e = T(this, e, t, 0) ?? g) === S) return; + const n = this._$AH, s = e === g && n !== g || e.capture !== n.capture || e.once !== n.once || e.passive !== n.passive, a = e !== g && (n === g || s); + s && this.element.removeEventListener(this.name, this, n), a && this.element.addEventListener(this.name, this, e), this._$AH = e; } handleEvent(e) { typeof this._$AH == "function" ? this._$AH.call(this.options?.host ?? this.element, e) : this._$AH.handleEvent(e); } } -class dt { +class _t { constructor(e, t, n) { this.element = e, this.type = 6, this._$AN = void 0, this._$AM = t, this.options = n; } @@ -458,22 +458,22 @@ class dt { return this._$AM._$AU; } _$AI(e) { - z(this, e); + T(this, e); } } -const pt = { I: N }, ht = ae.litHtmlPolyfillSupport; -ht?.(j, N), (ae.litHtmlVersions ??= []).push("3.3.3"); -const ut = (s, e, t) => { +const yt = { I: N }, bt = pe.litHtmlPolyfillSupport; +bt?.(U, N), (pe.litHtmlVersions ??= []).push("3.3.3"); +const vt = (i, e, t) => { const n = t?.renderBefore ?? e; - let i = n._$litPart$; - if (i === void 0) { + let s = n._$litPart$; + if (s === void 0) { const a = t?.renderBefore ?? null; - n._$litPart$ = i = new N(e.insertBefore(I(), a), a, void 0, t ?? {}); + n._$litPart$ = s = new N(e.insertBefore(P(), a), a, void 0, t ?? {}); } - return i._$AI(s), i; + return s._$AI(i), s; }; -const oe = globalThis; -let M = class extends T { +const ue = globalThis; +let C = class extends z { constructor() { super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; } @@ -483,7 +483,7 @@ let M = class extends T { } update(e) { const t = this.render(); - this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(e), this._$Do = ut(t, this.renderRoot, this.renderOptions); + this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(e), this._$Do = vt(t, this.renderRoot, this.renderOptions); } connectedCallback() { super.connectedCallback(), this._$Do?.setConnected(!0); @@ -495,53 +495,53 @@ let M = class extends T { return S; } }; -M._$litElement$ = !0, M.finalized = !0, oe.litElementHydrateSupport?.({ LitElement: M }); -const ft = oe.litElementPolyfillSupport; -ft?.({ LitElement: M }); -(oe.litElementVersions ??= []).push("4.2.2"); -const Qe = ':host{--nq-p-parchment-top: #f0e4c7;--nq-p-parchment-bottom: #ddcca3;--nq-p-parchment: radial-gradient(ellipse at 25% 10%, rgba(255,255,255,.5), transparent 55%), radial-gradient(ellipse at 85% 90%, rgba(120,86,44,.28), transparent 60%), repeating-linear-gradient(93deg, rgba(150,115,70,.05) 0 2px, transparent 2px 6px), repeating-linear-gradient(2deg, rgba(150,115,70,.04) 0 3px, transparent 3px 7px), linear-gradient(var(--nq-p-parchment-top), var(--nq-p-parchment-bottom));--nq-p-vignette: inset 0 0 200px rgba(80,52,20,.35);--nq-p-card-open: linear-gradient(#fdf7e6, #f2e7c9);--nq-p-card-done: linear-gradient(#f0e8d3, #e6dcc2);--nq-p-card-panel: linear-gradient(#fcf6e6, #f1e6ca);--nq-p-card-border: rgba(120,88,48,.38);--nq-p-panel-border: rgba(92,62,26,.4);--nq-p-card-shadow: 0 3px 0 rgba(120,88,48,.2), inset 0 1px 0 rgba(255,255,255,.7);--nq-p-panel-shadow: 0 6px 0 rgba(92,62,26,.18), inset 0 2px 0 rgba(255,255,255,.7);--nq-p-frame-outer: 2px solid rgba(92,62,26,.45);--nq-p-frame-inner: 1px solid rgba(92,62,26,.28);--nq-p-rule: 2px solid rgba(92,62,26,.35);--nq-p-ink: #2b1f14;--nq-p-ink-secondary: #5c452a;--nq-p-ink-muted: #6f6455;--nq-p-ink-away: #4d4433;--nq-p-ink-late: #8f1526;--nq-brand-blue: #3B3AB8;--nq-brand-purple: #A035CC;--nq-brand-gradient: linear-gradient(135deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-p-icon-tile: var(--nq-brand-gradient);--nq-p-icon-tile-done: rgba(92,62,26,.16);--nq-p-crest: var(--nq-brand-gradient);--nq-p-crest-away: linear-gradient(135deg, #6b6b7a, #7a7286);--nq-p-seal: radial-gradient(circle at 35% 30%, #a8283a, #6d1322);--nq-p-seal-shadow: 0 4px 10px rgba(60,10,20,.4), inset 0 0 0 4px rgba(255,255,255,.14);--nq-p-seal-size: 78px;--nq-p-dock-bg: rgba(43,31,20,.9);--nq-p-dock-ink: #f7efdb;--nq-p-dock-ink-secondary: #d8c9a6;--nq-p-dock-divider: rgba(233,220,189,.3);--nq-p-dock-height: 84px;--nq-p-font-display: "Cinzel Decorative", Cinzel, serif;--nq-p-font-heading: Cinzel, serif;--nq-p-font-body: Nunito, system-ui, sans-serif;--nq-p-size-hero: 86px;--nq-p-size-wordmark: 66px;--nq-p-size-title: 56px;--nq-p-size-name: 48px;--nq-p-size-section: 32px;--nq-p-size-quest: 30px;--nq-p-size-body: 22px;--nq-p-size-label: 21px;--nq-p-track-label: .14em;--nq-p-track-kicker: .3em;--nq-p-quest-min-height: 116px;--nq-p-quest-gap: 24px;--nq-p-quest-pad: 22px;--nq-p-tile-size: 64px;--nq-p-button-height: 72px;--nq-p-button-min-width: 150px;--nq-p-confirm-button: 108px;--nq-p-radius-card: 12px;--nq-p-radius-panel: 20px;--nq-p-radius-pill: 9999px;--nq-p-page-inset: 62px;--nq-p-frame-inset: 26px;--nq-ease-out: cubic-bezier(0, 0, .2, 1);--nq-dur-micro: .12s;--nq-dur-base: .2s;--nq-dur-modal: .35s}'; -function le(s) { - window.customCards = window.customCards ?? [], window.customCards.push(s); +C._$litElement$ = !0, C.finalized = !0, ue.litElementHydrateSupport?.({ LitElement: C }); +const xt = ue.litElementPolyfillSupport; +xt?.({ LitElement: C }); +(ue.litElementVersions ??= []).push("4.2.2"); +const Xe = ':host{--nq-p-parchment-top: #f0e4c7;--nq-p-parchment-bottom: #ddcca3;--nq-p-parchment: radial-gradient(ellipse at 25% 10%, rgba(255,255,255,.5), transparent 55%), radial-gradient(ellipse at 85% 90%, rgba(120,86,44,.28), transparent 60%), repeating-linear-gradient(93deg, rgba(150,115,70,.05) 0 2px, transparent 2px 6px), repeating-linear-gradient(2deg, rgba(150,115,70,.04) 0 3px, transparent 3px 7px), linear-gradient(var(--nq-p-parchment-top), var(--nq-p-parchment-bottom));--nq-p-vignette: inset 0 0 200px rgba(80,52,20,.35);--nq-p-card-open: linear-gradient(#fdf7e6, #f2e7c9);--nq-p-card-done: linear-gradient(#f0e8d3, #e6dcc2);--nq-p-card-panel: linear-gradient(#fcf6e6, #f1e6ca);--nq-p-card-border: rgba(120,88,48,.38);--nq-p-panel-border: rgba(92,62,26,.4);--nq-p-card-shadow: 0 3px 0 rgba(120,88,48,.2), inset 0 1px 0 rgba(255,255,255,.7);--nq-p-panel-shadow: 0 6px 0 rgba(92,62,26,.18), inset 0 2px 0 rgba(255,255,255,.7);--nq-p-frame-outer: 2px solid rgba(92,62,26,.45);--nq-p-frame-inner: 1px solid rgba(92,62,26,.28);--nq-p-rule: 2px solid rgba(92,62,26,.35);--nq-p-ink: #2b1f14;--nq-p-ink-secondary: #5c452a;--nq-p-ink-muted: #6f6455;--nq-p-ink-away: #4d4433;--nq-p-ink-late: #8f1526;--nq-brand-blue: #3B3AB8;--nq-brand-purple: #A035CC;--nq-brand-gradient: linear-gradient(135deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-p-icon-tile: var(--nq-brand-gradient);--nq-p-icon-tile-done: rgba(92,62,26,.16);--nq-p-crest: var(--nq-brand-gradient);--nq-p-crest-away: linear-gradient(135deg, #6b6b7a, #7a7286);--nq-p-seal: radial-gradient(circle at 35% 30%, #a8283a, #6d1322);--nq-p-seal-shadow: 0 4px 10px rgba(60,10,20,.4), inset 0 0 0 4px rgba(255,255,255,.14);--nq-p-seal-size: 78px;--nq-p-dock-bg: rgba(43,31,20,.9);--nq-p-dock-ink: #f7efdb;--nq-p-dock-ink-secondary: #d8c9a6;--nq-p-dock-divider: rgba(233,220,189,.3);--nq-p-dock-height: 84px;--nq-p-font-display: "Cinzel Decorative", Cinzel, serif;--nq-p-font-heading: Cinzel, serif;--nq-p-font-body: Nunito, system-ui, sans-serif;--nq-p-size-hero: 86px;--nq-p-size-wordmark: 66px;--nq-p-size-title: 56px;--nq-p-size-name: 48px;--nq-p-size-section: 32px;--nq-p-size-quest: 30px;--nq-p-size-body: 22px;--nq-p-size-label: 21px;--nq-p-track-label: .14em;--nq-p-track-kicker: .3em;--nq-p-quest-min-height: 116px;--nq-p-quest-gap: 24px;--nq-p-quest-pad: 22px;--nq-p-tile-size: 64px;--nq-p-button-height: 72px;--nq-p-button-min-width: 150px;--nq-p-confirm-button: 108px;--nq-p-radius-card: 12px;--nq-p-radius-panel: 20px;--nq-p-radius-pill: 9999px;--nq-p-page-inset: 62px;--nq-p-frame-inset: 26px;--nq-ease-out: cubic-bezier(0, 0, .2, 1);--nq-dur-micro: .12s;--nq-dur-base: .2s;--nq-dur-modal: .35s}'; +function fe(i) { + window.customCards = window.customCards ?? [], window.customCards.push(i); } -const mt = [ +const wt = [ "nestquest_quest_completed", "nestquest_quest_uncompleted", "nestquest_quest_missed", "nestquest_child_day_complete" -], xe = /* @__PURE__ */ new Map(); -function V(s, e) { - const t = `${s ?? "local"}|${JSON.stringify(e)}`, n = xe.get(t); +], Ce = /* @__PURE__ */ new Map(); +function Z(i, e) { + const t = `${i ?? "local"}|${JSON.stringify(e)}`, n = Ce.get(t); if (n) return n; - let i; + let s; try { - i = new Intl.DateTimeFormat("en-US", { + s = new Intl.DateTimeFormat("en-US", { ...e, - ...s ? { timeZone: s } : {} + ...i ? { timeZone: i } : {} }); } catch { - i = new Intl.DateTimeFormat("en-US", e); + s = new Intl.DateTimeFormat("en-US", e); } - return xe.set(t, i), i; + return Ce.set(t, s), s; } -function gt(s) { - const e = s?.config; +function kt(i) { + const e = i?.config; if (!e || typeof e != "object") return; const t = e.time_zone; return typeof t == "string" && t.trim() ? t.trim() : void 0; } -function J(s, e) { - return `${V(e, { weekday: "long" }).format(s)}, ${V(e, { month: "short", day: "numeric" }).format(s)}`; +function ee(i, e) { + return `${Z(e, { weekday: "long" }).format(i)}, ${Z(e, { month: "short", day: "numeric" }).format(i)}`; } -function _t(s, e) { - return V(e, { +function $t(i, e) { + return Z(e, { hour: "numeric", minute: "2-digit", hour12: !0 - }).format(s); + }).format(i); } -const yt = { +const qt = { "clear-night": "Clear", cloudy: "Cloudy", exceptional: "Clear", @@ -557,7 +557,7 @@ const yt = { sunny: "Sunny", windy: "Windy", "windy-variant": "Windy" -}, bt = { +}, Mt = { "clear-night": "Clear night skies", cloudy: "Grey skies today", exceptional: "A striking day", @@ -574,32 +574,32 @@ const yt = { windy: "A blustery day", "windy-variant": "A blustery day" }; -function we(s) { - return typeof s == "string" ? s.trim() : ""; +function Ae(i) { + return typeof i == "string" ? i.trim() : ""; } -function X(s, e) { - if (s == null || s === "") +function te(i, e) { + if (i == null || i === "") return e; - const t = typeof s == "number" ? s : Number(s); + const t = typeof i == "number" ? i : Number(i); return Number.isFinite(t) ? t : e; } -function vt(s) { - if (s === null || typeof s != "object" || Array.isArray(s)) +function Ct(i) { + if (i === null || typeof i != "object" || Array.isArray(i)) return ""; - const e = s.slug; + const e = i.slug; return typeof e == "string" ? e.trim() : ""; } -function ke(s) { - return Number.isFinite(s) ? Math.min(100, Math.max(0, s)) : 0; +function Se(i) { + return Number.isFinite(i) ? Math.min(100, Math.max(0, i)) : 0; } -function $e(s) { - return Math.max(0, Math.round(s)); +function ze(i) { + return Math.max(0, Math.round(i)); } -function xt(s) { - return s.split(/[-_]+/).filter(Boolean).map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join(" "); +function At(i) { + return i.split(/[-_]+/).filter(Boolean).map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join(" "); } -function wt(s, e) { - const t = V(e, { +function St(i, e) { + const t = Z(e, { year: "numeric", month: "2-digit", day: "2-digit", @@ -607,10 +607,10 @@ function wt(s, e) { minute: "2-digit", second: "2-digit", hourCycle: "h23" - }).formatToParts(new Date(s)), n = (a) => { + }).formatToParts(new Date(i)), n = (a) => { const r = t.find((l) => l.type === a); return r ? Number(r.value) : Number.NaN; - }, i = Date.UTC( + }, s = Date.UTC( n("year"), n("month") - 1, n("day"), @@ -618,32 +618,85 @@ function wt(s, e) { n("minute"), n("second") ); - return Number.isFinite(i) ? i - s : 0; + return Number.isFinite(s) ? s - i : 0; } -function kt(s, e) { - if (typeof s != "string") +function zt(i, e) { + if (typeof i != "string") return null; - const t = s.split("-"); + const t = i.split("-"); if (t.length !== 3) return null; - const [n, i, a] = t.map((o) => Number(o)); - if (!n || !i || !a) + const [n, s, a] = t.map((o) => Number(o)); + if (!n || !s || !a) return null; if (!e) { - const o = new Date(n, i - 1, a); + const o = new Date(n, s - 1, a); return Number.isNaN(o.getTime()) ? null : o; } - const r = Date.UTC(n, i - 1, a), l = new Date(r - wt(r, e)); + const r = Date.UTC(n, s - 1, a), l = new Date(r - St(r, e)); return Number.isNaN(l.getTime()) ? null : l; } -function $t(s) { - const e = yt[s]; - return e || s.charAt(0).toUpperCase() + s.slice(1); +function Tt(i) { + const e = qt[i]; + return e || i.charAt(0).toUpperCase() + i.slice(1); } -function qt(s, e) { - return bt[s] ?? e; +function Et(i, e) { + return Mt[i] ?? e; } -const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ct = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", Mt = Ie` +const Te = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ee = "M50 3 44.5 24.1 50 26 55.5 24.1Z M100 50 75.9 44.5 74 50 75.9 55.5Z M50 97 55.5 75.9 50 74 44.5 75.9Z M0 50 24.1 55.5 26 50 24.1 44.5Z M29.1 29.1 29.7 36.3 36.3 29.7Z M70.9 29.1 63.7 29.7 70.3 36.3Z M70.9 70.9 70.3 63.7 63.7 70.3Z M29.1 70.9 36.3 70.3 29.7 63.7Z", Nt = "M50 26.5 70.4 38.2 70.4 61.8 50 73.5 29.6 61.8 29.6 38.2Z", Lt = "M29.6 38.2H70.4M29.6 61.8H70.4M50 26.5 36.5 38.2M50 26.5 63.5 38.2M50 73.5 36.5 61.8M50 73.5 63.5 61.8M36.5 38.2V61.8M63.5 38.2V61.8M29.6 38.2 36.5 61.8M36.5 38.2 29.6 61.8M70.4 38.2 63.5 61.8M63.5 38.2 70.4 61.8", Ot = d``, Bt = Ve` * { box-sizing: border-box; } @@ -708,23 +761,11 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ct = "polygon(50% background: rgba(92, 62, 26, 0.35); } - .d20 { + .monogram { flex: none; - display: inline-flex; - align-items: center; - justify-content: center; - width: 34px; - height: 34px; - clip-path: ${v(Ct)}; - background: var(--nq-brand-gradient); - } - - .d20-numeral { - font-family: var(--nq-p-font-heading); - font-size: 15px; - font-weight: 700; - line-height: 1; - color: #ffffff; + display: block; + width: 80px; + height: 80px; } .wordmark { @@ -809,7 +850,7 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ct = "polygon(50% width: 188px; height: 214px; padding: 4px; - clip-path: ${v(qe)}; + clip-path: ${x(Te)}; background: rgba(255, 255, 255, 0.22); } @@ -820,7 +861,7 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ct = "polygon(50% align-items: center; justify-content: center; padding-bottom: 70px; - clip-path: ${v(qe)}; + clip-path: ${x(Te)}; background: var(--nq-p-crest); } @@ -906,11 +947,12 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ct = "polygon(50% .hint { position: absolute; - top: 930px; + top: 896px; left: 0; right: 0; + margin: 0; font-family: var(--nq-p-font-body); - font-size: 24px; + font-size: 27px; font-weight: 600; line-height: 1.4; text-align: center; @@ -1038,7 +1080,7 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ct = "polygon(50% font-weight: 800; line-height: 1; } -`, At = d` -`, St = d``, Pt = d` -`, Tt = d``, Ht = d` -`, zt = d``, Ut = d` -`, Et = d``, Rt = d` -`, Ce = { - icon: zt, +`, Ne = { + icon: Ut, headline: "NestQuest is not set up yet", body: "A parent needs to finish setting up NestQuest before the party can gather." -}, Nt = { - icon: Et, +}, jt = { + icon: Rt, headline: "The records cannot be reached", body: "The party's records are quiet right now. NestQuest will return shortly." -}, Lt = d` -`; -class Bt extends M { - static properties = { - hass: { attribute: !1 }, - _config: { state: !0 }, - _now: { state: !0 } - }; - static styles = [v(Qe), Mt]; - hass; - _config; - _now = /* @__PURE__ */ new Date(); - _clockTimer; - _unsubs = []; - _subscribed = !1; - _subGeneration = 0; - _retryTimer; +`, Q = class Q extends C { + constructor() { + super(...arguments), this._now = /* @__PURE__ */ new Date(), this._unsubs = [], this._subscribed = !1, this._subGeneration = 0; + } setConfig(e) { if (!e || typeof e != "object") throw new Error("Invalid configuration"); @@ -1149,16 +1179,16 @@ class Bt extends M { if (!e) return; this._subscribed = !0; - const t = ++this._subGeneration, n = (i) => { - i.then((a) => { + const t = ++this._subGeneration, n = (s) => { + s.then((a) => { this._subscribed && t === this._subGeneration ? this._unsubs.push(a) : a(); }).catch(() => { t === this._subGeneration && (this._unsubscribeLive(), this._armSubscribeRetry()); }); }; - for (const i of mt) + for (const s of wt) n( - e.subscribeEvents(() => this.requestUpdate(), i) + e.subscribeEvents(() => this.requestUpdate(), s) ); } _armSubscribeRetry() { @@ -1197,16 +1227,11 @@ class Bt extends M {
- + ${Ot} NestQuest -
-

The Party · ${J(this._now, this._timeZone())}

+

The Party · ${ee(this._now, this._timeZone())}

${e ? this._renderNotice(e) : d`
${this._plates().map((t) => this._renderPlate(t))} @@ -1219,7 +1244,7 @@ class Bt extends M { `; } _timeZone() { - return gt(this.hass); + return kt(this.hass); } _state(e) { const n = this.hass?.states?.[e]; @@ -1243,19 +1268,19 @@ class Bt extends M { const t = this._state( "sensor.nestquest_household_quests_due_today" )?.attributes?.child_roster; - return Array.isArray(t) ? t.map((n) => vt(n)).filter((n) => n.length > 0) : []; + return Array.isArray(t) ? t.map((n) => Ct(n)).filter((n) => n.length > 0) : []; } _plates() { return this._childSlugs().map((e) => this._plate(e)); } _plate(e) { - const t = this._dueSensorResolution(e), n = this._state(`sensor.nestquest_${e}_quests_due_today`), i = this._state( + const t = this._dueSensorResolution(e), n = this._state(`sensor.nestquest_${e}_quests_due_today`), s = this._state( `sensor.nestquest_${e}_quests_completed_today` ), a = this._state( `sensor.nestquest_${e}_completion_pct_today` ), r = this._state( `binary_sensor.nestquest_${e}_present_today` - ), l = n?.attributes ?? {}, o = $e(X(n?.state, 0)), p = $e(X(i?.state, 0)), u = a ? X(a.state, Number.NaN) : Number.NaN, c = Number.isFinite(u) ? ke(u) : o > 0 ? ke(p / o * 100) : 0; + ), l = n?.attributes ?? {}, o = ze(te(n?.state, 0)), p = ze(te(s?.state, 0)), u = a ? te(a.state, Number.NaN) : Number.NaN, c = Number.isFinite(u) ? Se(u) : o > 0 ? Se(p / o * 100) : 0; let f = !0; const h = l.present; typeof h == "boolean" ? f = h : r && (f = String(r.state ?? "").trim().toLowerCase() === "on"); @@ -1264,14 +1289,14 @@ class Bt extends M { if (typeof _ == "string" && _.trim()) m = _.trim(); else { - const G = r?.attributes?.child_name; - typeof G == "string" && G.trim() && (m = G.trim()); + const X = r?.attributes?.child_name; + typeof X == "string" && X.trim() && (m = X.trim()); } - m || (m = xt(e)); - const x = kt( + m || (m = At(e)); + const v = zt( r?.attributes?.next_present, this._timeZone() - ), R = x ? `Returns ${J(x, this._timeZone())}` : null; + ), R = v ? `Returns ${ee(v, this._timeZone())}` : null; return { slug: e, name: m, @@ -1297,7 +1322,7 @@ class Bt extends M { } _boardNotice() { const e = this._plates(); - return e.length === 0 ? Ce : e.every((t) => t.unresolved !== null) ? e.some((t) => t.unresolved === "stale") ? Nt : Ce : null; + return e.length === 0 ? Ne : e.every((t) => t.unresolved !== null) ? e.some((t) => t.unresolved === "stale") ? jt : Ne : null; } _renderNotice(e) { return d` @@ -1320,7 +1345,7 @@ class Bt extends M { ${e.name} - ${Tt} + ${Ht} Unknown — @@ -1340,7 +1365,7 @@ class Bt extends M { ${e.name} - ${At} + ${It} Home today @@ -1359,7 +1384,7 @@ class Bt extends M { ${e.name} - ${St} + ${Pt} On travels ${e.returns ?? "Returns —"} @@ -1368,11 +1393,11 @@ class Bt extends M { `; } _openQuestLog(e) { - const t = we(this._config?.quest_log_path); + const t = Ae(this._config?.quest_log_path); t && (window.history.pushState(null, "", `${t.replace(/\/+$/, "")}/${e}`), window.dispatchEvent(new Event("location-changed"))); } _dockWeather() { - const e = we(this._config?.weather_entity); + const e = Ae(this._config?.weather_entity); if (!e) return null; const t = this._state(e); @@ -1381,9 +1406,9 @@ class Bt extends M { const n = String(t.state ?? "").trim(); if (!n || n === "unavailable" || n === "unknown") return null; - const i = t.attributes ?? {}; + const s = t.attributes ?? {}; let a = null, r = null; - const l = i.forecast; + const l = s.forecast; if (Array.isArray(l) && l.length > 0) { const o = l[0]; if (o && typeof o == "object") { @@ -1393,7 +1418,7 @@ class Bt extends M { } return { condition: n, - temperature: this._optionalNumber(i.temperature), + temperature: this._optionalNumber(s.temperature), high: a, low: r }; @@ -1409,16 +1434,16 @@ class Bt extends M { if (!e) return d`
- ${J(this._now, this._timeZone())} + ${ee(this._now, this._timeZone())} - ${_t(this._now, this._timeZone())} + ${$t(this._now, this._timeZone())}
`; - const t = $t(e.condition), n = qt(e.condition, t), i = e.temperature === null ? g : d`${Math.round(e.temperature)}°`, a = e.high === null || e.low === null ? null : `${Math.round(e.high)}° / ${Math.round(e.low)}°`; + const t = Tt(e.condition), n = Et(e.condition, t), s = e.temperature === null ? g : d`${Math.round(e.temperature)}°`, a = e.high === null || e.low === null ? null : `${Math.round(e.high)}° / ${Math.round(e.low)}°`; return d`
- ${Lt} - ${i} + ${Dt} + ${s} ${a === null ? t : `${t} · ${a}`} @@ -1428,15 +1453,21 @@ class Bt extends M {
`; } -} -customElements.define("nestquest-party-board-card", Bt); -le({ +}; +Q.properties = { + hass: { attribute: !1 }, + _config: { state: !0 }, + _now: { state: !0 } +}, Q.styles = [x(Xe), Bt]; +let re = Q; +customElements.define("nestquest-party-board-card", re); +fe({ type: "nestquest-party-board-card", name: "NestQuest Party Board", description: "Kids' attract screen for present and away adventurers." }); -const Ot = { CHILD: 2 }, Pt = (s) => (...e) => ({ _$litDirective$: s, values: e }); -let It = class { +const Ft = { CHILD: 2 }, Vt = (i) => (...e) => ({ _$litDirective$: i, values: e }); +let Zt = class { constructor(e) { } get _$AU() { @@ -1452,79 +1483,79 @@ let It = class { return this.render(...t); } }; -const { I: Ht } = pt, Me = (s) => s, Ae = () => document.createComment(""), B = (s, e, t) => { - const n = s._$AA.parentNode, i = e === void 0 ? s._$AB : e._$AA; +const { I: Qt } = yt, Le = (i) => i, Oe = () => document.createComment(""), O = (i, e, t) => { + const n = i._$AA.parentNode, s = e === void 0 ? i._$AB : e._$AA; if (t === void 0) { - const a = n.insertBefore(Ae(), i), r = n.insertBefore(Ae(), i); - t = new Ht(a, r, s, s.options); + const a = n.insertBefore(Oe(), s), r = n.insertBefore(Oe(), s); + t = new Qt(a, r, i, i.options); } else { - const a = t._$AB.nextSibling, r = t._$AM, l = r !== s; + const a = t._$AB.nextSibling, r = t._$AM, l = r !== i; if (l) { let o; - t._$AQ?.(s), t._$AM = s, t._$AP !== void 0 && (o = s._$AU) !== r._$AU && t._$AP(o); + t._$AQ?.(i), t._$AM = i, t._$AP !== void 0 && (o = i._$AU) !== r._$AU && t._$AP(o); } - if (a !== i || l) { + if (a !== s || l) { let o = t._$AA; for (; o !== a; ) { - const p = Me(o).nextSibling; - Me(n).insertBefore(o, i), o = p; + const p = Le(o).nextSibling; + Le(n).insertBefore(o, s), o = p; } } } return t; -}, q = (s, e, t = s) => (s._$AI(e, t), s), jt = {}, Rt = (s, e = jt) => s._$AH = e, Ut = (s) => s._$AH, Y = (s) => { - s._$AR(), s._$AA.remove(); +}, q = (i, e, t = i) => (i._$AI(e, t), i), Wt = {}, Gt = (i, e = Wt) => i._$AH = e, Kt = (i) => i._$AH, ne = (i) => { + i._$AR(), i._$AA.remove(); }; -const Se = (s, e, t) => { +const Be = (i, e, t) => { const n = /* @__PURE__ */ new Map(); - for (let i = e; i <= t; i++) n.set(s[i], i); + for (let s = e; s <= t; s++) n.set(i[s], s); return n; -}, Te = Pt(class extends It { - constructor(s) { - if (super(s), s.type !== Ot.CHILD) throw Error("repeat() can only be used in text expressions"); +}, Ie = Vt(class extends Zt { + constructor(i) { + if (super(i), i.type !== Ft.CHILD) throw Error("repeat() can only be used in text expressions"); } - dt(s, e, t) { + dt(i, e, t) { let n; t === void 0 ? t = e : e !== void 0 && (n = e); - const i = [], a = []; + const s = [], a = []; let r = 0; - for (const l of s) i[r] = n ? n(l, r) : r, a[r] = t(l, r), r++; - return { values: a, keys: i }; + for (const l of i) s[r] = n ? n(l, r) : r, a[r] = t(l, r), r++; + return { values: a, keys: s }; } - render(s, e, t) { - return this.dt(s, e, t).values; + render(i, e, t) { + return this.dt(i, e, t).values; } - update(s, [e, t, n]) { - const i = Ut(s), { values: a, keys: r } = this.dt(e, t, n); - if (!Array.isArray(i)) return this.ut = r, a; + update(i, [e, t, n]) { + const s = Kt(i), { values: a, keys: r } = this.dt(e, t, n); + if (!Array.isArray(s)) return this.ut = r, a; const l = this.ut ??= [], o = []; - let p, u, c = 0, f = i.length - 1, h = 0, m = a.length - 1; - for (; c <= f && h <= m; ) if (i[c] === null) c++; - else if (i[f] === null) f--; - else if (l[c] === r[h]) o[h] = q(i[c], a[h]), c++, h++; - else if (l[f] === r[m]) o[m] = q(i[f], a[m]), f--, m--; - else if (l[c] === r[m]) o[m] = q(i[c], a[m]), B(s, o[m + 1], i[c]), c++, m--; - else if (l[f] === r[h]) o[h] = q(i[f], a[h]), B(s, i[c], i[f]), f--, h++; - else if (p === void 0 && (p = Se(r, h, m), u = Se(l, c, f)), p.has(l[c])) if (p.has(l[f])) { - const _ = u.get(r[h]), x = _ !== void 0 ? i[_] : null; - if (x === null) { - const R = B(s, i[c]); + let p, u, c = 0, f = s.length - 1, h = 0, m = a.length - 1; + for (; c <= f && h <= m; ) if (s[c] === null) c++; + else if (s[f] === null) f--; + else if (l[c] === r[h]) o[h] = q(s[c], a[h]), c++, h++; + else if (l[f] === r[m]) o[m] = q(s[f], a[m]), f--, m--; + else if (l[c] === r[m]) o[m] = q(s[c], a[m]), O(i, o[m + 1], s[c]), c++, m--; + else if (l[f] === r[h]) o[h] = q(s[f], a[h]), O(i, s[c], s[f]), f--, h++; + else if (p === void 0 && (p = Be(r, h, m), u = Be(l, c, f)), p.has(l[c])) if (p.has(l[f])) { + const _ = u.get(r[h]), v = _ !== void 0 ? s[_] : null; + if (v === null) { + const R = O(i, s[c]); q(R, a[h]), o[h] = R; - } else o[h] = q(x, a[h]), B(s, i[c], x), i[_] = null; + } else o[h] = q(v, a[h]), O(i, s[c], v), s[_] = null; h++; - } else Y(i[f]), f--; - else Y(i[c]), c++; + } else ne(s[f]), f--; + else ne(s[c]), c++; for (; h <= m; ) { - const _ = B(s, o[m + 1]); + const _ = O(i, o[m + 1]); q(_, a[h]), o[h++] = _; } for (; c <= f; ) { - const _ = i[c++]; - _ !== null && Y(_); + const _ = s[c++]; + _ !== null && ne(_); } - return this.ut = r, Rt(s, o), S; + return this.ut = r, Gt(i, o), S; } -}), Dt = [ +}), Jt = [ { kind: "lucide", key: "lucide:bed", @@ -1994,7 +2025,7 @@ const Se = (s, e, t) => { "m3 7 2 2 4-4" ] } -], Ft = [ +], Xt = [ { kind: "fa", key: "fa:broom", @@ -2215,20 +2246,20 @@ const Se = (s, e, t) => { "M384 512L96 512c-53 0-96-43-96-96L0 96C0 43 43 0 96 0L400 0c26.5 0 48 21.5 48 48l0 288c0 20.9-13.4 38.7-32 45.3l0 66.7c17.7 0 32 14.3 32 32s-14.3 32-32 32l-32 0zM96 384c-17.7 0-32 14.3-32 32s14.3 32 32 32l256 0 0-64-256 0zm32-232c0 13.3 10.7 24 24 24l176 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-176 0c-13.3 0-24 10.7-24 24zm24 72c-13.3 0-24 10.7-24 24s10.7 24 24 24l176 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-176 0z" ] } -], O = { kind: "fallback", key: null }, Qt = 8, ze = new Map(Dt.map((s) => [s.name, s])), Vt = new Map(Ft.map((s) => [s.name, s])); -function Zt(s) { - const e = [...s]; - return e.length === 0 || e.length > Qt ? !1 : !e.some((t) => new RegExp("[\\s<>&]|\\p{Cc}", "u").test(t)); +], B = { kind: "fallback", key: null }, Yt = 8, Pe = new Map(Jt.map((i) => [i.name, i])), en = new Map(Xt.map((i) => [i.name, i])); +function tn(i) { + const e = [...i]; + return e.length === 0 || e.length > Yt ? !1 : !e.some((t) => new RegExp("[\\s<>&]|\\p{Cc}", "u").test(t)); } -function Wt(s) { - if (!s || !s.trim()) return O; - const e = s.indexOf(":"); - if (e === -1) return ze.get(s) ?? O; - const t = s.slice(0, e), n = s.slice(e + 1); - return t === "lucide" ? ze.get(n) ?? O : t === "fa" ? Vt.get(n) ?? O : t === "emoji" && Zt(n) ? { kind: "emoji", key: s, text: n } : O; +function nn(i) { + if (!i || !i.trim()) return B; + const e = i.indexOf(":"); + if (e === -1) return Pe.get(i) ?? B; + const t = i.slice(0, e), n = i.slice(e + 1); + return t === "lucide" ? Pe.get(n) ?? B : t === "fa" ? en.get(n) ?? B : t === "emoji" && tn(n) ? { kind: "emoji", key: i, text: n } : B; } -function Gt(s, e) { - const t = Wt(s); +function sn(i, e) { + const t = nn(i); switch (t.kind) { case "lucide": return d``; case "fa": return d``; case "emoji": return d`${t.text}`; @@ -2258,59 +2289,59 @@ function Gt(s, e) { return e; } } -const Kt = [ +const an = [ "nestquest_quest_completed", "nestquest_quest_uncompleted", "nestquest_quest_missed", "nestquest_child_day_complete" -], Ee = /* @__PURE__ */ new Map(); -function E(s, e) { - const t = `${s ?? "local"}|${JSON.stringify(e)}`, n = Ee.get(t); +], He = /* @__PURE__ */ new Map(); +function E(i, e) { + const t = `${i ?? "local"}|${JSON.stringify(e)}`, n = He.get(t); if (n) return n; - let i; + let s; try { - i = new Intl.DateTimeFormat("en-US", { + s = new Intl.DateTimeFormat("en-US", { ...e, - ...s ? { timeZone: s } : {} + ...i ? { timeZone: i } : {} }); } catch { - i = new Intl.DateTimeFormat("en-US", e); + s = new Intl.DateTimeFormat("en-US", e); } - return Ee.set(t, i), i; + return He.set(t, s), s; } -function Jt(s) { - const e = s?.config; +function rn(i) { + const e = i?.config; if (!e || typeof e != "object") return; const t = e.time_zone; return typeof t == "string" && t.trim() ? t.trim() : void 0; } -function U(s, e) { - return `${E(e, { weekday: "long" }).format(s)}, ${E(e, { month: "short", day: "numeric" }).format(s)}`; +function j(i, e) { + return `${E(e, { weekday: "long" }).format(i)}, ${E(e, { month: "short", day: "numeric" }).format(i)}`; } -function Xt(s, e) { - return E(e, { weekday: "long" }).format(s); +function on(i, e) { + return E(e, { weekday: "long" }).format(i); } -function Yt(s, e) { - const t = new Date(s); +function ln(i, e) { + const t = new Date(i); return Number.isNaN(t.getTime()) ? "" : E(e, { hour: "numeric", minute: "2-digit", hour12: !0 }).format(t); } -function en(s, e) { +function cn(i, e) { return E(e, { hour: "numeric", minute: "2-digit", hour12: !0 - }).format(s); + }).format(i); } -function tn(s) { - if (!s) +function dn(i) { + if (!i) return null; - const e = /^(\d{1,2}):(\d{2})$/.exec(s.trim()); + const e = /^(\d{1,2}):(\d{2})$/.exec(i.trim()); if (!e) return null; const t = Number(e[1]); @@ -2319,22 +2350,22 @@ function tn(s) { const n = t >= 12 ? "PM" : "AM"; return `${t % 12 === 0 ? 12 : t % 12}:${e[2]} ${n}`; } -function w(s) { - return typeof s == "string" ? s.trim() : ""; +function w(i) { + return typeof i == "string" ? i.trim() : ""; } -function y(s, e) { - if (s == null || s === "") +function y(i, e) { + if (i == null || i === "") return e; - const t = typeof s == "number" ? s : Number(s); + const t = typeof i == "number" ? i : Number(i); return Number.isFinite(t) ? t : e; } -function b(s) { - return Math.max(0, Math.round(s)); +function b(i) { + return Math.max(0, Math.round(i)); } -function ee(s) { - return s.split(/[-_]+/).filter(Boolean).map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join(" "); +function ie(i) { + return i.split(/[-_]+/).filter(Boolean).map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join(" "); } -function nn(s, e) { +function pn(i, e) { const t = E(e, { year: "numeric", month: "2-digit", @@ -2343,10 +2374,10 @@ function nn(s, e) { minute: "2-digit", second: "2-digit", hourCycle: "h23" - }).formatToParts(new Date(s)), n = (a) => { + }).formatToParts(new Date(i)), n = (a) => { const r = t.find((l) => l.type === a); return r ? Number(r.value) : Number.NaN; - }, i = Date.UTC( + }, s = Date.UTC( n("year"), n("month") - 1, n("day"), @@ -2354,41 +2385,41 @@ function nn(s, e) { n("minute"), n("second") ); - return Number.isFinite(i) ? i - s : 0; + return Number.isFinite(s) ? s - i : 0; } -function Ne(s, e) { - if (typeof s != "string") +function Ue(i, e) { + if (typeof i != "string") return null; - const t = s.split("-"); + const t = i.split("-"); if (t.length !== 3) return null; - const [n, i, a] = t.map((o) => Number(o)); - if (!n || !i || !a) + const [n, s, a] = t.map((o) => Number(o)); + if (!n || !s || !a) return null; if (!e) { - const o = new Date(n, i - 1, a); + const o = new Date(n, s - 1, a); return Number.isNaN(o.getTime()) ? null : o; } - const r = Date.UTC(n, i - 1, a), l = new Date(r - nn(r, e)); + const r = Date.UTC(n, s - 1, a), l = new Date(r - pn(r, e)); return Number.isNaN(l.getTime()) ? null : l; } -function sn() { - const s = window.location.pathname.split("/").filter(Boolean); - return s.length > 0 ? s[s.length - 1].toLowerCase() : ""; +function hn() { + const i = window.location.pathname.split("/").filter(Boolean); + return i.length > 0 ? i[i.length - 1].toLowerCase() : ""; } -function an(s) { - if (!s || typeof s != "object") +function un(i) { + if (!i || typeof i != "object") return null; - const e = s, t = w(e.state).toLowerCase(); + const e = i, t = w(e.state).toLowerCase(); if (t !== "open" && t !== "completed") return null; const n = y(e.id, 0); if (!n) return null; - const i = y(e.child_id, 0); + const s = y(e.child_id, 0); return { id: n, - child_id: i > 0 ? i : null, + child_id: s > 0 ? s : null, title: w(e.title) || "Quest", icon: w(e.icon) || null, window: w(e.window).toLowerCase(), @@ -2399,7 +2430,7 @@ function an(s) { on_time: e.on_time === !0 ? !0 : e.on_time === !1 ? !1 : null }; } -const rn = { +const fn = { "clear-night": "Clear", cloudy: "Cloudy", exceptional: "Clear", @@ -2415,7 +2446,7 @@ const rn = { sunny: "Sunny", windy: "Windy", "windy-variant": "Windy" -}, on = { +}, mn = { "clear-night": "Clear night skies", cloudy: "Grey skies today", exceptional: "A striking day", @@ -2432,14 +2463,14 @@ const rn = { windy: "A blustery day", "windy-variant": "A blustery day" }; -function ln(s) { - const e = rn[s]; - return e || s.charAt(0).toUpperCase() + s.slice(1); +function gn(i) { + const e = fn[i]; + return e || i.charAt(0).toUpperCase() + i.slice(1); } -function cn(s, e) { - return on[s] ?? e; +function _n(i, e) { + return mn[i] ?? e; } -const Le = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", dn = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", Be = [-9, 6, -4], pn = Ie` +const Re = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", yn = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", je = [-9, 6, -4], bn = Ve` * { box-sizing: border-box; } @@ -2498,8 +2529,8 @@ const Le = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", dn = "polygon(50% flex: none; width: 96px; height: 110px; - padding: 3px; - clip-path: ${v(Le)}; + padding: 4px; + clip-path: ${x(Re)}; background: rgba(255, 255, 255, 0.22); } @@ -2510,7 +2541,7 @@ const Le = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", dn = "polygon(50% align-items: center; justify-content: center; padding-bottom: 32px; - clip-path: ${v(Le)}; + clip-path: ${x(Re)}; background: var(--nq-p-crest); } @@ -2576,7 +2607,7 @@ const Le = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", dn = "polygon(50% justify-content: center; width: 44px; height: 44px; - clip-path: ${v(dn)}; + clip-path: ${x(yn)}; background: var(--nq-brand-gradient); } @@ -3301,7 +3332,7 @@ const Le = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", dn = "polygon(50% aria-hidden="true" > -`, hn = d``, vn = d` -`, te = d``, se = d` -`, un = d``, xn = d` -`, Ve = d``, Ye = d` -`, fn = d``, wn = d` -`, mn = d``, kn = d` -`, gn = d``, $n = d` -`, _n = d``, qn = d` -`, yn = d``, Mn = d` -`, bn = d``, Cn = d` -`, vn = { - icon: gn, +`, An = { + icon: $n, headline: "No adventurer chosen", body: "Open The Party and tap your crest to open your quest log." -}, xn = { - icon: _n, +}, Sn = { + icon: qn, headline: "NestQuest is not set up yet", body: "A parent needs to finish setting up NestQuest." -}, wn = { - icon: yn, +}, zn = { + icon: Mn, headline: "The records cannot be reached", body: "The party's records are quiet right now. NestQuest will return shortly." -}, Oe = [ - { key: "morning", name: "Morning", range: "Until 11:59 AM", icon: un }, - { key: "afternoon", name: "Afternoon", range: "12:00–5:00 PM", icon: Ve }, - { key: "evening", name: "Evening", range: "5:00–9:00 PM", icon: fn } -]; -class kn extends M { - static properties = { - hass: { attribute: !1 }, - _config: { state: !0 }, - _now: { state: !0 }, - _confirm: { state: !0 }, - _optimistic: { state: !0 }, - _toast: { state: !0 }, - _countdown: { state: !0 } - }; - static styles = [v(Qe), pn]; - hass; - _config; - _now = /* @__PURE__ */ new Date(); - _confirm = null; - /** instanceId → the completion instant stamped before the service has - * answered; the seal shows immediately and reverts on failure. */ - _optimistic = /* @__PURE__ */ new Map(); - _toast = null; - _countdown = null; - _clockTimer; - _idleTimer; - _confirmTimer; - _toastTimer; - _completeTimer; - _unsubs = []; - _subscribed = !1; - _subGeneration = 0; - _retryTimer; +}, De = [ + { key: "morning", name: "Morning", range: "Until 11:59 AM", icon: xn }, + { key: "afternoon", name: "Afternoon", range: "12:00–5:00 PM", icon: Ye }, + { key: "evening", name: "Evening", range: "5:00–9:00 PM", icon: wn } +], W = class W extends C { + constructor() { + super(...arguments), this._now = /* @__PURE__ */ new Date(), this._confirm = null, this._optimistic = /* @__PURE__ */ new Map(), this._toast = null, this._countdown = null, this._unsubs = [], this._subscribed = !1, this._subGeneration = 0, this._onActivity = () => { + this._logView().kind !== "complete-day" && this._armIdle(), this._confirm !== null && this._armConfirmTimer(); + }; + } setConfig(e) { if (!e || typeof e != "object") throw new Error("Invalid configuration"); @@ -3487,16 +3494,16 @@ class kn extends M { if (!e) return; this._subscribed = !0; - const t = ++this._subGeneration, n = (i) => { - i.then((a) => { + const t = ++this._subGeneration, n = (s) => { + s.then((a) => { this._subscribed && t === this._subGeneration ? this._unsubs.push(a) : a(); }).catch(() => { t === this._subGeneration && (this._unsubscribeLive(), this._armSubscribeRetry()); }); }; - for (const i of Kt) + for (const s of an) n( - e.subscribeEvents(() => this.requestUpdate(), i) + e.subscribeEvents(() => this.requestUpdate(), s) ); } _armSubscribeRetry() { @@ -3547,18 +3554,18 @@ class kn extends M { _renderMain(e) { switch (e.kind) { case "no-adventurer": - return this._renderNotice(vn); + return this._renderNotice(An); case "not-set-up": - return this._renderNotice(xn); + return this._renderNotice(Sn); case "unreachable": - return this._renderNotice(wn); + return this._renderNotice(zn); case "away": { const t = ["The quest log unlocks when they return."]; return e.returns && t.push(`Returns ${e.returns}`), d` ${this._renderHeader(e)}
- ${mn} + ${kn} ${e.name} is on travels ${t.map((n) => d`

${n}

`)}
@@ -3570,7 +3577,7 @@ class kn extends M { ${this._renderHeader(e)}
- ${Ve} + ${Ye} No quests today

${`Nothing was posted for today, ${e.name}. Enjoy the day's rest!`} @@ -3614,7 +3621,7 @@ class kn extends M { return d` ${this._renderHeader(e)}

- ${Oe.map((t) => this._renderColumn(t))} + ${De.map((t) => this._renderColumn(t))}
`; } @@ -3630,11 +3637,11 @@ class kn extends M { return { kind: "not-set-up" }; if (t === "stale") return { kind: "unreachable" }; - const n = this._childName(e) ?? ee(e); + const n = this._childName(e) ?? ie(e); if (!this._childPresent(e)) return { kind: "away", name: n, returns: this._awayReturns(e) }; - const i = this._state(`binary_sensor.nestquest_${e}_all_done`); - if (i && String(i.state ?? "").trim().toLowerCase() === "on") + const s = this._state(`binary_sensor.nestquest_${e}_all_done`); + if (s && String(s.state ?? "").trim().toLowerCase() === "on") return { kind: "complete-day", name: n }; const a = this._state(`sensor.nestquest_${e}_quests_due_today`); return b(y(a?.state, 0)) === 0 ? { kind: "empty-day", name: n } : this._remaining() === 0 ? { kind: "complete-day", name: n } : { kind: "normal" }; @@ -3654,19 +3661,19 @@ class kn extends M { const n = this._state(`sensor.nestquest_${e}_quests_due_today`)?.attributes?.present; if (typeof n == "boolean") return n; - const i = this._state( + const s = this._state( `binary_sensor.nestquest_${e}_present_today` ); - return i ? String(i.state ?? "").trim().toLowerCase() === "on" : !0; + return s ? String(s.state ?? "").trim().toLowerCase() === "on" : !0; } _awayReturns(e) { const t = this._timeZone(), n = this._state( `binary_sensor.nestquest_${e}_present_today` - ), i = Ne( + ), s = Ue( n?.attributes?.next_present, t ); - return i ? U(i, t) : null; + return s ? j(s, t) : null; } _renderNotice(e) { return d` @@ -3680,14 +3687,14 @@ class kn extends M { `; } _timeZone() { - return Jt(this.hass); + return rn(this.hass); } _state(e) { const n = this.hass?.states?.[e]; return n && typeof n == "object" ? n : null; } _childSlug() { - return sn(); + return hn(); } _boardPath() { return w(this._config?.board_path).replace(/\/+$/, ""); @@ -3713,8 +3720,8 @@ class kn extends M { _childCounts(e) { const t = this._state(`sensor.nestquest_${e}_quests_due_today`), n = this._state( `sensor.nestquest_${e}_quests_completed_today` - ), i = b(y(t?.state, 0)), a = b(y(n?.state, 0)); - return { due: i, completed: a }; + ), s = b(y(t?.state, 0)), a = b(y(n?.state, 0)); + return { due: s, completed: a }; } _instances() { const e = this._childSlug(); @@ -3723,21 +3730,21 @@ class kn extends M { const t = this._state(`sensor.nestquest_${e}_quests_due_today`)?.attributes?.instances; if (!Array.isArray(t)) return []; - const n = t.map(an).filter((a) => a !== null); + const n = t.map(un).filter((a) => a !== null); if (this._optimistic.size === 0) return n; - let i = !1; + let s = !1; for (const a of n) { const r = this._optimistic.get(a.id); if (r !== void 0) { if (a.state === "completed") { - this._optimistic.delete(a.id), i = !0; + this._optimistic.delete(a.id), s = !0; continue; } a.state = "completed", a.overdue = !1, a.completed_at = r; } } - return i && (this._optimistic = new Map(this._optimistic)), n; + return s && (this._optimistic = new Map(this._optimistic)), n; } _remaining() { const e = this._childSlug(); @@ -3748,8 +3755,8 @@ class kn extends M { ); if (t) return b(y(t.state, 0)); - const { due: n, completed: i } = this._childCounts(e); - return b(n - i); + const { due: n, completed: s } = this._childCounts(e); + return b(n - s); } _partyCounts() { const e = this._state( @@ -3762,36 +3769,36 @@ class kn extends M { completed: b(y(t?.state, 0)), due: b(y(e?.state, 0)) }; - const n = this._childSlug(), { due: i, completed: a } = this._childCounts(n); - return { completed: a, due: i }; + const n = this._childSlug(), { due: s, completed: a } = this._childCounts(n); + return { completed: a, due: s }; } _completeStats() { const e = this._childCounts(this._childSlug()).completed, t = this._instances().filter( (a) => a.on_time === !0 - ).length, n = b(e - t), i = this._partyCounts(); + ).length, n = b(e - t), s = this._partyCounts(); return { claimed: e, onTime: t, late: n, - party: `${i.completed}/${i.due}` + party: `${s.completed}/${s.due}` }; } _completeSub(e) { - const t = U(this._now, this._timeZone()), { claimed: n, onTime: i, late: a } = this._completeStats(); - return n === 0 ? `${e} sealed the day on ${t}.` : a === 0 ? `${e} claimed every quest on ${t} — all on time.` : i === 0 ? `${e} claimed every quest on ${t} — all late.` : `${e} claimed every quest on ${t} — ${i} on time, ${a} late.`; + const t = j(this._now, this._timeZone()), { claimed: n, onTime: s, late: a } = this._completeStats(); + return n === 0 ? `${e} sealed the day on ${t}.` : a === 0 ? `${e} claimed every quest on ${t} — all on time.` : s === 0 ? `${e} claimed every quest on ${t} — all late.` : `${e} claimed every quest on ${t} — ${s} on time, ${a} late.`; } _otherChildren() { const e = this.hass?.states; if (!e) return []; - const t = this._childSlug(), n = this._timeZone(), i = []; + const t = this._childSlug(), n = this._timeZone(), s = []; for (const a of Object.keys(e)) { const r = /^sensor\.nestquest_(.+)_quests_due_today$/.exec(a); if (!r || r[1] === "household" || r[1] === t) continue; const l = r[1], o = e[a]?.attributes ?? {}; let p = ""; - typeof o.child_name == "string" && o.child_name.trim() ? p = o.child_name.trim() : p = ee(l); + typeof o.child_name == "string" && o.child_name.trim() ? p = o.child_name.trim() : p = ie(l); let u = !0; if (typeof o.present == "boolean") u = o.present; @@ -3808,13 +3815,13 @@ class kn extends M { if (!u) { const _ = this._state( `binary_sensor.nestquest_${l}_present_today` - ), x = Ne( + ), v = Ue( _?.attributes?.next_present, n ); - m = x ? Xt(x, n) : null; + m = v ? on(v, n) : null; } - i.push({ + s.push({ slug: l, name: p, present: u, @@ -3823,13 +3830,13 @@ class kn extends M { returnsWeekday: m }); } - return i.sort((a, r) => a.name.localeCompare(r.name)); + return s.sort((a, r) => a.name.localeCompare(r.name)); } _renderHeader(e) { - const t = this._childSlug(), n = e.kind === "away", i = n ? e.name : this._childName(t) ?? (t ? ee(t) : null), { due: a, completed: r } = this._childCounts(t), l = this._remaining(), o = this._partyCounts(), p = i ? `${i}'s Quest Log` : "Quest Log", u = (i?.charAt(0) || "?").toUpperCase(); + const t = this._childSlug(), n = e.kind === "away", s = n ? e.name : this._childName(t) ?? (t ? ie(t) : null), { due: a, completed: r } = this._childCounts(t), l = this._remaining(), o = this._partyCounts(), p = s ? `${s}'s Quest Log` : "Quest Log", u = (s?.charAt(0) || "?").toUpperCase(); return d`
-
- ${i.length}/${t.length} + ${s.length}/${t.length}
- ${Te( + ${Ie( n, (l) => l.id, (l) => this._renderQuest(l, !1, 0) )} - ${Te( - i, + ${Ie( + s, (l) => l.id, (l, o) => this._renderQuest(l, !0, o) )} @@ -3915,10 +3922,10 @@ class kn extends M { } _renderQuest(e, t, n) { if (t) { - const r = Be[n % Be.length], l = e.completed_at ? Yt(e.completed_at, this._timeZone()) : ""; + const r = je[n % je.length], l = e.completed_at ? ln(e.completed_at, this._timeZone()) : ""; return d`
- +
${e.title} @@ -3935,11 +3942,11 @@ class kn extends M {
`; } - const i = tn(e.due_time), a = e.overdue ? { - text: i ? `Overdue · due ${i}` : "Overdue", + const s = dn(e.due_time), a = e.overdue ? { + text: s ? `Overdue · due ${s}` : "Overdue", late: !0 } : { - text: i ? `Due by ${i}` : "Due today", + text: s ? `Due by ${s}` : "Due today", late: !1 }; return d` @@ -3954,10 +3961,10 @@ class kn extends M { (r.key === "Enter" || r.key === " ") && (r.preventDefault(), this._openConfirm(e.id)); }} > - +
${e.title} - ${a.text} + ${a.text}