Preview future presence overrides in coordinator and fit the party board in 1080px

This commit is contained in:
2026-09-20 11:36:01 -04:00
parent 66a8dd2751
commit a4e612da86
4 changed files with 68 additions and 32 deletions
+9 -4
View File
@@ -50,6 +50,7 @@ from .dao_rules import QuestDefinitionsDao
from .dao_presence import PresenceOverridesDao, PresenceSchedulesDao from .dao_presence import PresenceOverridesDao, PresenceSchedulesDao
from .db import NestQuestDatabase from .db import NestQuestDatabase
from .presence import ( from .presence import (
MAX_PREVIEW_SCAN_DAYS,
PresenceEngine, PresenceEngine,
PresenceOverride, PresenceOverride,
PresenceSchedule, PresenceSchedule,
@@ -167,10 +168,14 @@ class NestQuestCoordinator(DataUpdateCoordinator):
schedules[child.id] = PresenceSchedule.decode( schedules[child.id] = PresenceSchedule.decode(
child.id, schedule.anchor_date, schedule.pattern 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 self.database
).list_by_child_and_range(child.id, today_iso, today_iso) ).list_by_child_and_range(child.id, today_iso, window_end_iso)
if todays: if ranged:
# The engine validates MODEL objects, not DAO records; # The engine validates MODEL objects, not DAO records;
# each record converts losslessly (the schema guarantees # each record converts losslessly (the schema guarantees
# end >= start and a real is_present). # end >= start and a real is_present).
@@ -182,7 +187,7 @@ class NestQuestCoordinator(DataUpdateCoordinator):
record.is_present, record.is_present,
record.note, record.note,
) )
for record in todays for record in ranged
] ]
engine = PresenceEngine(schedules, overrides) engine = PresenceEngine(schedules, overrides)
@@ -603,8 +603,8 @@ const lt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Kt = "polygon(50%
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
min-height: 100vh; height: 100vh;
padding: 64px 110px 150px; padding: 48px 110px 150px;
overflow: hidden; overflow: hidden;
background: var(--nq-p-parchment); background: var(--nq-p-parchment);
box-shadow: var(--nq-p-vignette); 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 { .kicker {
margin: 24px 0 0; margin: 14px 0 0;
font-family: var(--nq-p-font-heading); font-family: var(--nq-p-font-heading);
font-size: 22px; font-size: 22px;
font-weight: 600; font-weight: 600;
@@ -689,7 +689,7 @@ const lt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Kt = "polygon(50%
.plates { .plates {
width: 100%; width: 100%;
margin-top: 72px; margin-top: 42px;
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 56px; 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; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 26px; gap: 20px;
height: 600px; height: 560px;
padding: 44px 28px 40px; padding: 36px 28px 32px;
border: 2px solid var(--nq-p-panel-border); border: 2px solid var(--nq-p-panel-border);
border-radius: var(--nq-p-radius-panel); border-radius: var(--nq-p-radius-panel);
background: var(--nq-p-card-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 { .crest {
position: relative; position: relative;
flex: none; flex: none;
width: 188px; width: 168px;
height: 214px; height: 192px;
padding: 4px; padding: 4px;
clip-path: ${$(lt)}; clip-path: ${$(lt)};
background: rgba(255, 255, 255, 0.22); 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; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding-bottom: 78px; padding-bottom: 70px;
clip-path: ${$(lt)}; clip-path: ${$(lt)};
background: var(--nq-p-crest); 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; display: inline-flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 10px 24px; padding: 8px 20px;
border-radius: var(--nq-p-radius-pill); border-radius: var(--nq-p-radius-pill);
background: rgba(22, 120, 60, 0.35); background: rgba(22, 120, 60, 0.35);
font-family: var(--nq-p-font-heading); 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 { .progress-line {
font-family: var(--nq-p-font-heading); font-family: var(--nq-p-font-heading);
font-size: 26px; font-size: 24px;
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
color: var(--nq-p-ink-secondary); 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 { .hint {
margin: 40px 0 0; margin: 26px 0 0;
font-family: var(--nq-p-font-body); font-family: var(--nq-p-font-body);
font-size: 27px; font-size: 24px;
font-weight: 600; font-weight: 600;
line-height: 1.4; line-height: 1.4;
text-align: center; text-align: center;
+14 -14
View File
@@ -160,8 +160,8 @@ const boardStyles = css`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
min-height: 100vh; height: 100vh;
padding: 64px 110px 150px; padding: 48px 110px 150px;
overflow: hidden; overflow: hidden;
background: var(--nq-p-parchment); background: var(--nq-p-parchment);
box-shadow: var(--nq-p-vignette); box-shadow: var(--nq-p-vignette);
@@ -233,7 +233,7 @@ const boardStyles = css`
} }
.kicker { .kicker {
margin: 24px 0 0; margin: 14px 0 0;
font-family: var(--nq-p-font-heading); font-family: var(--nq-p-font-heading);
font-size: 22px; font-size: 22px;
font-weight: 600; font-weight: 600;
@@ -246,7 +246,7 @@ const boardStyles = css`
.plates { .plates {
width: 100%; width: 100%;
margin-top: 72px; margin-top: 42px;
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 56px; gap: 56px;
@@ -258,9 +258,9 @@ const boardStyles = css`
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 26px; gap: 20px;
height: 600px; height: 560px;
padding: 44px 28px 40px; padding: 36px 28px 32px;
border: 2px solid var(--nq-p-panel-border); border: 2px solid var(--nq-p-panel-border);
border-radius: var(--nq-p-radius-panel); border-radius: var(--nq-p-radius-panel);
background: var(--nq-p-card-panel); background: var(--nq-p-card-panel);
@@ -294,8 +294,8 @@ const boardStyles = css`
.crest { .crest {
position: relative; position: relative;
flex: none; flex: none;
width: 188px; width: 168px;
height: 214px; height: 192px;
padding: 4px; padding: 4px;
clip-path: ${unsafeCSS(SHIELD_CLIP)}; clip-path: ${unsafeCSS(SHIELD_CLIP)};
background: rgba(255, 255, 255, 0.22); background: rgba(255, 255, 255, 0.22);
@@ -307,7 +307,7 @@ const boardStyles = css`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding-bottom: 78px; padding-bottom: 70px;
clip-path: ${unsafeCSS(SHIELD_CLIP)}; clip-path: ${unsafeCSS(SHIELD_CLIP)};
background: var(--nq-p-crest); background: var(--nq-p-crest);
} }
@@ -341,7 +341,7 @@ const boardStyles = css`
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 10px 24px; padding: 8px 20px;
border-radius: var(--nq-p-radius-pill); border-radius: var(--nq-p-radius-pill);
background: rgba(22, 120, 60, 0.35); background: rgba(22, 120, 60, 0.35);
font-family: var(--nq-p-font-heading); font-family: var(--nq-p-font-heading);
@@ -370,7 +370,7 @@ const boardStyles = css`
.progress-line { .progress-line {
font-family: var(--nq-p-font-heading); font-family: var(--nq-p-font-heading);
font-size: 26px; font-size: 24px;
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
color: var(--nq-p-ink-secondary); color: var(--nq-p-ink-secondary);
@@ -393,9 +393,9 @@ const boardStyles = css`
} }
.hint { .hint {
margin: 40px 0 0; margin: 26px 0 0;
font-family: var(--nq-p-font-body); font-family: var(--nq-p-font-body);
font-size: 27px; font-size: 24px;
font-weight: 600; font-weight: 600;
line-height: 1.4; line-height: 1.4;
text-align: center; text-align: center;
+31
View File
@@ -218,6 +218,37 @@ async def test_coordinator_presence_resolves_schedule_and_override(
assert snapshot.children[0].present is True 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( async def test_coordinator_derives_missed_for_past_due_open(
hass, make_entry hass, make_entry
) -> None: ) -> None: