diff --git a/custom_components/nestquest/coordinator.py b/custom_components/nestquest/coordinator.py index 9a58831..39dc777 100644 --- a/custom_components/nestquest/coordinator.py +++ b/custom_components/nestquest/coordinator.py @@ -50,6 +50,7 @@ from .dao_rules import QuestDefinitionsDao from .dao_presence import PresenceOverridesDao, PresenceSchedulesDao from .db import NestQuestDatabase from .presence import ( + MAX_PREVIEW_SCAN_DAYS, PresenceEngine, PresenceOverride, PresenceSchedule, @@ -167,10 +168,14 @@ class NestQuestCoordinator(DataUpdateCoordinator): schedules[child.id] = PresenceSchedule.decode( child.id, schedule.anchor_date, schedule.pattern ) - todays = await PresenceOverridesDao( + window_end = today + datetime.timedelta( + days=MAX_PREVIEW_SCAN_DAYS + ) + window_end_iso = window_end.isoformat() + ranged = await PresenceOverridesDao( self.database - ).list_by_child_and_range(child.id, today_iso, today_iso) - if todays: + ).list_by_child_and_range(child.id, today_iso, window_end_iso) + if ranged: # The engine validates MODEL objects, not DAO records; # each record converts losslessly (the schema guarantees # end >= start and a real is_present). @@ -182,7 +187,7 @@ class NestQuestCoordinator(DataUpdateCoordinator): record.is_present, record.note, ) - for record in todays + for record in ranged ] engine = PresenceEngine(schedules, overrides) diff --git a/custom_components/nestquest/www/nestquest-cards.js b/custom_components/nestquest/www/nestquest-cards.js index d2646b0..adf4f3e 100644 --- a/custom_components/nestquest/www/nestquest-cards.js +++ b/custom_components/nestquest/www/nestquest-cards.js @@ -603,8 +603,8 @@ const lt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Kt = "polygon(50% display: flex; flex-direction: column; align-items: center; - min-height: 100vh; - padding: 64px 110px 150px; + height: 100vh; + padding: 48px 110px 150px; overflow: hidden; background: var(--nq-p-parchment); box-shadow: var(--nq-p-vignette); @@ -676,7 +676,7 @@ const lt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Kt = "polygon(50% } .kicker { - margin: 24px 0 0; + margin: 14px 0 0; font-family: var(--nq-p-font-heading); font-size: 22px; font-weight: 600; @@ -689,7 +689,7 @@ const lt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Kt = "polygon(50% .plates { width: 100%; - margin-top: 72px; + margin-top: 42px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 56px; @@ -701,9 +701,9 @@ const lt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Kt = "polygon(50% flex-direction: column; align-items: center; justify-content: center; - gap: 26px; - height: 600px; - padding: 44px 28px 40px; + gap: 20px; + height: 560px; + padding: 36px 28px 32px; border: 2px solid var(--nq-p-panel-border); border-radius: var(--nq-p-radius-panel); background: var(--nq-p-card-panel); @@ -737,8 +737,8 @@ const lt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Kt = "polygon(50% .crest { position: relative; flex: none; - width: 188px; - height: 214px; + width: 168px; + height: 192px; padding: 4px; clip-path: ${$(lt)}; background: rgba(255, 255, 255, 0.22); @@ -750,7 +750,7 @@ const lt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Kt = "polygon(50% display: flex; align-items: center; justify-content: center; - padding-bottom: 78px; + padding-bottom: 70px; clip-path: ${$(lt)}; background: var(--nq-p-crest); } @@ -784,7 +784,7 @@ const lt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Kt = "polygon(50% display: inline-flex; align-items: center; gap: 10px; - padding: 10px 24px; + padding: 8px 20px; border-radius: var(--nq-p-radius-pill); background: rgba(22, 120, 60, 0.35); font-family: var(--nq-p-font-heading); @@ -813,7 +813,7 @@ const lt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Kt = "polygon(50% .progress-line { font-family: var(--nq-p-font-heading); - font-size: 26px; + font-size: 24px; font-weight: 700; line-height: 1.2; color: var(--nq-p-ink-secondary); @@ -836,9 +836,9 @@ const lt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Kt = "polygon(50% } .hint { - margin: 40px 0 0; + margin: 26px 0 0; font-family: var(--nq-p-font-body); - font-size: 27px; + font-size: 24px; font-weight: 600; line-height: 1.4; text-align: center; diff --git a/frontend/src/cards/party-board.ts b/frontend/src/cards/party-board.ts index 17322d7..7fd428d 100644 --- a/frontend/src/cards/party-board.ts +++ b/frontend/src/cards/party-board.ts @@ -160,8 +160,8 @@ const boardStyles = css` display: flex; flex-direction: column; align-items: center; - min-height: 100vh; - padding: 64px 110px 150px; + height: 100vh; + padding: 48px 110px 150px; overflow: hidden; background: var(--nq-p-parchment); box-shadow: var(--nq-p-vignette); @@ -233,7 +233,7 @@ const boardStyles = css` } .kicker { - margin: 24px 0 0; + margin: 14px 0 0; font-family: var(--nq-p-font-heading); font-size: 22px; font-weight: 600; @@ -246,7 +246,7 @@ const boardStyles = css` .plates { width: 100%; - margin-top: 72px; + margin-top: 42px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 56px; @@ -258,9 +258,9 @@ const boardStyles = css` flex-direction: column; align-items: center; justify-content: center; - gap: 26px; - height: 600px; - padding: 44px 28px 40px; + gap: 20px; + height: 560px; + padding: 36px 28px 32px; border: 2px solid var(--nq-p-panel-border); border-radius: var(--nq-p-radius-panel); background: var(--nq-p-card-panel); @@ -294,8 +294,8 @@ const boardStyles = css` .crest { position: relative; flex: none; - width: 188px; - height: 214px; + width: 168px; + height: 192px; padding: 4px; clip-path: ${unsafeCSS(SHIELD_CLIP)}; background: rgba(255, 255, 255, 0.22); @@ -307,7 +307,7 @@ const boardStyles = css` display: flex; align-items: center; justify-content: center; - padding-bottom: 78px; + padding-bottom: 70px; clip-path: ${unsafeCSS(SHIELD_CLIP)}; background: var(--nq-p-crest); } @@ -341,7 +341,7 @@ const boardStyles = css` display: inline-flex; align-items: center; gap: 10px; - padding: 10px 24px; + padding: 8px 20px; border-radius: var(--nq-p-radius-pill); background: rgba(22, 120, 60, 0.35); font-family: var(--nq-p-font-heading); @@ -370,7 +370,7 @@ const boardStyles = css` .progress-line { font-family: var(--nq-p-font-heading); - font-size: 26px; + font-size: 24px; font-weight: 700; line-height: 1.2; color: var(--nq-p-ink-secondary); @@ -393,9 +393,9 @@ const boardStyles = css` } .hint { - margin: 40px 0 0; + margin: 26px 0 0; font-family: var(--nq-p-font-body); - font-size: 27px; + font-size: 24px; font-weight: 600; line-height: 1.4; text-align: center; diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py index 1c17f6d..415318e 100644 --- a/tests/test_coordinator.py +++ b/tests/test_coordinator.py @@ -218,6 +218,37 @@ async def test_coordinator_presence_resolves_schedule_and_override( assert snapshot.children[0].present is True +async def test_coordinator_next_present_honours_future_override( + hass, make_entry +) -> None: + entry = wire_entry_to_registry(make_entry(), hass.registry) + assert await async_setup_entry(hass, entry) is True + coordinator = entry.runtime_data.coordinator + database = entry.runtime_data.database + await create_child(database, "Ada") + ada = (await list_children(database))[0] + today = datetime.date.today() + tomorrow = today + datetime.timedelta(days=1) + # Scheduled present tomorrow (single-day cycle, weekday set set below + # covers every day), but overridden absent tomorrow. + await PresenceSchedulesDao(database).upsert_by_child( + ada.id, 1, today.isoformat(), "0,1,2,3,4,5,6" + ) + from custom_components.nestquest.dao_presence import ( + PresenceOverridesDao, + ) + + await PresenceOverridesDao(database).create( + ada.id, tomorrow.isoformat(), tomorrow.isoformat(), False + ) + snapshot = await coordinator._async_update_data() + child = snapshot.children[0] + assert child.present is True + assert child.next_present != tomorrow.isoformat(), ( + "a future absent override must remove tomorrow from the preview" + ) + + async def test_coordinator_derives_missed_for_past_due_open( hass, make_entry ) -> None: