mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 19:27:02 +01:00
A date in the new shared sickdays pool (not a free day) now means, on the PC: no fallback ring (a ring in progress stops, reason sick_day), no RTC self-wake for that morning (ours is disarmed), no morning-routine trigger, and no workday_penalty.json; a penalty issued on a day later marked sick is retracted (penalty_cancelled, cause sick_day) by the next session refresh or RTC tick. The audit treats the day as one where nothing is expected. All pool access goes through wake_alarm/_sick.py: fail closed (unreadable pool or missing package = not sick), and act modes pull the pool from Firebase first (RTC --arm on a stick-day target, in-window fallback ticks and ring polls, the day's first penalty write) because the sickdays timer pauses through hibernate. Dry runs never pull. Pins: sickdays-v0.1.0, freedays-v0.4.0, crdt-sync-v0.9.2 (needed by both); pre-commit 4.7.0, pylint 4.1.3, ruff 0.17.1 (freshness gate). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
139 lines
3.9 KiB
Python
139 lines
3.9 KiB
Python
"""What the daily audit knows about one morning, as plain data.
|
|
|
|
Split from ``_audit_checks`` (the rules) so the evidence shape can be read on
|
|
its own: the PC's side (named events, when anything was logged, boots and
|
|
resumes, the penalty file) and the phone's side (its event log, escapes, the
|
|
ring screen and the stick streak), plus the day and its alarm.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import TYPE_CHECKING
|
|
|
|
from wake_alarm._enforcement_records import parse_instant
|
|
|
|
if TYPE_CHECKING:
|
|
from datetime import date, datetime
|
|
|
|
from wake_alarm._audit_sources import PcEvent, Up
|
|
from wake_alarm._enforcement_records import Escape, PhoneEvent, StickState
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PcSide:
|
|
"""The PC's own record of the morning."""
|
|
|
|
events: list[PcEvent] = field(default_factory=list)
|
|
seen: list[datetime] = field(default_factory=list)
|
|
ups: list[Up] = field(default_factory=list)
|
|
penalty_issued: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PhoneSide:
|
|
"""What the phone published about the morning."""
|
|
|
|
events: list[PhoneEvent] = field(default_factory=list)
|
|
escapes: list[Escape] = field(default_factory=list)
|
|
ring_started: bool = False
|
|
stick_state: StickState | None = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Evidence:
|
|
"""Everything the audit gathered about ``day``."""
|
|
|
|
day: date
|
|
stick_day: bool
|
|
free_day: bool
|
|
alarm_at: datetime | None
|
|
alarm_source: str
|
|
pc_side: PcSide = field(default_factory=PcSide)
|
|
phone_side: PhoneSide = field(default_factory=PhoneSide)
|
|
sick_day: bool = False
|
|
|
|
@property
|
|
def pc(self) -> list[PcEvent]:
|
|
"""Named PC events."""
|
|
return self.pc_side.events
|
|
|
|
@property
|
|
def pc_seen(self) -> list[datetime]:
|
|
"""When the PC logged anything at all."""
|
|
return self.pc_side.seen
|
|
|
|
@property
|
|
def ups(self) -> list[Up]:
|
|
"""Boots and resumes."""
|
|
return self.pc_side.ups
|
|
|
|
@property
|
|
def penalty_issued(self) -> str | None:
|
|
"""``issued_date`` of the penalty file, if one is on disk."""
|
|
return self.pc_side.penalty_issued
|
|
|
|
@property
|
|
def phone(self) -> list[PhoneEvent]:
|
|
"""The phone's own events."""
|
|
return self.phone_side.events
|
|
|
|
@property
|
|
def escapes(self) -> list[Escape]:
|
|
"""Escapes excusing this day (honoured or not)."""
|
|
return self.phone_side.escapes
|
|
|
|
@property
|
|
def ring_started(self) -> bool:
|
|
"""Whether the phone's ring screen opened this day."""
|
|
return self.phone_side.ring_started
|
|
|
|
@property
|
|
def stick_state(self) -> StickState | None:
|
|
"""The phone's streak, as published now."""
|
|
return self.phone_side.stick_state
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Check:
|
|
"""One expectation and what was observed."""
|
|
|
|
name: str
|
|
ok: bool | None
|
|
detail: str
|
|
|
|
|
|
def ok(name: str, detail: str) -> Check:
|
|
"""An expectation that held."""
|
|
return Check(name, ok=True, detail=detail)
|
|
|
|
|
|
def bad(name: str, detail: str) -> Check:
|
|
"""A mismatch: lands under ``## Flags``."""
|
|
return Check(name, ok=False, detail=detail)
|
|
|
|
|
|
def na(name: str, detail: str) -> Check:
|
|
"""Not applicable to this morning."""
|
|
return Check(name, ok=None, detail=detail)
|
|
|
|
|
|
def pc_on_day(ev: Evidence, name: str) -> list[PcEvent]:
|
|
"""PC events called ``name`` on the audited day itself."""
|
|
return [e for e in ev.pc if e.event == name and e.at.date() == ev.day]
|
|
|
|
|
|
def phone_on_day(ev: Evidence) -> list[PhoneEvent]:
|
|
"""Phone events stamped on the audited day itself."""
|
|
on_day: list[PhoneEvent] = []
|
|
for event in ev.phone:
|
|
at = parse_instant(event.at)
|
|
if at is not None and at.date() == ev.day:
|
|
on_day.append(event)
|
|
return on_day
|
|
|
|
|
|
def phone_kinds(ev: Evidence) -> set[str]:
|
|
"""Phone event kinds recorded on the audited day."""
|
|
return {event.kind for event in phone_on_day(ev)}
|