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 .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)
@@ -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;
+14 -14
View File
@@ -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;
+31
View File
@@ -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: