Phase 6: fix the live Home Assistant write (Local Calendar, no event id)

The first live run against the new calendar.school entity failed loudly twice,
and both causes are fixed here.

- The Local Calendar rejects `return_response` on calendar.create_event. The
  client retries the call without it and ignores the changed-states array that
  Home Assistant returns instead.
- Home Assistant then reports no event id. The item keeps the description
  marker ([compass-nhs:<id>]) as its reference, so it leaves the candidate set
  and is never written twice. The live retry proved it: both events already in
  the calendar were adopted by their markers, with no duplicate.
- calendar.get_events reports `start` / `end` (a date-time or a bare date), not
  `start_date_time`; the reader now accepts all three shapes.

A model must no longer classify events. The calendar write depends on the
school-event rule, and the LLM labelled every event "normal"; events now keep
the deterministic rule under both classifiers.

Also fix the Phase 5 screenshot script: the calendar banner shares the `.scan`
class, so select each banner by its labelled section.

Docs record the accepted decision: Sam chose a Home Assistant Local Calendar,
so the target is calendar.school.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-15 19:09:06 +11:00
parent f56dbb46ac
commit da57efad73
9 changed files with 279 additions and 68 deletions

View File

@@ -1,8 +1,14 @@
# Phase 6 — the calendar write (Sam's Home Assistant calendar)
Status: **code complete and tested; the calendar target needs Sam's decision**
(AUD-110). The service now writes important school events through the Home
Assistant REST API. Nothing is written until a calendar entity is chosen.
Status: **done** (AUD-110). Sam chose a **Home Assistant Local Calendar**, and
the service writes important school events to it through the Home Assistant REST
API.
| | |
|---|---|
| Target entity | `calendar.school` (Local Calendar "School", added 2026-10-15) |
| Configuration | `environment.d/school-service.env` (gitignored): `SCHOOL_HA_URL`, `SCHOOL_HA_TOKEN`, `SCHOOL_HA_CALENDAR_ENTITY`, `SCHOOL_HA_TIMEOUT` |
| First live events | *Senior Sub-School Community Service for Uniform Infringement* and *End of Year Senior Music Concert — HALL* (2026-10-15) |
## What it does
@@ -82,32 +88,37 @@ scripts/calendar-push.sh push --dry-run --json
The item screen has an **Add to calendar** button for an important future event,
and shows `in the calendar` with the Home Assistant event id once it is written.
## The open decision: which calendar entity?
## Decision (accepted 2026-10-15)
The Home Assistant instance (`192.168.20.30`, HA 2026.9.4) has three calendar
entities, and only two are writable:
The Home Assistant instance (`192.168.20.30`, HA 2026.9.4) had three calendar
entities, and all the writable ones were Google-backed
(`calendar.samuelrolfe_googlemail_com`, `calendar.birthdays`); the brief says
**Sam's Home Assistant calendar (not Google)**, and no non-Google calendar
existed. Sam chose to **add a Local Calendar**, so:
| Entity | Writable | Backing |
|---|---|---|
| `calendar.samuelrolfe_googlemail_com` | yes | Google, synced into Home Assistant |
| `calendar.birthdays` | yes | Google |
| `calendar.holidays_in_australia` | no | Google |
- The Local Calendar integration was added through the Home Assistant config
flow, creating the entity **`calendar.school`** (title "School"). It is
Home-Assistant-local, never touches Google, and the change is purely additive.
- `SCHOOL_HA_CALENDAR_ENTITY=calendar.school`.
The brief says **Sam's Home Assistant calendar (not Google)**. Today Home
Assistant has **no non-Google writable calendar**: the `local_calendar`
integration is not installed, and no CalDAV/remote calendar is configured.
The calendar is a family calendar, so an event carries the child's name only
when it belongs to one child.
So the target must be chosen before the event is written:
## Home Assistant quirks found on the first live run
1. **Add a Local Calendar to Home Assistant** (recommended). One Home Assistant
config-flow entry creates `calendar.school`. It is Home-Assistant-local, never
touches Google, and is entirely additive. The service then uses
`SCHOOL_HA_CALENDAR_ENTITY=calendar.school`.
2. **Use `calendar.samuelrolfe_googlemail_com`.** No Home Assistant change, but
every school event also appears in Sam's personal Google Calendar.
Both are handled, and both were caught by the fail-loudly design rather than
silently writing nothing:
The question is posted on AUD-110. The code needs only the configuration line
changed once the answer is in.
- **The Local Calendar rejects `return_response`.** `calendar.create_event` is
called with the service response to learn the event id; the Local Calendar
answers `Service does not support responses`. The client retries the call
without it. Home Assistant then replies with the changed states, a JSON array,
which the client ignores.
- **The Local Calendar reports no event id.** With no id to store, the item keeps
the description marker (`[compass-nhs:<item id>]`) as its reference, so the
item leaves the candidate set and is never written twice. The live retry
proved it: after a failed first attempt the two events already in the calendar
were adopted by their markers, and nothing was duplicated.
## Where the code lives
@@ -141,8 +152,12 @@ make test # go test ./...
```
The tests cover the Home Assistant client against a fake Home Assistant (create,
read-back, error mapping), the sync run (write once, adopt by marker, dry run,
loud failure, refuse-to-write-blind), the store (candidates, the push log) and
the web surface (the action, the CSRF guard, the failure banner).
read-back, error mapping, the no-response fallback, the `start`/`end` shapes),
the sync run (write once, adopt by marker, dry run, loud failure,
refuse-to-write-blind, no-event-id idempotency), the store (candidates, the push
log), the parser (a model cannot label an event) and the web surface (the
action, the CSRF guard, the failure banner).
Not tested here: the live Home Assistant write. That needs the entity decision.
Live proof on `.13` (2026-10-15): two Compass school events were written to
`calendar.school`, read back through the Home Assistant API, and the dashboard
and item screen show the calendar state.